1use nom::{
16 IResult, Parser,
17 branch::alt,
18 bytes::complete::{tag, tag_no_case, take_while1},
19 character::complete::{char, multispace0, multispace1, not_line_ending},
20 combinator::map,
21 multi::{many0, separated_list0},
22};
23
24#[derive(Debug, Clone, Default)]
26pub struct QueryFile {
27 pub queries: Vec<QueryDef>,
29}
30
31#[derive(Debug, Clone)]
33pub struct QueryDef {
34 pub name: String,
36 pub params: Vec<QueryParam>,
38 pub return_type: Option<ReturnType>,
40 pub body: String,
42 pub is_execute: bool,
44}
45
46#[derive(Debug, Clone)]
48pub struct QueryParam {
49 pub name: String,
51 pub typ: String,
53}
54
55#[derive(Debug, Clone)]
57pub enum ReturnType {
58 Single(String),
60 Vec(String),
62 Option(String),
64}
65
66impl QueryFile {
67 pub fn parse(input: &str) -> Result<Self, String> {
69 match parse_query_file(input) {
70 Ok(("", qf)) => Ok(qf),
71 Ok((remaining, _)) => Err(format!("Unexpected content: '{}'", remaining.trim())),
72 Err(e) => Err(format!("Parse error: {:?}", e)),
73 }
74 }
75
76 pub fn find_query(&self, name: &str) -> Option<&QueryDef> {
78 self.queries
79 .iter()
80 .find(|q| q.name.eq_ignore_ascii_case(name))
81 }
82}
83
84fn identifier(input: &str) -> IResult<&str, &str> {
90 take_while1(|c: char| c.is_alphanumeric() || c == '_').parse(input)
91}
92
93fn ws_and_comments(input: &str) -> IResult<&str, ()> {
95 let (input, _) = many0(alt((
96 map(multispace1, |_| ()),
97 map((tag("--"), not_line_ending), |_| ()),
98 )))
99 .parse(input)?;
100 Ok((input, ()))
101}
102
103fn parse_param(input: &str) -> IResult<&str, QueryParam> {
105 let (input, _) = multispace0(input)?;
106 let (input, name) = identifier(input)?;
107 let (input, _) = multispace0(input)?;
108 let (input, _) = char(':').parse(input)?;
109 let (input, _) = multispace0(input)?;
110 let (input, typ) = identifier(input)?;
111
112 Ok((
113 input,
114 QueryParam {
115 name: name.to_string(),
116 typ: typ.to_string(),
117 },
118 ))
119}
120
121fn parse_params(input: &str) -> IResult<&str, Vec<QueryParam>> {
123 let (input, _) = char('(').parse(input)?;
124 let (input, params) = separated_list0(char(','), parse_param).parse(input)?;
125 let (input, _) = multispace0(input)?;
126 let (input, _) = char(')').parse(input)?;
127
128 Ok((input, params))
129}
130
131fn parse_return_type(input: &str) -> IResult<&str, ReturnType> {
133 let (input, _) = multispace0(input)?;
134 let (input, _) = tag("->").parse(input)?;
135 let (input, _) = multispace0(input)?;
136
137 if let Ok((input, _)) = tag::<_, _, nom::error::Error<&str>>("Vec<")(input) {
138 let (input, inner) = take_while1(|c: char| c != '>').parse(input)?;
139 let (input, _) = char('>').parse(input)?;
140 return Ok((input, ReturnType::Vec(inner.to_string())));
141 }
142
143 if let Ok((input, _)) = tag::<_, _, nom::error::Error<&str>>("Option<")(input) {
144 let (input, inner) = take_while1(|c: char| c != '>').parse(input)?;
145 let (input, _) = char('>').parse(input)?;
146 return Ok((input, ReturnType::Option(inner.to_string())));
147 }
148
149 let (input, typ) = identifier(input)?;
151 Ok((input, ReturnType::Single(typ.to_string())))
152}
153
154fn parse_body(input: &str) -> IResult<&str, &str> {
156 let (input, _) = multispace0(input)?;
157 let (input, _) = char(':').parse(input)?;
158 let (input, _) = multispace0(input)?;
159
160 let mut end = input.len();
162
163 for (i, _) in input.char_indices() {
164 if i == 0 || input.as_bytes().get(i.saturating_sub(1)) == Some(&b'\n') {
165 let line_rest = &input[i..];
167 let trimmed = line_rest.trim_start();
168 if trimmed.starts_with("query ") || trimmed.starts_with("execute ") {
169 let ws_len = line_rest.len() - trimmed.len();
171 end = i + ws_len;
172 break;
173 }
174 }
175 }
176
177 let body = input[..end].trim();
178 Ok((&input[end..], body))
179}
180
181fn parse_query_def(input: &str) -> IResult<&str, QueryDef> {
183 let (input, _) = ws_and_comments(input)?;
184
185 let (input, is_execute) = alt((
186 map(tag_no_case("query"), |_| false),
187 map(tag_no_case("execute"), |_| true),
188 ))
189 .parse(input)?;
190
191 let (input, _) = multispace1(input)?;
192 let (input, name) = identifier(input)?;
193 let (input, params) = parse_params(input)?;
194
195 let (input, return_type) = if is_execute {
197 (input, None)
198 } else {
199 let (input, rt) = parse_return_type(input)?;
200 (input, Some(rt))
201 };
202
203 let (input, body) = parse_body(input)?;
204
205 Ok((
206 input,
207 QueryDef {
208 name: name.to_string(),
209 params,
210 return_type,
211 body: body.to_string(),
212 is_execute,
213 },
214 ))
215}
216
217fn parse_query_file(input: &str) -> IResult<&str, QueryFile> {
219 let (input, _) = ws_and_comments(input)?;
220 let (input, queries) = many0(parse_query_def).parse(input)?;
221 let (input, _) = ws_and_comments(input)?;
222
223 Ok((input, QueryFile { queries }))
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 #[test]
231 fn test_parse_simple_query() {
232 let input = r#"
233 query find_user(id: Uuid) -> User:
234 get users where id = :id
235 "#;
236
237 let qf = QueryFile::parse(input).expect("parse failed");
238 assert_eq!(qf.queries.len(), 1);
239
240 let q = &qf.queries[0];
241 assert_eq!(q.name, "find_user");
242 assert!(!q.is_execute);
243 assert_eq!(q.params.len(), 1);
244 assert_eq!(q.params[0].name, "id");
245 assert_eq!(q.params[0].typ, "Uuid");
246 assert!(matches!(q.return_type, Some(ReturnType::Single(ref t)) if t == "User"));
247 assert!(q.body.contains("get users"));
248 }
249
250 #[test]
251 fn test_parse_vec_return() {
252 let input = r#"
253 query list_orders(user_id: Uuid) -> Vec<Order>:
254 get orders where user_id = :user_id order by created_at desc
255 "#;
256
257 let qf = QueryFile::parse(input).expect("parse failed");
258 let q = &qf.queries[0];
259 assert!(matches!(q.return_type, Some(ReturnType::Vec(ref t)) if t == "Order"));
260 }
261
262 #[test]
263 fn test_parse_option_return() {
264 let input = r#"
265 query find_optional(email: String) -> Option<User>:
266 get users where email = :email limit 1
267 "#;
268
269 let qf = QueryFile::parse(input).expect("parse failed");
270 let q = &qf.queries[0];
271 assert!(matches!(q.return_type, Some(ReturnType::Option(ref t)) if t == "User"));
272 }
273
274 #[test]
275 fn test_parse_execute() {
276 let input = r#"
277 execute create_user(email: String, name: String):
278 add users fields email, name values :email, :name
279 "#;
280
281 let qf = QueryFile::parse(input).expect("parse failed");
282 let q = &qf.queries[0];
283 assert!(q.is_execute);
284 assert!(q.return_type.is_none());
285 assert_eq!(q.params.len(), 2);
286 }
287
288 #[test]
289 fn test_parse_multiple_queries() {
290 let input = r#"
291 -- User queries
292 query find_user(id: Uuid) -> User:
293 get users where id = :id
294
295 query list_users() -> Vec<User>:
296 get users order by created_at desc
297
298 execute delete_user(id: Uuid):
299 del users where id = :id
300 "#;
301
302 let qf = QueryFile::parse(input).expect("parse failed");
303 assert_eq!(qf.queries.len(), 3);
304
305 assert_eq!(qf.queries[0].name, "find_user");
306 assert_eq!(qf.queries[1].name, "list_users");
307 assert_eq!(qf.queries[2].name, "delete_user");
308 }
309}