Skip to main content

sql_composer/parser/
compose.rs

1//! Parser for `:compose(path)` macros.
2
3use std::path::PathBuf;
4
5use winnow::combinator::trace;
6use winnow::error::ParserError;
7use winnow::stream::{AsBStr, AsChar, Compare, Stream, StreamIsPartial};
8use winnow::token::{literal, take_while};
9use winnow::Parser;
10
11use crate::types::ComposeRef;
12
13/// Parse a file path inside a compose macro: one or more non-`)` characters.
14pub fn compose_path<'i, Input, Error>(input: &mut Input) -> Result<PathBuf, Error>
15where
16    Input: StreamIsPartial + Stream + Compare<&'i str>,
17    <Input as Stream>::Slice: AsBStr,
18    <Input as Stream>::Token: AsChar + Clone,
19    Error: ParserError<Input>,
20{
21    trace("compose_path", move |input: &mut Input| {
22        let path_str = take_while(1.., |c: <Input as Stream>::Token| {
23            let ch = c.as_char();
24            ch != ')' && ch != ' ' && ch != '\t' && ch != '\n' && ch != '\r'
25        })
26        .parse_next(input)?;
27        let path_str = String::from_utf8_lossy(path_str.as_bstr()).to_string();
28        Ok(PathBuf::from(path_str))
29    })
30    .parse_next(input)
31}
32
33/// Parse a complete `:compose(path)` macro.
34///
35/// Assumes the `:compose(` prefix has already been consumed. Parses the path
36/// and the closing `)`.
37pub fn compose<'i, Input, Error>(input: &mut Input) -> Result<ComposeRef, Error>
38where
39    Input: StreamIsPartial + Stream + Compare<&'i str>,
40    <Input as Stream>::Slice: AsBStr,
41    <Input as Stream>::Token: AsChar + Clone,
42    Error: ParserError<Input>,
43{
44    trace("compose", move |input: &mut Input| {
45        let path = compose_path(input)?;
46        literal(")").parse_next(input)?;
47        Ok(ComposeRef { path })
48    })
49    .parse_next(input)
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55    use winnow::error::ContextError;
56
57    type TestInput<'a> = &'a str;
58
59    #[test]
60    fn test_compose_simple() {
61        let mut input: TestInput = "templates/get_user.tql)";
62        let result = compose::<_, ContextError>.parse_next(&mut input).unwrap();
63        assert_eq!(result.path, PathBuf::from("templates/get_user.tql"));
64        assert_eq!(input, "");
65    }
66
67    #[test]
68    fn test_compose_relative_path() {
69        let mut input: TestInput = "src/tests/simple-template.tql)";
70        let result = compose::<_, ContextError>.parse_next(&mut input).unwrap();
71        assert_eq!(result.path, PathBuf::from("src/tests/simple-template.tql"));
72    }
73
74    #[test]
75    fn test_compose_with_trailing() {
76        let mut input: TestInput = "get_user.tql) AND active";
77        let result = compose::<_, ContextError>.parse_next(&mut input).unwrap();
78        assert_eq!(result.path, PathBuf::from("get_user.tql"));
79        assert_eq!(input, " AND active");
80    }
81}