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    pub return_type: Option<ReturnType>,
39    /// The QAIL query body
40    pub body: String,
41    pub is_execute: bool,
42}
43
44/// Query parameter
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct QueryParam {
47    pub name: String,
48    pub typ: String,
49}
50
51/// Return type for queries
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub enum ReturnType {
54    /// Single result: -> User
55    Single(String),
56    /// Multiple results: -> Vec<User>
57    Vec(String),
58    /// Optional result: -> Option<User>
59    Option(String),
60}
61
62impl QueryFile {
63    /// Parse a query file from `.qail` format string
64    pub fn parse(input: &str) -> Result<Self, String> {
65        match parse_query_file(input) {
66            Ok(("", qf)) => Ok(qf),
67            Ok((remaining, _)) => Err(format!("Unexpected content: '{}'", remaining.trim())),
68            Err(e) => Err(format!("Parse error: {:?}", e)),
69        }
70    }
71
72    /// Find a query by name
73    pub fn find_query(&self, name: &str) -> Option<&QueryDef> {
74        self.queries
75            .iter()
76            .find(|q| q.name.eq_ignore_ascii_case(name))
77    }
78
79    /// Export to JSON
80    pub fn to_json(&self) -> Result<String, String> {
81        serde_json::to_string_pretty(self).map_err(|e| format!("JSON serialization failed: {}", e))
82    }
83
84    /// Import from JSON
85    pub fn from_json(json: &str) -> Result<Self, String> {
86        serde_json::from_str(json).map_err(|e| format!("JSON deserialization failed: {}", e))
87    }
88}
89
90// =============================================================================
91// Parsing Combinators
92// =============================================================================
93
94/// Parse identifier
95fn identifier(input: &str) -> IResult<&str, &str> {
96    take_while1(|c: char| c.is_alphanumeric() || c == '_').parse(input)
97}
98
99/// Skip whitespace and comments
100fn ws_and_comments(input: &str) -> IResult<&str, ()> {
101    let (input, _) = many0(alt((
102        map(multispace1, |_| ()),
103        map((tag("--"), not_line_ending), |_| ()),
104    )))
105    .parse(input)?;
106    Ok((input, ()))
107}
108
109/// Parse a single parameter: name: Type
110fn parse_param(input: &str) -> IResult<&str, QueryParam> {
111    let (input, _) = multispace0(input)?;
112    let (input, name) = identifier(input)?;
113    let (input, _) = multispace0(input)?;
114    let (input, _) = char(':').parse(input)?;
115    let (input, _) = multispace0(input)?;
116    let (input, typ) = identifier(input)?;
117
118    Ok((
119        input,
120        QueryParam {
121            name: name.to_string(),
122            typ: typ.to_string(),
123        },
124    ))
125}
126
127/// Parse parameter list: (param1: Type, param2: Type)
128fn parse_params(input: &str) -> IResult<&str, Vec<QueryParam>> {
129    let (input, _) = char('(').parse(input)?;
130    let (input, params) = separated_list0(char(','), parse_param).parse(input)?;
131    let (input, _) = multispace0(input)?;
132    let (input, _) = char(')').parse(input)?;
133
134    Ok((input, params))
135}
136
137/// Parse return type: -> Type, -> Vec<Type>, -> Option<Type>
138fn parse_return_type(input: &str) -> IResult<&str, ReturnType> {
139    let (input, _) = multispace0(input)?;
140    let (input, _) = tag("->").parse(input)?;
141    let (input, _) = multispace0(input)?;
142
143    if let Ok((input, _)) = tag::<_, _, nom::error::Error<&str>>("Vec<")(input) {
144        let (input, inner) = take_while1(|c: char| c != '>').parse(input)?;
145        let (input, _) = char('>').parse(input)?;
146        return Ok((input, ReturnType::Vec(inner.to_string())));
147    }
148
149    if let Ok((input, _)) = tag::<_, _, nom::error::Error<&str>>("Option<")(input) {
150        let (input, inner) = take_while1(|c: char| c != '>').parse(input)?;
151        let (input, _) = char('>').parse(input)?;
152        return Ok((input, ReturnType::Option(inner.to_string())));
153    }
154
155    // Single type
156    let (input, typ) = identifier(input)?;
157    Ok((input, ReturnType::Single(typ.to_string())))
158}
159
160/// Parse query body (everything after : until next query/execute or EOF)
161fn parse_body(input: &str) -> IResult<&str, &str> {
162    let (input, _) = multispace0(input)?;
163    let (input, _) = char(':').parse(input)?;
164    let (input, _) = multispace0(input)?;
165
166    // Find end: next "query" or "execute" keyword at line start (after whitespace), or EOF
167    let mut end = input.len();
168
169    for (i, _) in input.char_indices() {
170        if i == 0 || input.as_bytes().get(i.saturating_sub(1)) == Some(&b'\n') {
171            // At start of line, skip whitespace and check for keyword
172            let line_rest = &input[i..];
173            let trimmed = line_rest.trim_start();
174            if trimmed.starts_with("query ") || trimmed.starts_with("execute ") {
175                // Find where the trimmed content starts
176                let ws_len = line_rest.len() - trimmed.len();
177                end = i + ws_len;
178                break;
179            }
180        }
181    }
182
183    let body = input[..end].trim();
184    Ok((&input[end..], body))
185}
186
187/// Parse a single query definition
188fn parse_query_def(input: &str) -> IResult<&str, QueryDef> {
189    let (input, _) = ws_and_comments(input)?;
190
191    let (input, is_execute) = alt((
192        map(tag_no_case("query"), |_| false),
193        map(tag_no_case("execute"), |_| true),
194    ))
195    .parse(input)?;
196
197    let (input, _) = multispace1(input)?;
198    let (input, name) = identifier(input)?;
199    let (input, params) = parse_params(input)?;
200
201    // Return type (optional for execute)
202    let (input, return_type) = if is_execute {
203        (input, None)
204    } else {
205        let (input, rt) = parse_return_type(input)?;
206        (input, Some(rt))
207    };
208
209    let (input, body) = parse_body(input)?;
210
211    Ok((
212        input,
213        QueryDef {
214            name: name.to_string(),
215            params,
216            return_type,
217            body: body.to_string(),
218            is_execute,
219        },
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}