hledger_parser/directive/auto_postings/
query.rs

1use chumsky::prelude::*;
2
3use crate::{component::whitespace::whitespace, state::State};
4
5#[derive(Clone, Debug, PartialEq)]
6pub struct Query {
7    pub terms: Vec<Term>,
8}
9
10#[derive(Clone, Debug, PartialEq)]
11pub struct Term {
12    pub r#type: Option<String>,
13    pub is_not: bool,
14    pub value: String,
15}
16
17pub fn query<'a>() -> impl Parser<'a, &'a str, Query, extra::Full<Rich<'a, char>, State, ()>> {
18    term()
19        .separated_by(whitespace().repeated().at_least(1))
20        .at_least(1)
21        .collect::<Vec<_>>()
22        .map(|terms| Query { terms })
23}
24
25fn term<'a>() -> impl Parser<'a, &'a str, Term, extra::Full<Rich<'a, char>, State, ()>> {
26    let value = any()
27        .and_is(text::newline().not())
28        .and_is(whitespace().not())
29        .repeated()
30        .at_least(1)
31        .collect::<String>();
32    let quoted_value = any()
33        .and_is(text::newline().not())
34        .and_is(just("'").not()) // indicated end of quote
35        .repeated()
36        .at_least(1)
37        .collect::<String>()
38        .delimited_by(just("'"), just("'"));
39    let r#type = just("date")
40        .or(just("status"))
41        .or(just("desc"))
42        .or(just("cur"))
43        .or(just("amt"))
44        .then_ignore(just(":"))
45        .map(ToString::to_string);
46
47    just("not:")
48        .or_not()
49        .then(r#type.or_not())
50        .then(quoted_value.or(value))
51        .map(|((is_not, r#type), value)| Term {
52            r#type,
53            is_not: is_not.is_some(),
54            value,
55        })
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61
62    #[test]
63    fn single_quoted_term() {
64        let result = query()
65            .then_ignore(end())
66            .parse("'personal care'")
67            .into_result();
68        assert_eq!(
69            result,
70            Ok(Query {
71                terms: vec![Term {
72                    is_not: false,
73                    value: String::from("personal care"),
74                    r#type: None,
75                }]
76            })
77        );
78    }
79
80    #[test]
81    fn single_account_term() {
82        let result = query()
83            .then_ignore(end())
84            .parse("expenses:dining")
85            .into_result();
86        assert_eq!(
87            result,
88            Ok(Query {
89                terms: vec![Term {
90                    is_not: false,
91                    value: String::from("expenses:dining"),
92                    r#type: None,
93                }]
94            })
95        );
96    }
97
98    #[test]
99    fn single_simple_term() {
100        let result = query().then_ignore(end()).parse("dining").into_result();
101        assert_eq!(
102            result,
103            Ok(Query {
104                terms: vec![Term {
105                    is_not: false,
106                    value: String::from("dining"),
107                    r#type: None,
108                }]
109            })
110        );
111    }
112
113    #[test]
114    fn multiple_simple_terms() {
115        let result = query()
116            .then_ignore(end())
117            .parse("dining groceries")
118            .into_result();
119        assert_eq!(
120            result,
121            Ok(Query {
122                terms: vec![
123                    Term {
124                        is_not: false,
125                        value: String::from("dining"),
126                        r#type: None,
127                    },
128                    Term {
129                        is_not: false,
130                        value: String::from("groceries"),
131                        r#type: None,
132                    }
133                ]
134            })
135        );
136    }
137
138    #[test]
139    fn not_term() {
140        let result = query()
141            .then_ignore(end())
142            .parse("not:'opening closing'")
143            .into_result();
144        assert_eq!(
145            result,
146            Ok(Query {
147                terms: vec![Term {
148                    is_not: true,
149                    value: String::from("opening closing"),
150                    r#type: None,
151                }]
152            })
153        );
154    }
155
156    #[test]
157    fn typed_term() {
158        let result = query()
159            .then_ignore(end())
160            .parse("desc:'opening|closing'")
161            .into_result();
162        assert_eq!(
163            result,
164            Ok(Query {
165                terms: vec![Term {
166                    is_not: false,
167                    value: String::from("opening|closing"),
168                    r#type: Some(String::from("desc")),
169                }]
170            })
171        );
172    }
173
174    #[test]
175    fn not_typed_term() {
176        let result = query()
177            .then_ignore(end())
178            .parse("not:desc:'opening|closing'")
179            .into_result();
180        assert_eq!(
181            result,
182            Ok(Query {
183                terms: vec![Term {
184                    is_not: true,
185                    value: String::from("opening|closing"),
186                    r#type: Some(String::from("desc")),
187                }]
188            })
189        );
190    }
191
192    #[test]
193    fn complex() {
194        let result = query()
195            .then_ignore(end())
196            .parse("account 'testing account' cur:\\$ not:desc:'opening|closing'")
197            .into_result();
198        assert_eq!(
199            result,
200            Ok(Query {
201                terms: vec![
202                    Term {
203                        is_not: false,
204                        value: String::from("account"),
205                        r#type: None,
206                    },
207                    Term {
208                        is_not: false,
209                        value: String::from("testing account"),
210                        r#type: None,
211                    },
212                    Term {
213                        is_not: false,
214                        value: String::from("\\$"),
215                        r#type: Some(String::from("cur")),
216                    },
217                    Term {
218                        is_not: true,
219                        value: String::from("opening|closing"),
220                        r#type: Some(String::from("desc")),
221                    }
222                ]
223            })
224        );
225    }
226}