Skip to main content

qail_core/parser/
query_file.rs

1//! Query file parser for `.qail` format.
2//!
3//! Parses named query templates like:
4//! ```text
5//! query find_user_by_email(email: String) -> User:
6//!   get users where email = :email
7//!
8//! query list_orders(user_id: Uuid) -> Vec<Order>:
9//!   get orders where user_id = :user_id order by created_at desc
10//!
11//! execute create_user(email: String, name: String):
12//!   add users fields email, name values :email, :name
13//! ```
14
15use 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/// Collection of named queries from a queries.qail file
25#[derive(Debug, Clone, Default)]
26pub struct QueryFile {
27    /// Named query definitions.
28    pub queries: Vec<QueryDef>,
29}
30
31/// A named query definition
32#[derive(Debug, Clone)]
33pub struct QueryDef {
34    /// Query name (function name)
35    pub name: String,
36    /// Parameters with types
37    pub params: Vec<QueryParam>,
38    /// Return type (None for execute-only queries).
39    pub return_type: Option<ReturnType>,
40    /// The QAIL query body.
41    pub body: String,
42    /// Whether this is an `execute` (write) rather than `query` (read).
43    pub is_execute: bool,
44}
45
46/// Query parameter
47#[derive(Debug, Clone)]
48pub struct QueryParam {
49    /// Parameter name.
50    pub name: String,
51    /// Parameter type (e.g., "Uuid", "String").
52    pub typ: String,
53}
54
55/// Return type for queries
56#[derive(Debug, Clone)]
57pub enum ReturnType {
58    /// Single result: -> User
59    Single(String),
60    /// Multiple results: -> `Vec<User>`
61    Vec(String),
62    /// Optional result: -> `Option<User>`
63    Option(String),
64}
65
66impl QueryFile {
67    /// Parse a query file from `.qail` format string
68    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    /// Find a query by name
77    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
84// =============================================================================
85// Parsing Combinators
86// =============================================================================
87
88/// Parse identifier
89fn identifier(input: &str) -> IResult<&str, &str> {
90    take_while1(|c: char| c.is_alphanumeric() || c == '_').parse(input)
91}
92
93/// Skip whitespace and comments
94fn 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
103/// Parse a single parameter: name: Type
104fn 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
121/// Parse parameter list: (param1: Type, param2: Type)
122fn 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
131/// Parse return type: -> Type, -> Vec<Type>, -> Option<Type>
132fn 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    // Single type
150    let (input, typ) = identifier(input)?;
151    Ok((input, ReturnType::Single(typ.to_string())))
152}
153
154/// Parse query body (everything after : until next query/execute or EOF)
155fn 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    // Find end: next "query" or "execute" keyword at line start (after whitespace), or EOF
161    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            // At start of line, skip whitespace and check for keyword
166            let line_rest = &input[i..];
167            let trimmed = line_rest.trim_start();
168            if trimmed.starts_with("query ") || trimmed.starts_with("execute ") {
169                // Find where the trimmed content starts
170                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
181/// Parse a single query definition
182fn 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    // Return type (optional for execute)
196    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
217/// Parse complete query file
218fn 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}