Skip to main content

sql_composer/parser/
mod.rs

1//! winnow 0.7 parsers for sql-composer template macros.
2//!
3//! The parser treats SQL text as opaque literals and only recognizes the
4//! template macro syntax: `:bind(...)`, `:compose(...)`, `:count(...)`,
5//! and `:union(...)`.
6
7pub mod bind;
8pub mod command;
9pub mod compose;
10pub mod template;
11
12use winnow::error::ContextError;
13use winnow::Parser;
14
15use crate::error;
16use crate::types::{Element, Template, TemplateSource};
17
18/// Parse a template string into a [`Template`].
19///
20/// This is the main entry point for parsing template content from a string.
21pub fn parse_template(input: &str, source: TemplateSource) -> error::Result<Template> {
22    let mut remaining = input;
23    let elements: Vec<Element> = template::template::<_, ContextError>
24        .parse_next(&mut remaining)
25        .map_err(|e| error::Error::Parse {
26            location: format!("offset {}", input.len() - remaining.len()),
27            message: e.to_string(),
28        })?;
29
30    Ok(Template { elements, source })
31}
32
33/// Parse a template from a file path.
34///
35/// Reads the file content and parses it as a template.
36pub fn parse_template_file(path: &std::path::Path) -> error::Result<Template> {
37    let content = std::fs::read_to_string(path)?;
38    parse_template(&content, TemplateSource::File(path.to_path_buf()))
39}
40
41#[cfg(test)]
42mod tests {
43    use super::*;
44    use crate::types::{Binding, Element};
45
46    #[test]
47    fn test_parse_template_literal() {
48        let tpl = parse_template(
49            "SELECT * FROM users WHERE id = :bind(user_id);",
50            TemplateSource::Literal("test".into()),
51        )
52        .unwrap();
53
54        assert_eq!(tpl.elements.len(), 3);
55        assert_eq!(
56            tpl.elements[0],
57            Element::Sql("SELECT * FROM users WHERE id = ".into())
58        );
59        assert_eq!(
60            tpl.elements[1],
61            Element::Bind(Binding {
62                name: "user_id".into(),
63                min_values: None,
64                max_values: None,
65                nullable: false,
66            })
67        );
68        assert_eq!(tpl.elements[2], Element::Sql(";".into()));
69    }
70
71    #[test]
72    fn test_parse_template_multiline() {
73        let input = "SELECT id, name, email\nFROM users\nWHERE id = :bind(user_id)\n  AND active = :bind(active);";
74        let tpl = parse_template(input, TemplateSource::Literal("test".into())).unwrap();
75
76        // Count bindings
77        let bind_count = tpl
78            .elements
79            .iter()
80            .filter(|e| matches!(e, Element::Bind(_)))
81            .count();
82        assert_eq!(bind_count, 2);
83    }
84}