1#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
2pub mod builder;
3pub mod errors;
4pub mod filter;
5pub mod limits;
6pub(crate) mod odata_filters;
8mod odata_parse;
10pub mod page;
11pub mod pagination;
12pub mod problem_mapping;
13pub mod schema;
14
15pub use builder::QueryBuilder;
16pub use limits::ODataLimits;
17pub use page::{Page, PageInfo};
18pub use pagination::{normalize_filter_for_hash, short_filter_hash};
19pub use schema::{FieldRef, Schema};
20
21pub mod ast {
22 use bigdecimal::BigDecimal;
23 use chrono::{DateTime, NaiveDate, NaiveTime, Utc};
24 use uuid::Uuid;
25
26 #[derive(Clone, Debug)]
27 pub enum Expr {
28 And(Box<Expr>, Box<Expr>),
29 Or(Box<Expr>, Box<Expr>),
30 Not(Box<Expr>),
31 Compare(Box<Expr>, CompareOperator, Box<Expr>),
32 In(Box<Expr>, Vec<Expr>),
33 Function(String, Vec<Expr>),
34 Identifier(String),
35 Value(Value),
36 }
37
38 impl Expr {
39 #[must_use]
47 pub fn and(self, other: Expr) -> Expr {
48 Expr::And(Box::new(self), Box::new(other))
49 }
50
51 #[must_use]
53 pub fn or(self, other: Expr) -> Expr {
54 Expr::Or(Box::new(self), Box::new(other))
55 }
56
57 #[must_use]
59 #[allow(clippy::should_implement_trait)]
60 pub fn not(self) -> Expr {
61 !self
62 }
63 }
64
65 impl std::ops::Not for Expr {
66 type Output = Expr;
67
68 fn not(self) -> Self::Output {
69 Expr::Not(Box::new(self))
70 }
71 }
72
73 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
74 pub enum CompareOperator {
75 Eq,
76 Ne,
77 Gt,
78 Ge,
79 Lt,
80 Le,
81 }
82
83 #[derive(Clone, Debug)]
84 pub enum Value {
85 Null,
86 Bool(bool),
87 Number(BigDecimal),
88 Uuid(Uuid),
89 DateTime(DateTime<Utc>),
90 Date(NaiveDate),
91 Time(NaiveTime),
92 String(String),
93 }
94
95 impl std::fmt::Display for Value {
96 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97 match self {
98 Value::Null => write!(f, "null"),
99 Value::Bool(_) => write!(f, "bool"),
100 Value::Number(_) => write!(f, "number"),
101 Value::Uuid(_) => write!(f, "uuid"),
102 Value::DateTime(_) => write!(f, "datetime"),
103 Value::Date(_) => write!(f, "date"),
104 Value::Time(_) => write!(f, "time"),
105 Value::String(_) => write!(f, "string"),
106 }
107 }
108 }
109}
110
111#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
113pub enum SortDir {
114 #[serde(rename = "asc")]
115 Asc,
116 #[serde(rename = "desc")]
117 Desc,
118}
119
120impl SortDir {
121 #[must_use]
123 pub fn reverse(self) -> Self {
124 match self {
125 SortDir::Asc => SortDir::Desc,
126 SortDir::Desc => SortDir::Asc,
127 }
128 }
129}
130
131#[derive(Clone, Debug)]
132pub struct OrderKey {
133 pub field: String,
134 pub dir: SortDir,
135}
136
137#[derive(Clone, Debug, Default)]
138#[must_use]
139pub struct ODataOrderBy(pub Vec<OrderKey>);
140
141impl ODataOrderBy {
142 pub fn empty() -> Self {
143 Self(vec![])
144 }
145
146 #[must_use]
147 pub fn is_empty(&self) -> bool {
148 self.0.is_empty()
149 }
150
151 #[must_use]
153 pub fn to_signed_tokens(&self) -> String {
154 self.0
155 .iter()
156 .map(|k| {
157 if matches!(k.dir, SortDir::Asc) {
158 format!("+{}", k.field)
159 } else {
160 format!("-{}", k.field)
161 }
162 })
163 .collect::<Vec<_>>()
164 .join(",")
165 }
166
167 pub fn from_signed_tokens(signed: &str) -> Result<Self, Error> {
173 let mut out = Vec::new();
174 for seg in signed.split(',') {
175 let seg = seg.trim();
176 if seg.is_empty() {
177 continue;
178 }
179 let (dir, name) = match seg.as_bytes()[0] {
180 b'+' => (SortDir::Asc, &seg[1..]),
181 b'-' => (SortDir::Desc, &seg[1..]),
182 _ => (SortDir::Asc, seg), };
184 if name.is_empty() {
185 return Err(Error::InvalidOrderByField(seg.to_owned()));
186 }
187 out.push(OrderKey {
188 field: name.to_owned(),
189 dir,
190 });
191 }
192 if out.is_empty() {
193 return Err(Error::InvalidOrderByField("empty order".into()));
194 }
195 Ok(ODataOrderBy(out))
196 }
197
198 #[must_use]
200 pub fn equals_signed_tokens(&self, signed: &str) -> bool {
201 let parse = |t: &str| -> Option<(String, SortDir)> {
202 let t = t.trim();
203 if t.is_empty() {
204 return None;
205 }
206 let (dir, name) = match t.as_bytes()[0] {
207 b'+' => (SortDir::Asc, &t[1..]),
208 b'-' => (SortDir::Desc, &t[1..]),
209 _ => (SortDir::Asc, t),
210 };
211 if name.is_empty() {
212 return None;
213 }
214 Some((name.to_owned(), dir))
215 };
216 let theirs: Vec<_> = signed.split(',').filter_map(parse).collect();
217 if theirs.len() != self.0.len() {
218 return false;
219 }
220 self.0
221 .iter()
222 .zip(theirs.iter())
223 .all(|(a, (n, d))| a.field == *n && a.dir == *d)
224 }
225
226 pub fn ensure_tiebreaker(mut self, tiebreaker: &str, dir: SortDir) -> Self {
228 if !self.0.iter().any(|k| k.field == tiebreaker) {
229 self.0.push(OrderKey {
230 field: tiebreaker.to_owned(),
231 dir,
232 });
233 }
234 self
235 }
236
237 pub fn reverse_directions(mut self) -> Self {
239 for key in &mut self.0 {
240 key.dir = key.dir.reverse();
241 }
242 self
243 }
244}
245
246impl std::fmt::Display for ODataOrderBy {
248 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
249 if self.0.is_empty() {
250 return write!(f, "(none)");
251 }
252
253 let formatted: Vec<String> = self
254 .0
255 .iter()
256 .map(|key| {
257 let dir_str = match key.dir {
258 SortDir::Asc => "asc",
259 SortDir::Desc => "desc",
260 };
261 format!("{} {}", key.field, dir_str)
262 })
263 .collect();
264
265 write!(f, "{}", formatted.join(", "))
266 }
267}
268
269#[derive(thiserror::Error, Debug, Clone)]
285pub enum Error {
286 #[error("invalid $filter: {0}")]
288 InvalidFilter(String),
289
290 #[error("unsupported $orderby field: {0}")]
292 InvalidOrderByField(String),
293
294 #[error("ORDER_MISMATCH")]
296 OrderMismatch,
297
298 #[error("FILTER_MISMATCH")]
299 FilterMismatch,
300
301 #[error("INVALID_CURSOR")]
302 InvalidCursor,
303
304 #[error("INVALID_LIMIT")]
305 InvalidLimit,
306
307 #[error("ORDER_WITH_CURSOR")]
308 OrderWithCursor,
309
310 #[error("invalid cursor: invalid base64url encoding")]
312 CursorInvalidBase64,
313
314 #[error("invalid cursor: malformed JSON")]
315 CursorInvalidJson,
316
317 #[error("invalid cursor: unsupported version")]
318 CursorInvalidVersion,
319
320 #[error("invalid cursor: empty or invalid keys")]
321 CursorInvalidKeys,
322
323 #[error("invalid cursor: empty or invalid fields")]
324 CursorInvalidFields,
325
326 #[error("invalid cursor: invalid sort direction")]
327 CursorInvalidDirection,
328
329 #[error("database error: {0}")]
331 Db(String),
332
333 #[error("OData parsing unavailable: {0}")]
335 ParsingUnavailable(&'static str),
336}
337
338pub fn validate_cursor_against(
348 cursor: &CursorV1,
349 effective_order: &ODataOrderBy,
350 effective_filter_hash: Option<&str>,
351) -> Result<(), Error> {
352 if !effective_order.equals_signed_tokens(&cursor.s) {
353 return Err(Error::OrderMismatch);
354 }
355 if let Some(cf) = cursor.f.as_deref()
356 && effective_filter_hash != Some(cf)
357 {
358 return Err(Error::FilterMismatch);
359 }
360 Ok(())
361}
362
363#[derive(Clone, Debug)]
365pub struct CursorV1 {
366 pub k: Vec<String>,
367 pub o: SortDir,
368 pub s: String,
369 pub f: Option<String>,
370 pub d: String, }
372
373impl CursorV1 {
374 pub fn encode(&self) -> serde_json::Result<String> {
379 #[derive(serde::Serialize)]
380 struct Wire<'a> {
381 v: u8,
382 k: &'a [String],
383 o: &'a str,
384 s: &'a str,
385 #[serde(skip_serializing_if = "Option::is_none")]
386 f: &'a Option<String>,
387 d: &'a str,
388 }
389 let o = match self.o {
390 SortDir::Asc => "asc",
391 SortDir::Desc => "desc",
392 };
393 let w = Wire {
394 v: 1,
395 k: &self.k,
396 o,
397 s: &self.s,
398 f: &self.f,
399 d: &self.d,
400 };
401 serde_json::to_vec(&w).map(|x| base64_url::encode(&x))
402 }
403
404 pub fn decode(token: &str) -> Result<Self, Error> {
412 #[derive(serde::Deserialize)]
413 struct Wire {
414 v: u8,
415 k: Vec<String>,
416 o: String,
417 s: String,
418 #[serde(default)]
419 f: Option<String>,
420 #[serde(default = "default_direction")]
421 d: String,
422 }
423
424 fn default_direction() -> String {
425 "fwd".to_owned()
426 }
427
428 let bytes = base64_url::decode(token).map_err(|_| Error::CursorInvalidBase64)?;
429 let w: Wire = serde_json::from_slice(&bytes).map_err(|_| Error::CursorInvalidJson)?;
430 if w.v != 1 {
431 return Err(Error::CursorInvalidVersion);
432 }
433 let o = match w.o.as_str() {
434 "asc" => SortDir::Asc,
435 "desc" => SortDir::Desc,
436 _ => return Err(Error::CursorInvalidDirection),
437 };
438 if w.k.is_empty() {
439 return Err(Error::CursorInvalidKeys);
440 }
441 if w.s.trim().is_empty() {
442 return Err(Error::CursorInvalidFields);
443 }
444 if w.d != "fwd" && w.d != "bwd" {
446 return Err(Error::CursorInvalidDirection);
447 }
448 Ok(CursorV1 {
449 k: w.k,
450 o,
451 s: w.s,
452 f: w.f,
453 d: w.d,
454 })
455 }
456}
457
458mod base64_url {
461 use base64::Engine;
462
463 pub fn encode(bytes: &[u8]) -> String {
465 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes)
466 }
467
468 pub fn decode(s: &str) -> Result<Vec<u8>, base64::DecodeError> {
470 base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(s)
471 }
472}
473
474#[derive(Clone, Debug, Default)]
476#[must_use]
477pub struct ODataQuery {
478 pub filter: Option<Box<ast::Expr>>,
479 pub order: ODataOrderBy,
480 pub limit: Option<u64>,
481 pub cursor: Option<CursorV1>,
482 pub filter_hash: Option<String>,
483 pub select: Option<Vec<String>>,
484}
485
486impl ODataQuery {
487 pub fn new() -> Self {
488 Self::default()
489 }
490
491 pub fn with_filter(mut self, expr: ast::Expr) -> Self {
492 self.filter = Some(Box::new(expr));
493 self
494 }
495
496 pub fn with_order(mut self, order: ODataOrderBy) -> Self {
497 self.order = order;
498 self
499 }
500
501 pub fn with_limit(mut self, limit: u64) -> Self {
502 self.limit = Some(limit);
503 self
504 }
505
506 pub fn with_cursor(mut self, cursor: CursorV1) -> Self {
507 self.cursor = Some(cursor);
508 self
509 }
510
511 pub fn with_filter_hash(mut self, hash: String) -> Self {
512 self.filter_hash = Some(hash);
513 self
514 }
515
516 pub fn with_select(mut self, fields: Vec<String>) -> Self {
517 self.select = Some(fields);
518 self
519 }
520
521 #[must_use]
523 pub fn filter(&self) -> Option<&ast::Expr> {
524 self.filter.as_deref()
525 }
526
527 #[must_use]
529 pub fn has_filter(&self) -> bool {
530 self.filter.is_some()
531 }
532
533 #[must_use]
535 pub fn into_filter(self) -> Option<ast::Expr> {
536 self.filter.map(|b| *b)
537 }
538
539 #[must_use]
541 pub fn has_select(&self) -> bool {
542 self.select.is_some()
543 }
544
545 #[must_use]
547 pub fn selected_fields(&self) -> Option<&[String]> {
548 self.select.as_deref()
549 }
550}
551
552impl From<Option<ast::Expr>> for ODataQuery {
553 fn from(opt: Option<ast::Expr>) -> Self {
554 match opt {
555 Some(e) => Self::default().with_filter(e),
556 None => Self::default(),
557 }
558 }
559}
560
561#[cfg(test)]
562mod odata_parse_tests;
563mod tests;
564
565mod convert_odata_filters {
566 use super::ast::{CompareOperator, Expr, Value};
567 use crate::odata_filters as od;
568
569 impl From<od::CompareOperator> for CompareOperator {
570 fn from(op: od::CompareOperator) -> Self {
571 use od::CompareOperator::{
572 Equal, GreaterOrEqual, GreaterThan, LessOrEqual, LessThan, NotEqual,
573 };
574 match op {
575 Equal => CompareOperator::Eq,
576 NotEqual => CompareOperator::Ne,
577 GreaterThan => CompareOperator::Gt,
578 GreaterOrEqual => CompareOperator::Ge,
579 LessThan => CompareOperator::Lt,
580 LessOrEqual => CompareOperator::Le,
581 }
582 }
583 }
584
585 impl From<od::Value> for Value {
586 fn from(v: od::Value) -> Self {
587 match v {
588 od::Value::Null => Value::Null,
589 od::Value::Bool(b) => Value::Bool(b),
590 od::Value::Number(n) => Value::Number(n),
591 od::Value::Uuid(u) => Value::Uuid(u),
592 od::Value::DateTime(dt) => Value::DateTime(dt),
593 od::Value::Date(d) => Value::Date(d),
594 od::Value::Time(t) => Value::Time(t),
595 od::Value::String(s) => Value::String(s),
596 }
597 }
598 }
599
600 impl From<od::Expr> for Expr {
601 fn from(e: od::Expr) -> Self {
602 use od::Expr::{And, Compare, Function, Identifier, In, Not, Or, Value};
603 match e {
604 And(a, b) => Expr::And(Box::new((*a).into()), Box::new((*b).into())),
605 Or(a, b) => Expr::Or(Box::new((*a).into()), Box::new((*b).into())),
606 Not(x) => Expr::Not(Box::new((*x).into())),
607 Compare(l, op, r) => {
608 Expr::Compare(Box::new((*l).into()), op.into(), Box::new((*r).into()))
609 }
610 In(l, list) => Expr::In(
611 Box::new((*l).into()),
612 list.into_iter().map(Into::into).collect(),
613 ),
614 Function(n, args) => Expr::Function(n, args.into_iter().map(Into::into).collect()),
615 Identifier(s) => Expr::Identifier(s),
616 Value(v) => Expr::Value(v.into()),
617 }
618 }
619 }
620}
621
622#[derive(Clone, Debug)]
624pub struct ParsedFilter {
625 expr: ast::Expr,
626 node_count: usize,
627}
628
629impl ParsedFilter {
630 #[must_use]
632 pub fn as_expr(&self) -> &ast::Expr {
633 &self.expr
634 }
635
636 #[must_use]
638 pub fn into_expr(self) -> ast::Expr {
639 self.expr
640 }
641
642 #[must_use]
644 pub fn node_count(&self) -> usize {
645 self.node_count
646 }
647}
648
649pub fn parse_filter_string(raw: &str) -> Result<ParsedFilter, Error> {
664 use crate::odata_filters as od;
665
666 fn count_ast_nodes(e: &od::Expr) -> usize {
668 use od::Expr::{And, Compare, Function, Identifier, In, Not, Or, Value};
669 match e {
670 Value(_) | Identifier(_) => 1,
671 Not(x) => 1 + count_ast_nodes(x),
672 And(a, b) | Or(a, b) | Compare(a, _, b) => 1 + count_ast_nodes(a) + count_ast_nodes(b),
673 In(a, list) => 1 + count_ast_nodes(a) + list.iter().map(count_ast_nodes).sum::<usize>(),
674 Function(_, args) => 1 + args.iter().map(count_ast_nodes).sum::<usize>(),
675 }
676 }
677
678 let ast_src = od::parse_str(raw).map_err(|e| Error::InvalidFilter(format!("{e}")))?;
679
680 let node_count = count_ast_nodes(&ast_src);
681 let expr: ast::Expr = ast_src.into();
682
683 Ok(ParsedFilter { expr, node_count })
684}