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    branch::alt,
17    bytes::complete::{tag, tag_no_case, take_while1},
18    character::complete::{multispace0, multispace1, char, not_line_ending},
19    combinator::map,
20    multi::{many0, separated_list0},
21    Parser,
22    IResult,
23};
24use serde::{Deserialize, Serialize};
25
26/// Collection of named queries from a queries.qail file
27#[derive(Debug, Clone, Serialize, Deserialize, Default)]
28pub struct QueryFile {
29    pub queries: Vec<QueryDef>,
30}
31
32/// A named query definition
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct QueryDef {
35    /// Query name (function name)
36    pub name: String,
37    /// Parameters with types
38    pub params: Vec<QueryParam>,
39    /// Return type (None for execute statements)
40    pub return_type: Option<ReturnType>,
41    /// The QAIL query body
42    pub body: String,
43    /// Is this an execute (mutation) vs query (read)
44    pub is_execute: bool,
45}
46
47/// Query parameter
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct QueryParam {
50    pub name: String,
51    pub typ: String,
52}
53
54/// Return type for queries
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub enum ReturnType {
57    /// Single result: -> User
58    Single(String),
59    /// Multiple results: -> Vec<User>
60    Vec(String),
61    /// Optional result: -> Option<User>
62    Option(String),
63}
64
65impl QueryFile {
66    /// Parse a query file from `.qail` format string
67    pub fn parse(input: &str) -> Result<Self, String> {
68        match parse_query_file(input) {
69            Ok(("", qf)) => Ok(qf),
70            Ok((remaining, _)) => Err(format!("Unexpected content: '{}'", remaining.trim())),
71            Err(e) => Err(format!("Parse error: {:?}", e)),
72        }
73    }
74    
75    /// Find a query by name
76    pub fn find_query(&self, name: &str) -> Option<&QueryDef> {
77        self.queries.iter().find(|q| q.name.eq_ignore_ascii_case(name))
78    }
79    
80    /// Export to JSON
81    pub fn to_json(&self) -> Result<String, String> {
82        serde_json::to_string_pretty(self)
83            .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)
89            .map_err(|e| format!("JSON deserialization failed: {}", e))
90    }
91}
92
93// =============================================================================
94// Parsing Combinators
95// =============================================================================
96
97/// Parse identifier
98fn identifier(input: &str) -> IResult<&str, &str> {
99    take_while1(|c: char| c.is_alphanumeric() || c == '_').parse(input)
100}
101
102/// Skip whitespace and comments
103fn ws_and_comments(input: &str) -> IResult<&str, ()> {
104    let (input, _) = many0(alt((
105        map(multispace1, |_| ()),
106        map((tag("--"), not_line_ending), |_| ()),
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((input, QueryParam {
121        name: name.to_string(),
122        typ: typ.to_string(),
123    }))
124}
125
126/// Parse parameter list: (param1: Type, param2: Type)
127fn parse_params(input: &str) -> IResult<&str, Vec<QueryParam>> {
128    let (input, _) = char('(').parse(input)?;
129    let (input, params) = separated_list0(
130        char(','),
131        parse_param,
132    ).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    )).parse(input)?;
199    
200    let (input, _) = multispace1(input)?;
201    let (input, name) = identifier(input)?;
202    let (input, params) = parse_params(input)?;
203    
204    // Return type (optional for execute)
205    let (input, return_type) = if is_execute {
206        (input, None)
207    } else {
208        let (input, rt) = parse_return_type(input)?;
209        (input, Some(rt))
210    };
211    
212    let (input, body) = parse_body(input)?;
213    
214    Ok((input, QueryDef {
215        name: name.to_string(),
216        params,
217        return_type,
218        body: body.to_string(),
219        is_execute,
220    }))
221}
222
223/// Parse complete query file
224fn parse_query_file(input: &str) -> IResult<&str, QueryFile> {
225    let (input, _) = ws_and_comments(input)?;
226    let (input, queries) = many0(parse_query_def).parse(input)?;
227    let (input, _) = ws_and_comments(input)?;
228    
229    Ok((input, QueryFile { queries }))
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235
236    #[test]
237    fn test_parse_simple_query() {
238        let input = r#"
239            query find_user(id: Uuid) -> User:
240              get users where id = :id
241        "#;
242        
243        let qf = QueryFile::parse(input).expect("parse failed");
244        assert_eq!(qf.queries.len(), 1);
245        
246        let q = &qf.queries[0];
247        assert_eq!(q.name, "find_user");
248        assert!(!q.is_execute);
249        assert_eq!(q.params.len(), 1);
250        assert_eq!(q.params[0].name, "id");
251        assert_eq!(q.params[0].typ, "Uuid");
252        assert!(matches!(q.return_type, Some(ReturnType::Single(ref t)) if t == "User"));
253        assert!(q.body.contains("get users"));
254    }
255
256    #[test]
257    fn test_parse_vec_return() {
258        let input = r#"
259            query list_orders(user_id: Uuid) -> Vec<Order>:
260              get orders where user_id = :user_id order by created_at desc
261        "#;
262        
263        let qf = QueryFile::parse(input).expect("parse failed");
264        let q = &qf.queries[0];
265        assert!(matches!(q.return_type, Some(ReturnType::Vec(ref t)) if t == "Order"));
266    }
267
268    #[test]
269    fn test_parse_option_return() {
270        let input = r#"
271            query find_optional(email: String) -> Option<User>:
272              get users where email = :email limit 1
273        "#;
274        
275        let qf = QueryFile::parse(input).expect("parse failed");
276        let q = &qf.queries[0];
277        assert!(matches!(q.return_type, Some(ReturnType::Option(ref t)) if t == "User"));
278    }
279
280    #[test]
281    fn test_parse_execute() {
282        let input = r#"
283            execute create_user(email: String, name: String):
284              add::users : email, name [ :email, :name ]
285        "#;
286        
287        let qf = QueryFile::parse(input).expect("parse failed");
288        let q = &qf.queries[0];
289        assert!(q.is_execute);
290        assert!(q.return_type.is_none());
291        assert_eq!(q.params.len(), 2);
292    }
293
294    #[test]
295    fn test_parse_multiple_queries() {
296        let input = r#"
297            -- User queries
298            query find_user(id: Uuid) -> User:
299              get users where id = :id
300            
301            query list_users() -> Vec<User>:
302              get users order by created_at desc
303            
304            execute delete_user(id: Uuid):
305              del::users where id = :id
306        "#;
307        
308        let qf = QueryFile::parse(input).expect("parse failed");
309        assert_eq!(qf.queries.len(), 3);
310        
311        assert_eq!(qf.queries[0].name, "find_user");
312        assert_eq!(qf.queries[1].name, "list_users");
313        assert_eq!(qf.queries[2].name, "delete_user");
314    }
315}