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