1use std::fmt;
2
3use thiserror::Error;
4
5use crate::ast as odata_ast;
6
7pub use crate::ast::Value as ODataValue;
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum FieldKind {
11 String,
12 I64,
13 F64,
14 Bool,
15 Uuid,
16 DateTimeUtc,
17 Date,
18 Time,
19 Decimal,
20}
21
22impl fmt::Display for FieldKind {
23 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24 match self {
25 FieldKind::String => write!(f, "String"),
26 FieldKind::I64 => write!(f, "I64"),
27 FieldKind::F64 => write!(f, "F64"),
28 FieldKind::Bool => write!(f, "Bool"),
29 FieldKind::Uuid => write!(f, "Uuid"),
30 FieldKind::DateTimeUtc => write!(f, "DateTimeUtc"),
31 FieldKind::Date => write!(f, "Date"),
32 FieldKind::Time => write!(f, "Time"),
33 FieldKind::Decimal => write!(f, "Decimal"),
34 }
35 }
36}
37
38pub trait FilterField: Copy + Eq + std::hash::Hash + fmt::Debug + 'static {
39 const FIELDS: &'static [Self];
40
41 fn name(&self) -> &'static str;
42
43 fn kind(&self) -> FieldKind;
44
45 fn from_name(name: &str) -> Option<Self> {
46 let exact = Self::FIELDS
49 .iter()
50 .copied()
51 .find(|f| f.name().eq_ignore_ascii_case(name));
52 if exact.is_some() {
53 return exact;
54 }
55 if let Some(last) = name.rsplit('/').next()
62 && last != name
63 {
64 let mut iter = Self::FIELDS
65 .iter()
66 .copied()
67 .filter(|f| f.name().eq_ignore_ascii_case(last));
68 if let Some(first) = iter.next() {
69 if iter.next().is_some() {
73 return None;
74 }
75 return Some(first);
76 }
77 }
78 None
79 }
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub enum FilterOp {
84 Eq,
85 Ne,
86 Gt,
87 Ge,
88 Lt,
89 Le,
90 In,
91 Contains,
92 StartsWith,
93 EndsWith,
94 And,
95 Or,
96}
97
98impl FieldKind {
99 #[must_use]
107 pub const fn allows(self, op: FilterOp) -> bool {
108 match self {
109 Self::String => matches!(
110 op,
111 FilterOp::Eq
112 | FilterOp::Ne
113 | FilterOp::In
114 | FilterOp::Contains
115 | FilterOp::StartsWith
116 | FilterOp::EndsWith
117 ),
118 Self::Uuid => matches!(op, FilterOp::Eq | FilterOp::Ne | FilterOp::In),
119 Self::Bool => matches!(op, FilterOp::Eq | FilterOp::Ne),
120 Self::I64 | Self::F64 | Self::Decimal | Self::DateTimeUtc | Self::Date | Self::Time => {
121 matches!(
122 op,
123 FilterOp::Eq
124 | FilterOp::Ne
125 | FilterOp::Gt
126 | FilterOp::Ge
127 | FilterOp::Lt
128 | FilterOp::Le
129 | FilterOp::In
130 )
131 }
132 }
133 }
134}
135
136impl fmt::Display for FilterOp {
137 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138 match self {
139 FilterOp::Eq => write!(f, "eq"),
140 FilterOp::Ne => write!(f, "ne"),
141 FilterOp::Gt => write!(f, "gt"),
142 FilterOp::Ge => write!(f, "ge"),
143 FilterOp::Lt => write!(f, "lt"),
144 FilterOp::Le => write!(f, "le"),
145 FilterOp::In => write!(f, "in"),
146 FilterOp::Contains => write!(f, "contains"),
147 FilterOp::StartsWith => write!(f, "startswith"),
148 FilterOp::EndsWith => write!(f, "endswith"),
149 FilterOp::And => write!(f, "and"),
150 FilterOp::Or => write!(f, "or"),
151 }
152 }
153}
154
155#[derive(Debug, Clone)]
156pub enum FilterNode<F: FilterField> {
157 Binary {
158 field: F,
159 op: FilterOp,
160 value: ODataValue,
161 },
162 InList {
163 field: F,
164 values: Vec<ODataValue>,
165 },
166 Composite {
167 op: FilterOp,
168 children: Vec<FilterNode<F>>,
169 },
170 Not(Box<FilterNode<F>>),
171}
172
173impl<F: FilterField> FilterNode<F> {
174 pub fn binary(field: F, op: FilterOp, value: ODataValue) -> Self {
175 FilterNode::Binary { field, op, value }
176 }
177
178 #[must_use]
179 pub fn and(children: Vec<FilterNode<F>>) -> Self {
180 FilterNode::Composite {
181 op: FilterOp::And,
182 children,
183 }
184 }
185
186 #[must_use]
187 pub fn or(children: Vec<FilterNode<F>>) -> Self {
188 FilterNode::Composite {
189 op: FilterOp::Or,
190 children,
191 }
192 }
193
194 #[allow(clippy::should_implement_trait)]
195 pub fn not(inner: FilterNode<F>) -> Self {
196 FilterNode::Not(Box::new(inner))
197 }
198}
199
200#[derive(Debug, Error, Clone)]
201pub enum FilterError {
202 #[error("Unknown field: {0}")]
203 UnknownField(String),
204
205 #[error("Type mismatch for field {field}: expected {expected}, got {got}")]
206 TypeMismatch {
207 field: String,
208 expected: FieldKind,
209 got: String,
210 },
211
212 #[error("Unsupported operation: {0}")]
213 UnsupportedOperation(String),
214
215 #[error("Invalid filter expression: {0}")]
216 InvalidExpression(String),
217
218 #[error("Field-to-field comparisons are not supported")]
219 FieldToFieldComparison,
220
221 #[error("Bare identifier in filter: {0}")]
222 BareIdentifier(String),
223
224 #[error("Bare literal in filter")]
225 BareLiteral,
226}
227
228pub type FilterResult<T> = Result<T, FilterError>;
229
230pub fn parse_odata_filter<F: FilterField>(raw: &str) -> FilterResult<FilterNode<F>> {
237 use crate::odata_filters::parse_str;
238
239 let ast = parse_str(raw).map_err(|e| FilterError::InvalidExpression(format!("{e:?}")))?;
240 let ast: odata_ast::Expr = ast.into();
241 convert_expr_to_filter_node::<F>(&ast)
242}
243
244pub fn convert_expr_to_filter_node<F: FilterField>(
251 expr: &odata_ast::Expr,
252) -> FilterResult<FilterNode<F>> {
253 use odata_ast::Expr as E;
254
255 match expr {
256 E::And(left, right) => {
257 let left_node = convert_expr_to_filter_node::<F>(left)?;
258 let right_node = convert_expr_to_filter_node::<F>(right)?;
259 Ok(FilterNode::and(vec![left_node, right_node]))
260 }
261 E::Or(left, right) => {
262 let left_node = convert_expr_to_filter_node::<F>(left)?;
263 let right_node = convert_expr_to_filter_node::<F>(right)?;
264 Ok(FilterNode::or(vec![left_node, right_node]))
265 }
266 E::Not(inner) => {
267 let inner_node = convert_expr_to_filter_node::<F>(inner)?;
268 Ok(FilterNode::not(inner_node))
269 }
270
271 E::Compare(left, op, right) => {
272 let (field_name, value) = match (&**left, &**right) {
273 (E::Identifier(name), E::Value(val)) => (name.as_str(), val.clone()),
274 (E::Identifier(_), E::Identifier(_)) => {
275 return Err(FilterError::FieldToFieldComparison);
276 }
277 _ => {
278 return Err(FilterError::InvalidExpression(
279 "Comparison must be between field and value".to_owned(),
280 ));
281 }
282 };
283
284 let field = F::from_name(field_name)
285 .ok_or_else(|| FilterError::UnknownField(field_name.to_owned()))?;
286
287 validate_value_type(field, &value)?;
288
289 let filter_op = match op {
290 odata_ast::CompareOperator::Eq => FilterOp::Eq,
291 odata_ast::CompareOperator::Ne => FilterOp::Ne,
292 odata_ast::CompareOperator::Gt => FilterOp::Gt,
293 odata_ast::CompareOperator::Ge => FilterOp::Ge,
294 odata_ast::CompareOperator::Lt => FilterOp::Lt,
295 odata_ast::CompareOperator::Le => FilterOp::Le,
296 };
297 reject_unsupported_op(field, field_name, filter_op)?;
298
299 Ok(FilterNode::binary(field, filter_op, value))
300 }
301
302 E::Function(func_name, args) => {
303 let name_lower = func_name.to_ascii_lowercase();
304 match (name_lower.as_str(), args.as_slice()) {
305 (
306 "contains",
307 [
308 E::Identifier(field_name),
309 E::Value(odata_ast::Value::String(s)),
310 ],
311 ) => {
312 let field = F::from_name(field_name)
313 .ok_or_else(|| FilterError::UnknownField(field_name.clone()))?;
314
315 if field.kind() != FieldKind::String {
316 return Err(FilterError::TypeMismatch {
317 field: field_name.clone(),
318 expected: FieldKind::String,
319 got: "non-string".to_owned(),
320 });
321 }
322
323 Ok(FilterNode::binary(
324 field,
325 FilterOp::Contains,
326 odata_ast::Value::String(s.clone()),
327 ))
328 }
329 (
330 "startswith",
331 [
332 E::Identifier(field_name),
333 E::Value(odata_ast::Value::String(s)),
334 ],
335 ) => {
336 let field = F::from_name(field_name)
337 .ok_or_else(|| FilterError::UnknownField(field_name.clone()))?;
338
339 if field.kind() != FieldKind::String {
340 return Err(FilterError::TypeMismatch {
341 field: field_name.clone(),
342 expected: FieldKind::String,
343 got: "non-string".to_owned(),
344 });
345 }
346
347 Ok(FilterNode::binary(
348 field,
349 FilterOp::StartsWith,
350 odata_ast::Value::String(s.clone()),
351 ))
352 }
353 (
354 "endswith",
355 [
356 E::Identifier(field_name),
357 E::Value(odata_ast::Value::String(s)),
358 ],
359 ) => {
360 let field = F::from_name(field_name)
361 .ok_or_else(|| FilterError::UnknownField(field_name.clone()))?;
362
363 if field.kind() != FieldKind::String {
364 return Err(FilterError::TypeMismatch {
365 field: field_name.clone(),
366 expected: FieldKind::String,
367 got: "non-string".to_owned(),
368 });
369 }
370
371 Ok(FilterNode::binary(
372 field,
373 FilterOp::EndsWith,
374 odata_ast::Value::String(s.clone()),
375 ))
376 }
377 _ => Err(FilterError::UnsupportedOperation(format!(
378 "Function '{func_name}'"
379 ))),
380 }
381 }
382
383 E::In(left, list) => {
384 let field_name = match &**left {
385 E::Identifier(name) => name.as_str(),
386 _ => {
387 return Err(FilterError::InvalidExpression(
388 "IN operator requires a field identifier on the left side".to_owned(),
389 ));
390 }
391 };
392
393 let field = F::from_name(field_name)
394 .ok_or_else(|| FilterError::UnknownField(field_name.to_owned()))?;
395
396 let mut values = Vec::with_capacity(list.len());
397 for item in list {
398 match item {
399 E::Value(val) => {
400 validate_value_type(field, val)?;
401 values.push(val.clone());
402 }
403 _ => {
404 return Err(FilterError::InvalidExpression(
405 "IN operator values must be literals".to_owned(),
406 ));
407 }
408 }
409 }
410
411 if values.is_empty() {
412 return Err(FilterError::InvalidExpression(
413 "IN operator requires at least one value".to_owned(),
414 ));
415 }
416
417 reject_unsupported_op(field, field_name, FilterOp::In)?;
418
419 Ok(FilterNode::InList { field, values })
420 }
421
422 E::Identifier(name) => Err(FilterError::BareIdentifier(name.clone())),
423 E::Value(_) => Err(FilterError::BareLiteral),
424 }
425}
426
427fn reject_unsupported_op<F: FilterField>(field: F, name: &str, op: FilterOp) -> FilterResult<()> {
430 if field.kind().allows(op) {
431 Ok(())
432 } else {
433 Err(FilterError::UnsupportedOperation(format!(
434 "`{op}` on field `{name}` of type {}",
435 field.kind()
436 )))
437 }
438}
439
440fn validate_value_type<F: FilterField>(field: F, value: &odata_ast::Value) -> FilterResult<()> {
441 use odata_ast::Value as V;
442
443 let kind = field.kind();
444 let matches = matches!(
445 (kind, value),
446 (FieldKind::String, V::String(_))
447 | (
448 FieldKind::I64 | FieldKind::F64 | FieldKind::Decimal,
449 V::Number(_)
450 )
451 | (FieldKind::Bool, V::Bool(_))
452 | (FieldKind::Uuid, V::Uuid(_))
453 | (FieldKind::DateTimeUtc, V::DateTime(_))
454 | (FieldKind::Date, V::Date(_))
455 | (FieldKind::Time, V::Time(_))
456 );
457
458 if matches {
459 Ok(())
460 } else {
461 Err(FilterError::TypeMismatch {
462 field: field.name().to_owned(),
463 expected: kind,
464 got: value.to_string(),
465 })
466 }
467}