hledger_parser/directive/auto_postings/
query.rs1use 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()) .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}