Skip to main content

sql_composer/parser/
bind.rs

1//! Parser for `:bind(name [EXPECTING min[..max]] [NULL])` macros.
2
3use winnow::combinator::{opt, preceded, trace};
4use winnow::error::ParserError;
5use winnow::stream::{AsBStr, AsChar, Compare, Stream, StreamIsPartial};
6use winnow::token::{literal, take_while};
7use winnow::Parser;
8
9use crate::types::Binding;
10
11/// Parse a bind parameter name: one or more alphanumeric or underscore characters.
12pub fn bind_name<'i, Input, Error>(input: &mut Input) -> Result<String, Error>
13where
14    Input: StreamIsPartial + Stream + Compare<&'i str>,
15    <Input as Stream>::Slice: AsBStr,
16    <Input as Stream>::Token: AsChar + Clone,
17    Error: ParserError<Input>,
18{
19    trace("bind_name", move |input: &mut Input| {
20        let name = take_while(1.., |c: <Input as Stream>::Token| {
21            let ch = c.as_char();
22            ch.is_alphanumeric() || ch == '_'
23        })
24        .parse_next(input)?;
25        let name = String::from_utf8_lossy(name.as_bstr()).to_string();
26        Ok(name)
27    })
28    .parse_next(input)
29}
30
31/// Parse optional whitespace (spaces and tabs only, not newlines within macro parens).
32fn ws<'i, Input, Error>(input: &mut Input) -> Result<(), Error>
33where
34    Input: StreamIsPartial + Stream + Compare<&'i str>,
35    <Input as Stream>::Slice: AsBStr,
36    <Input as Stream>::Token: AsChar + Clone,
37    Error: ParserError<Input>,
38{
39    take_while(0.., |c: <Input as Stream>::Token| {
40        let ch = c.as_char();
41        ch == ' ' || ch == '\t'
42    })
43    .void()
44    .parse_next(input)
45}
46
47/// Parse a u32 value from decimal digits.
48fn parse_u32<'i, Input, Error>(input: &mut Input) -> Result<u32, Error>
49where
50    Input: StreamIsPartial + Stream + Compare<&'i str>,
51    <Input as Stream>::Slice: AsBStr,
52    <Input as Stream>::Token: AsChar + Clone,
53    Error: ParserError<Input>,
54{
55    let digits = take_while(1.., |c: <Input as Stream>::Token| {
56        c.as_char().is_ascii_digit()
57    })
58    .parse_next(input)?;
59    let s = String::from_utf8_lossy(digits.as_bstr());
60    let n = s
61        .parse::<u32>()
62        .map_err(|_| ParserError::from_input(input))?;
63    Ok(n)
64}
65
66/// Parse `EXPECTING min[..max]` clause.
67fn expecting<'i, Input, Error>(input: &mut Input) -> Result<(u32, Option<u32>), Error>
68where
69    Input: StreamIsPartial + Stream + Compare<&'i str>,
70    <Input as Stream>::Slice: AsBStr,
71    <Input as Stream>::Token: AsChar + Clone,
72    Error: ParserError<Input>,
73{
74    trace("expecting", move |input: &mut Input| {
75        literal("EXPECTING").parse_next(input)?;
76        ws(input)?;
77        let min = parse_u32(input)?;
78        let max = opt(preceded(literal(".."), parse_u32)).parse_next(input)?;
79        Ok((min, max))
80    })
81    .parse_next(input)
82}
83
84/// Parse a complete `:bind(name [EXPECTING min[..max]] [NULL])` macro.
85///
86/// Assumes the `:bind(` prefix has already been consumed. Parses the contents
87/// up to and including the closing `)`.
88pub fn bind<'i, Input, Error>(input: &mut Input) -> Result<Binding, Error>
89where
90    Input: StreamIsPartial + Stream + Compare<&'i str>,
91    <Input as Stream>::Slice: AsBStr,
92    <Input as Stream>::Token: AsChar + Clone,
93    Error: ParserError<Input>,
94{
95    trace("bind", move |input: &mut Input| {
96        let name = bind_name(input)?;
97        ws(input)?;
98
99        let expecting_result = opt(expecting).parse_next(input)?;
100        ws(input)?;
101
102        let null_kw = opt(literal("NULL")).parse_next(input)?;
103        ws(input)?;
104
105        literal(")").parse_next(input)?;
106
107        let (min_values, max_values) = match expecting_result {
108            Some((min, max)) => (Some(min), max),
109            None => (None, None),
110        };
111
112        Ok(Binding {
113            name,
114            min_values,
115            max_values,
116            nullable: null_kw.is_some(),
117        })
118    })
119    .parse_next(input)
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125    use winnow::error::ContextError;
126
127    type TestInput<'a> = &'a str;
128
129    #[test]
130    fn test_bind_simple() {
131        let mut input: TestInput = "user_id)";
132        let result = bind::<_, ContextError>.parse_next(&mut input).unwrap();
133        assert_eq!(result.name, "user_id");
134        assert_eq!(result.min_values, None);
135        assert_eq!(result.max_values, None);
136        assert!(!result.nullable);
137        assert_eq!(input, "");
138    }
139
140    #[test]
141    fn test_bind_with_expecting() {
142        let mut input: TestInput = "values EXPECTING 1..10)";
143        let result = bind::<_, ContextError>.parse_next(&mut input).unwrap();
144        assert_eq!(result.name, "values");
145        assert_eq!(result.min_values, Some(1));
146        assert_eq!(result.max_values, Some(10));
147        assert!(!result.nullable);
148    }
149
150    #[test]
151    fn test_bind_with_expecting_min_only() {
152        let mut input: TestInput = "values EXPECTING 3)";
153        let result = bind::<_, ContextError>.parse_next(&mut input).unwrap();
154        assert_eq!(result.name, "values");
155        assert_eq!(result.min_values, Some(3));
156        assert_eq!(result.max_values, None);
157        assert!(!result.nullable);
158    }
159
160    #[test]
161    fn test_bind_nullable() {
162        let mut input: TestInput = "email NULL)";
163        let result = bind::<_, ContextError>.parse_next(&mut input).unwrap();
164        assert_eq!(result.name, "email");
165        assert!(result.nullable);
166    }
167
168    #[test]
169    fn test_bind_full() {
170        let mut input: TestInput = "tags EXPECTING 1..5 NULL)";
171        let result = bind::<_, ContextError>.parse_next(&mut input).unwrap();
172        assert_eq!(result.name, "tags");
173        assert_eq!(result.min_values, Some(1));
174        assert_eq!(result.max_values, Some(5));
175        assert!(result.nullable);
176    }
177
178    #[test]
179    fn test_bind_name_only() {
180        let mut input: TestInput = "active";
181        let result = bind_name::<_, ContextError>.parse_next(&mut input).unwrap();
182        assert_eq!(result, "active");
183    }
184}