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 : email, name [ :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};
23use serde::{Deserialize, Serialize};
24
25/// Collection of named queries from a queries.qail file
26#[derive(Debug, Clone, Serialize, Deserialize, Default)]
27pub struct QueryFile {
28    pub queries: Vec<QueryDef>,
29}
30
31/// A named query definition
32#[derive(Debug, Clone, Serialize, Deserialize)]
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 statements)
39    pub return_type: Option<ReturnType>,
40    /// The QAIL query body
41    pub body: String,
42    /// Is this an execute (mutation) vs query (read)
43    pub is_execute: bool,
44}
45
46/// Query parameter
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct QueryParam {
49    pub name: String,
50    pub typ: String,
51}
52
53/// Return type for queries
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub enum ReturnType {
56    /// Single result: -> User
57    Single(String),
58    /// Multiple results: -> Vec<User>
59    Vec(String),
60    /// Optional result: -> Option<User>
61    Option(String),
62}
63
64impl QueryFile {
65    /// Parse a query file from `.qail` format string
66    pub fn parse(input: &str) -> Result<Self, String> {
67        match parse_query_file(input) {
68            Ok(("", qf)) => Ok(qf),
69            Ok((remaining, _)) => Err(format!("Unexpected content: '{}'", remaining.trim())),
70            Err(e) => Err(format!("Parse error: {:?}", e)),
71        }
72    }
73
74    /// Find a query by name
75    pub fn find_query(&self, name: &str) -> Option<&QueryDef> {
76        self.queries
77            .iter()
78            .find(|q| q.name.eq_ignore_ascii_case(name))
79    }
80
81    /// Export to JSON
82    pub fn to_json(&self) -> Result<String, String> {
83        serde_json::to_string_pretty(self).map_err(|e| format!("JSON serialization failed: {}", e))
84    }
85
86    /// Import from JSON
87    pub fn from_json(json: &str) -> Result<Self, String> {
88        serde_json::from_str(json).map_err(|e| format!("JSON deserialization failed: {}", e))
89    }
90}
91
92// =============================================================================
93// Parsing Combinators
94// =============================================================================
95
96/// Parse identifier
97fn identifier(input: &str) -> IResult<&str, &str> {
98    take_while1(|c: char| c.is_alphanumeric() || c == '_').parse(input)
99}
100
101/// Skip whitespace and comments
102fn ws_and_comments(input: &str) -> IResult<&str, ()> {
103    let (input, _) = many0(alt((
104        map(multispace1, |_| ()),
105        map((tag("--"), not_line_ending), |_| ()),
106    )))
107    .parse(input)?;
108    Ok((input, ()))
109}
110
111/// Parse a single parameter: name: Type
112fn parse_param(input: &str) -> IResult<&str, QueryParam> {
113    let (input, _) = multispace0(input)?;
114    let (input, name) = identifier(input)?;
115    let (input, _) = multispace0(input)?;
116    let (input, _) = char(':').parse(input)?;
117    let (input, _) = multispace0(input)?;
118    let (input, typ) = identifier(input)?;
119
120    Ok((
121        input,
122        QueryParam {
123            name: name.to_string(),
124            typ: typ.to_string(),
125        },
126    ))
127}
128
129/// Parse parameter list: (param1: Type, param2: Type)
130fn parse_params(input: &str) -> IResult<&str, Vec<QueryParam>> {
131    let (input, _) = char('(').parse(input)?;
132    let (input, params) = separated_list0(char(','), parse_param).parse(input)?;
133    let (input, _) = multispace0(input)?;
134    let (input, _) = char(')').parse(input)?;
135
136    Ok((input, params))
137}
138
139/// Parse return type: -> Type, -> Vec<Type>, -> Option<Type>
140fn parse_return_type(input: &str) -> IResult<&str, ReturnType> {
141    let (input, _) = multispace0(input)?;
142    let (input, _) = tag("->").parse(input)?;
143    let (input, _) = multispace0(input)?;
144
145    // Check for Vec<T> or Option<T>
146    if let Ok((input, _)) = tag::<_, _, nom::error::Error<&str>>("Vec<")(input) {
147        let (input, inner) = take_while1(|c: char| c != '>').parse(input)?;
148        let (input, _) = char('>').parse(input)?;
149        return Ok((input, ReturnType::Vec(inner.to_string())));
150    }
151
152    if let Ok((input, _)) = tag::<_, _, nom::error::Error<&str>>("Option<")(input) {
153        let (input, inner) = take_while1(|c: char| c != '>').parse(input)?;
154        let (input, _) = char('>').parse(input)?;
155        return Ok((input, ReturnType::Option(inner.to_string())));
156    }
157
158    // Single type
159    let (input, typ) = identifier(input)?;
160    Ok((input, ReturnType::Single(typ.to_string())))
161}
162
163/// Parse query body (everything after : until next query/execute or EOF)
164fn parse_body(input: &str) -> IResult<&str, &str> {
165    let (input, _) = multispace0(input)?;
166    let (input, _) = char(':').parse(input)?;
167    let (input, _) = multispace0(input)?;
168
169    // Find end: next "query" or "execute" keyword at line start (after whitespace), or EOF
170    let mut end = input.len();
171
172    for (i, _) in input.char_indices() {
173        if i == 0 || input.as_bytes().get(i.saturating_sub(1)) == Some(&b'\n') {
174            // At start of line, skip whitespace and check for keyword
175            let line_rest = &input[i..];
176            let trimmed = line_rest.trim_start();
177            if trimmed.starts_with("query ") || trimmed.starts_with("execute ") {
178                // Find where the trimmed content starts
179                let ws_len = line_rest.len() - trimmed.len();
180                end = i + ws_len;
181                break;
182            }
183        }
184    }
185
186    let body = input[..end].trim();
187    Ok((&input[end..], body))
188}
189
190/// Parse a single query definition
191fn parse_query_def(input: &str) -> IResult<&str, QueryDef> {
192    let (input, _) = ws_and_comments(input)?;
193
194    // Check for query or execute keyword
195    let (input, is_execute) = alt((
196        map(tag_no_case("query"), |_| false),
197        map(tag_no_case("execute"), |_| true),
198    ))
199    .parse(input)?;
200
201    let (input, _) = multispace1(input)?;
202    let (input, name) = identifier(input)?;
203    let (input, params) = parse_params(input)?;
204
205    // Return type (optional for execute)
206    let (input, return_type) = if is_execute {
207        (input, None)
208    } else {
209        let (input, rt) = parse_return_type(input)?;
210        (input, Some(rt))
211    };
212
213    let (input, body) = parse_body(input)?;
214
215    Ok((
216        input,
217        QueryDef {
218            name: name.to_string(),
219            params,
220            return_type,
221            body: body.to_string(),
222            is_execute,
223        },
224    ))
225}
226
227/// Parse complete query file
228fn parse_query_file(input: &str) -> IResult<&str, QueryFile> {
229    let (input, _) = ws_and_comments(input)?;
230    let (input, queries) = many0(parse_query_def).parse(input)?;
231    let (input, _) = ws_and_comments(input)?;
232
233    Ok((input, QueryFile { queries }))
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    #[test]
241    fn test_parse_simple_query() {
242        let input = r#"
243            query find_user(id: Uuid) -> User:
244              get users where id = :id
245        "#;
246
247        let qf = QueryFile::parse(input).expect("parse failed");
248        assert_eq!(qf.queries.len(), 1);
249
250        let q = &qf.queries[0];
251        assert_eq!(q.name, "find_user");
252        assert!(!q.is_execute);
253        assert_eq!(q.params.len(), 1);
254        assert_eq!(q.params[0].name, "id");
255        assert_eq!(q.params[0].typ, "Uuid");
256        assert!(matches!(q.return_type, Some(ReturnType::Single(ref t)) if t == "User"));
257        assert!(q.body.contains("get users"));
258    }
259
260    #[test]
261    fn test_parse_vec_return() {
262        let input = r#"
263            query list_orders(user_id: Uuid) -> Vec<Order>:
264              get orders where user_id = :user_id order by created_at desc
265        "#;
266
267        let qf = QueryFile::parse(input).expect("parse failed");
268        let q = &qf.queries[0];
269        assert!(matches!(q.return_type, Some(ReturnType::Vec(ref t)) if t == "Order"));
270    }
271
272    #[test]
273    fn test_parse_option_return() {
274        let input = r#"
275            query find_optional(email: String) -> Option<User>:
276              get users where email = :email limit 1
277        "#;
278
279        let qf = QueryFile::parse(input).expect("parse failed");
280        let q = &qf.queries[0];
281        assert!(matches!(q.return_type, Some(ReturnType::Option(ref t)) if t == "User"));
282    }
283
284    #[test]
285    fn test_parse_execute() {
286        let input = r#"
287            execute create_user(email: String, name: String):
288              add::users : email, name [ :email, :name ]
289        "#;
290
291        let qf = QueryFile::parse(input).expect("parse failed");
292        let q = &qf.queries[0];
293        assert!(q.is_execute);
294        assert!(q.return_type.is_none());
295        assert_eq!(q.params.len(), 2);
296    }
297
298    #[test]
299    fn test_parse_multiple_queries() {
300        let input = r#"
301            -- User queries
302            query find_user(id: Uuid) -> User:
303              get users where id = :id
304            
305            query list_users() -> Vec<User>:
306              get users order by created_at desc
307            
308            execute delete_user(id: Uuid):
309              del::users where id = :id
310        "#;
311
312        let qf = QueryFile::parse(input).expect("parse failed");
313        assert_eq!(qf.queries.len(), 3);
314
315        assert_eq!(qf.queries[0].name, "find_user");
316        assert_eq!(qf.queries[1].name, "list_users");
317        assert_eq!(qf.queries[2].name, "delete_user");
318    }
319}