use nom::{
IResult, Parser,
branch::alt,
bytes::complete::{tag, tag_no_case, take_while1},
character::complete::{char, multispace0, multispace1, not_line_ending},
combinator::map,
multi::{many0, separated_list0},
};
#[derive(Debug, Clone, Default)]
pub struct QueryFile {
pub queries: Vec<QueryDef>,
}
#[derive(Debug, Clone)]
pub struct QueryDef {
pub name: String,
pub params: Vec<QueryParam>,
pub return_type: Option<ReturnType>,
pub body: String,
pub is_execute: bool,
}
#[derive(Debug, Clone)]
pub struct QueryParam {
pub name: String,
pub typ: String,
}
#[derive(Debug, Clone)]
pub enum ReturnType {
Single(String),
Vec(String),
Option(String),
}
impl QueryFile {
pub fn parse(input: &str) -> Result<Self, String> {
match parse_query_file(input) {
Ok(("", qf)) => Ok(qf),
Ok((remaining, _)) => Err(format!("Unexpected content: '{}'", remaining.trim())),
Err(e) => Err(format!("Parse error: {:?}", e)),
}
}
pub fn find_query(&self, name: &str) -> Option<&QueryDef> {
self.queries
.iter()
.find(|q| q.name.eq_ignore_ascii_case(name))
}
}
fn identifier(input: &str) -> IResult<&str, &str> {
take_while1(|c: char| c.is_alphanumeric() || c == '_').parse(input)
}
fn ws_and_comments(input: &str) -> IResult<&str, ()> {
let (input, _) = many0(alt((
map(multispace1, |_| ()),
map((tag("--"), not_line_ending), |_| ()),
)))
.parse(input)?;
Ok((input, ()))
}
fn parse_param(input: &str) -> IResult<&str, QueryParam> {
let (input, _) = multispace0(input)?;
let (input, name) = identifier(input)?;
let (input, _) = multispace0(input)?;
let (input, _) = char(':').parse(input)?;
let (input, _) = multispace0(input)?;
let (input, typ) = identifier(input)?;
Ok((
input,
QueryParam {
name: name.to_string(),
typ: typ.to_string(),
},
))
}
fn parse_params(input: &str) -> IResult<&str, Vec<QueryParam>> {
let (input, _) = char('(').parse(input)?;
let (input, params) = separated_list0(char(','), parse_param).parse(input)?;
let (input, _) = multispace0(input)?;
let (input, _) = char(')').parse(input)?;
Ok((input, params))
}
fn parse_return_type(input: &str) -> IResult<&str, ReturnType> {
let (input, _) = multispace0(input)?;
let (input, _) = tag("->").parse(input)?;
let (input, _) = multispace0(input)?;
if let Ok((input, _)) = tag::<_, _, nom::error::Error<&str>>("Vec<")(input) {
let (input, inner) = take_while1(|c: char| c != '>').parse(input)?;
let (input, _) = char('>').parse(input)?;
return Ok((input, ReturnType::Vec(inner.to_string())));
}
if let Ok((input, _)) = tag::<_, _, nom::error::Error<&str>>("Option<")(input) {
let (input, inner) = take_while1(|c: char| c != '>').parse(input)?;
let (input, _) = char('>').parse(input)?;
return Ok((input, ReturnType::Option(inner.to_string())));
}
let (input, typ) = identifier(input)?;
Ok((input, ReturnType::Single(typ.to_string())))
}
fn parse_body(input: &str) -> IResult<&str, &str> {
let (input, _) = multispace0(input)?;
let (input, _) = char(':').parse(input)?;
let (input, _) = multispace0(input)?;
let mut end = input.len();
for (i, _) in input.char_indices() {
if i == 0 || input.as_bytes().get(i.saturating_sub(1)) == Some(&b'\n') {
let line_rest = &input[i..];
let trimmed = line_rest.trim_start();
if trimmed.starts_with("query ") || trimmed.starts_with("execute ") {
let ws_len = line_rest.len() - trimmed.len();
end = i + ws_len;
break;
}
}
}
let body = input[..end].trim();
Ok((&input[end..], body))
}
fn parse_query_def(input: &str) -> IResult<&str, QueryDef> {
let (input, _) = ws_and_comments(input)?;
let (input, is_execute) = alt((
map(tag_no_case("query"), |_| false),
map(tag_no_case("execute"), |_| true),
))
.parse(input)?;
let (input, _) = multispace1(input)?;
let (input, name) = identifier(input)?;
let (input, params) = parse_params(input)?;
let (input, return_type) = if is_execute {
(input, None)
} else {
let (input, rt) = parse_return_type(input)?;
(input, Some(rt))
};
let (input, body) = parse_body(input)?;
Ok((
input,
QueryDef {
name: name.to_string(),
params,
return_type,
body: body.to_string(),
is_execute,
},
))
}
fn parse_query_file(input: &str) -> IResult<&str, QueryFile> {
let (input, _) = ws_and_comments(input)?;
let (input, queries) = many0(parse_query_def).parse(input)?;
let (input, _) = ws_and_comments(input)?;
Ok((input, QueryFile { queries }))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_query() {
let input = r#"
query find_user(id: Uuid) -> User:
get users where id = :id
"#;
let qf = QueryFile::parse(input).expect("parse failed");
assert_eq!(qf.queries.len(), 1);
let q = &qf.queries[0];
assert_eq!(q.name, "find_user");
assert!(!q.is_execute);
assert_eq!(q.params.len(), 1);
assert_eq!(q.params[0].name, "id");
assert_eq!(q.params[0].typ, "Uuid");
assert!(matches!(q.return_type, Some(ReturnType::Single(ref t)) if t == "User"));
assert!(q.body.contains("get users"));
}
#[test]
fn test_parse_vec_return() {
let input = r#"
query list_orders(user_id: Uuid) -> Vec<Order>:
get orders where user_id = :user_id order by created_at desc
"#;
let qf = QueryFile::parse(input).expect("parse failed");
let q = &qf.queries[0];
assert!(matches!(q.return_type, Some(ReturnType::Vec(ref t)) if t == "Order"));
}
#[test]
fn test_parse_option_return() {
let input = r#"
query find_optional(email: String) -> Option<User>:
get users where email = :email limit 1
"#;
let qf = QueryFile::parse(input).expect("parse failed");
let q = &qf.queries[0];
assert!(matches!(q.return_type, Some(ReturnType::Option(ref t)) if t == "User"));
}
#[test]
fn test_parse_execute() {
let input = r#"
execute create_user(email: String, name: String):
add users fields email, name values :email, :name
"#;
let qf = QueryFile::parse(input).expect("parse failed");
let q = &qf.queries[0];
assert!(q.is_execute);
assert!(q.return_type.is_none());
assert_eq!(q.params.len(), 2);
}
#[test]
fn test_parse_multiple_queries() {
let input = r#"
-- User queries
query find_user(id: Uuid) -> User:
get users where id = :id
query list_users() -> Vec<User>:
get users order by created_at desc
execute delete_user(id: Uuid):
del users where id = :id
"#;
let qf = QueryFile::parse(input).expect("parse failed");
assert_eq!(qf.queries.len(), 3);
assert_eq!(qf.queries[0].name, "find_user");
assert_eq!(qf.queries[1].name, "list_users");
assert_eq!(qf.queries[2].name, "delete_user");
}
}