From fba32c0ac313beb60a0c0fbcef86011e20912cf3 Mon Sep 17 00:00:00 2001 From: Eric Rodrigues Pires Date: Thu, 27 Nov 2025 11:19:37 -0300 Subject: [PATCH] Add accessors and clean up query --- duper/src/lib.rs | 2 +- duper/src/parser/mod.rs | 17 +- duperq/src/accessor.rs | 134 ++++++++++++++++ duperq/src/filter.rs | 346 +++++++++++++++++++++++++--------------- duperq/src/lib.rs | 1 + duperq/src/query.rs | 341 +++++++++++++++++++++++++++++++++++++-- 6 files changed, 698 insertions(+), 143 deletions(-) create mode 100644 duperq/src/accessor.rs diff --git a/duper/src/lib.rs b/duper/src/lib.rs index d7fa0ae..04aa01f 100644 --- a/duper/src/lib.rs +++ b/duper/src/lib.rs @@ -38,7 +38,7 @@ pub mod ast; pub mod escape; pub mod format; -mod parser; +pub mod parser; #[cfg(feature = "serde")] pub mod serde; pub mod validate; diff --git a/duper/src/parser/mod.rs b/duper/src/parser/mod.rs index 2238fa8..3b92386 100644 --- a/duper/src/parser/mod.rs +++ b/duper/src/parser/mod.rs @@ -67,13 +67,13 @@ impl DuperParser { // Base rules -pub(crate) fn duper_trunk<'a>() --> impl Parser<'a, &'a str, DuperValue<'a>, extra::Err>> { +pub fn duper_trunk<'a>() +-> impl Parser<'a, &'a str, DuperValue<'a>, extra::Err>> + Clone { identified_trunk().then_ignore(end()) } -pub(crate) fn duper_value<'a>() --> impl Parser<'a, &'a str, DuperValue<'a>, extra::Err>> { +pub fn duper_value<'a>() +-> impl Parser<'a, &'a str, DuperValue<'a>, extra::Err>> + Clone { identified_value().then_ignore(end()) } @@ -164,7 +164,8 @@ pub(crate) fn identified_value<'a>() boolean().map(DuperInner::Boolean), null().map(|_| DuperInner::Null), )) - .padded_by(whitespace_and_comments()); + .padded_by(whitespace_and_comments()) + .boxed(); choice(( temporal_specified().map(|temporal| { @@ -229,8 +230,8 @@ pub(crate) fn object<'a>( ) } -pub(crate) fn object_key<'a>() --> impl Parser<'a, &'a str, DuperKey<'a>, extra::Err>> + Clone { +pub fn object_key<'a>() -> impl Parser<'a, &'a str, DuperKey<'a>, extra::Err>> + Clone +{ let plain_key = ascii_alphabetic() .to_slice() .or(just('_').then(ascii_alphanumeric()).to_slice()) @@ -457,7 +458,7 @@ pub(crate) fn float<'a>() -> impl Parser<'a, &'a str, f64, extra::Err() -> impl Parser<'a, &'a str, i64, extra::Err>> + Clone { +pub fn integer<'a>() -> impl Parser<'a, &'a str, i64, extra::Err>> + Clone { let decimal_integer = one_of("+-") .or_not() .then(integer_digits()) diff --git a/duperq/src/accessor.rs b/duperq/src/accessor.rs new file mode 100644 index 0000000..505805d --- /dev/null +++ b/duperq/src/accessor.rs @@ -0,0 +1,134 @@ +use std::{iter, ops::Bound}; + +use duper::{DuperInner, DuperValue}; + +pub(crate) trait DuperAccessor { + fn access<'accessor: 'value, 'value>( + &'accessor self, + value: &'value DuperValue<'value>, + ) -> Box> + 'value>; +} + +// Flattened accessor + +pub(crate) struct FlattenedAccessor(pub(crate) Vec>); + +impl DuperAccessor for FlattenedAccessor { + fn access<'accessor: 'value, 'value>( + &'accessor self, + value: &'value DuperValue<'value>, + ) -> Box> + 'value> { + let mut values: Box> + 'value> = + Box::new(iter::once(value)); + for accessor in self.0.iter() { + values = Box::new(values.flat_map(|value| accessor.access(value))); + } + values + } +} + +// Base accessors + +pub(crate) struct FieldAccessor(pub(crate) String); + +impl DuperAccessor for FieldAccessor { + fn access<'accessor: 'value, 'value>( + &'accessor self, + value: &'value DuperValue<'value>, + ) -> Box> + 'value> { + if let DuperInner::Object(object) = &value.inner { + Box::new( + object + .iter() + .find(|(key, _)| key.as_ref() == self.0) + .into_iter() + .map(|(_, value)| value), + ) + } else if let DuperInner::Array(array) = &value.inner { + Box::new(array.iter().filter_map(|duper| { + if let DuperInner::Object(object) = &duper.inner { + object + .iter() + .find(|(key, _)| key.as_ref() == self.0) + .map(|(_, value)| value) + } else { + None + } + })) + } else { + Box::new(iter::empty()) + } + } +} + +pub(crate) struct IndexAccessor(pub(crate) usize); + +impl DuperAccessor for IndexAccessor { + fn access<'accessor: 'value, 'value>( + &'accessor self, + value: &'value DuperValue<'value>, + ) -> Box> + 'value> { + if let DuperInner::Array(array) = &value.inner { + Box::new(array.get(self.0).into_iter()) + } else { + Box::new(iter::empty()) + } + } +} + +pub(crate) struct ReverseIndexAccessor(pub(crate) usize); + +impl DuperAccessor for ReverseIndexAccessor { + fn access<'accessor: 'value, 'value>( + &'accessor self, + value: &'value DuperValue<'value>, + ) -> Box> + 'value> { + if let DuperInner::Array(array) = &value.inner { + Box::new(array.get(array.len() - self.0).into_iter()) + } else { + Box::new(iter::empty()) + } + } +} + +pub(crate) struct RangeIndexAccessor { + pub(crate) start: Bound, + pub(crate) end: Bound, +} + +impl DuperAccessor for RangeIndexAccessor { + fn access<'accessor: 'value, 'value>( + &'accessor self, + value: &'value DuperValue<'value>, + ) -> Box> + 'value> { + if let DuperInner::Array(array) = &value.inner { + let start = match self.start { + Bound::Included(i) => i, + Bound::Excluded(i) => i + 1, + Bound::Unbounded => 0, + }; + Box::new(array.iter().skip(start).take(match self.end { + Bound::Included(i) => (i + 1) - start, + Bound::Excluded(i) => i - start, + Bound::Unbounded => usize::MAX, + })) + } else { + Box::new(iter::empty()) + } + } +} + +pub(crate) struct AnyAccessor; + +impl DuperAccessor for AnyAccessor { + fn access<'accessor: 'value, 'value>( + &'accessor self, + value: &'value DuperValue<'value>, + ) -> Box> + 'value> { + if let DuperInner::Array(array) = &value.inner { + Box::new(array.iter()) + } else { + Box::new(iter::empty()) + } + } +} diff --git a/duperq/src/filter.rs b/duperq/src/filter.rs index b549df4..b46321c 100644 --- a/duperq/src/filter.rs +++ b/duperq/src/filter.rs @@ -1,37 +1,32 @@ -use std::{borrow::Cow, cmp::Ordering, str::FromStr}; +use std::{cmp::Ordering, fmt::Display, str::FromStr}; use duper::{DuperInner, DuperValue}; use temporal_rs::{ Duration, Instant, PlainDate, PlainDateTime, PlainMonthDay, PlainTime, PlainYearMonth, - ZonedDateTime, + TemporalError, ZonedDateTime, options::{Disambiguation, OffsetDisambiguation}, }; -use tinyvec::TinyVec; -pub(crate) trait Filter { - fn apply<'v>(&self, value: &'v DuperValue<'_>) -> bool; -} +use crate::accessor::DuperAccessor; -impl<'filter> Default for &'filter dyn Filter { - fn default() -> Self { - &FalseFilter - } +pub(crate) trait DuperFilter { + fn filter<'v>(&self, value: &'v DuperValue<'_>) -> bool; } // Branchless filters pub(crate) struct FalseFilter; -impl Filter for FalseFilter { - fn apply<'v>(&self, _: &'v DuperValue<'v>) -> bool { +impl DuperFilter for FalseFilter { + fn filter<'v>(&self, _: &'v DuperValue<'v>) -> bool { false } } pub(crate) struct TrueFilter; -impl Filter for TrueFilter { - fn apply<'v>(&self, _: &'v DuperValue<'v>) -> bool { +impl DuperFilter for TrueFilter { + fn filter<'v>(&self, _: &'v DuperValue<'v>) -> bool { true } } @@ -39,118 +34,104 @@ impl Filter for TrueFilter { // Container filters #[derive(Default)] -pub(crate) struct AndFilter<'filter>(TinyVec<[&'filter dyn Filter; 4]>); +pub(crate) struct AndFilter(pub(crate) Vec>); -impl<'filter> Filter for AndFilter<'filter> { - fn apply<'v>(&self, value: &'v DuperValue<'v>) -> bool { - self.0.iter().all(|inner| inner.apply(value)) +impl DuperFilter for AndFilter { + fn filter<'v>(&self, value: &'v DuperValue<'v>) -> bool { + self.0.iter().all(|inner| inner.filter(value)) } } -impl<'filter> FromIterator<&'filter dyn Filter> for AndFilter<'filter> { - fn from_iter>(iter: T) -> Self { +impl FromIterator> for AndFilter { + fn from_iter>>(iter: T) -> Self { Self(iter.into_iter().collect()) } } -impl<'a> chumsky::container::Container<&'a dyn Filter> for AndFilter<'a> { - fn push(&mut self, item: &'a dyn Filter) { +impl chumsky::container::Container> for AndFilter { + fn push(&mut self, item: Box) { self.0.push(item); } } #[derive(Default)] -pub(crate) struct OrFilter<'filter>(TinyVec<[&'filter dyn Filter; 4]>); +pub(crate) struct OrFilter(pub(crate) Vec>); -impl<'filter> Filter for OrFilter<'filter> { - fn apply<'v>(&self, value: &'v DuperValue<'v>) -> bool { - self.0.iter().any(|inner| inner.apply(value)) +impl DuperFilter for OrFilter { + fn filter<'v>(&self, value: &'v DuperValue<'v>) -> bool { + self.0.iter().any(|inner| inner.filter(value)) } } -impl<'filter> FromIterator<&'filter dyn Filter> for OrFilter<'filter> { - fn from_iter>(iter: T) -> Self { +impl FromIterator> for OrFilter { + fn from_iter>>(iter: T) -> Self { Self(iter.into_iter().collect()) } } -impl<'a> chumsky::container::Container<&'a dyn Filter> for OrFilter<'a> { - fn push(&mut self, item: &'a dyn Filter) { +impl<'a> chumsky::container::Container> for OrFilter { + fn push(&mut self, item: Box) { self.0.push(item); } } -pub(crate) struct NotFilter<'filter>(&'filter dyn Filter); +pub(crate) struct NotFilter(pub(crate) Box); -impl<'filter> Filter for NotFilter<'filter> { - fn apply<'v>(&self, value: &'v DuperValue<'v>) -> bool { - !self.0.apply(value) +impl DuperFilter for NotFilter { + fn filter<'v>(&self, value: &'v DuperValue<'v>) -> bool { + !self.0.filter(value) } } -// Access filters - -pub(crate) struct FieldAccessFilter<'filter>(Cow<'filter, str>, &'filter dyn Filter); +pub(crate) struct AccessorFilter { + pub(crate) filter: Box, + pub(crate) accessor: Box, +} -impl<'filter> Filter for FieldAccessFilter<'filter> { - fn apply<'v>(&self, value: &'v DuperValue<'_>) -> bool { - if let DuperInner::Object(object) = &value.inner { - object - .iter() - .find(|(key, _)| key.as_ref() == self.0) - .is_some_and(|(_, value)| self.1.apply(value)) - } else { - false - } +impl DuperFilter for AccessorFilter { + fn filter<'v>(&self, value: &'v DuperValue<'_>) -> bool { + self.accessor + .access(value) + .any(|inner| self.filter.filter(inner)) } } -pub(crate) struct IndexAccessFilter<'filter>(usize, &'filter dyn Filter); +// Leaf filters -impl<'filter> Filter for IndexAccessFilter<'filter> { - fn apply<'v>(&self, value: &'v DuperValue<'_>) -> bool { - if let DuperInner::Array(array) = &value.inner { - array.get(self.0).is_some_and(|value| self.1.apply(value)) - } else { - false - } - } +#[derive(Debug, Clone)] +pub(crate) enum TryFromDuperValueError { + InvalidType(&'static str), + InvalidSize(i64), + UnspecifiedTemporal, + TemporalError(TemporalError), } -pub(crate) struct ReverseIndexAccessFilter<'filter>(usize, &'filter dyn Filter); - -impl<'filter> Filter for ReverseIndexAccessFilter<'filter> { - fn apply<'v>(&self, value: &'v DuperValue<'_>) -> bool { - if let DuperInner::Array(array) = &value.inner { - array - .len() - .checked_sub(self.0) - .is_some_and(|i| array.get(i).is_some_and(|value| self.1.apply(value))) - } else { - false +impl Display for TryFromDuperValueError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TryFromDuperValueError::InvalidType(typ) => { + f.write_fmt(format_args!("invalid type {typ}")) + } + TryFromDuperValueError::InvalidSize(size) => { + f.write_fmt(format_args!("invalid size {size}")) + } + TryFromDuperValueError::UnspecifiedTemporal => f.write_str("unspecified Temporal type"), + TryFromDuperValueError::TemporalError(inner) => inner.fmt(f), } } } -pub(crate) struct AnyAccessFilter<'filter>(&'filter dyn Filter); - -impl<'filter> Filter for AnyAccessFilter<'filter> { - fn apply<'v>(&self, value: &'v DuperValue<'_>) -> bool { - if let DuperInner::Array(array) = &value.inner { - array.iter().any(|value| self.0.apply(value)) - } else { - false - } +impl From for TryFromDuperValueError { + fn from(value: TemporalError) -> Self { + TryFromDuperValueError::TemporalError(value) } } -// Leaf filters - -enum EqValue<'filter> { +pub(crate) enum EqValue { Len(usize), - Tuple(TinyVec<[&'filter EqFilter<'filter>; 4]>), - String(Cow<'filter, str>), - Bytes(Cow<'filter, [u8]>), + Tuple(Vec), + String(String), + Bytes(Vec), TemporalInstant(Instant), TemporalZonedDateTime(ZonedDateTime), TemporalPlainDate(PlainDate), @@ -160,21 +141,74 @@ enum EqValue<'filter> { TemporalPlainMonthDay(PlainMonthDay), TemporalDuration(Duration), Integer(i64), - Float(f64), + Float(f64, Option), Boolean(bool), Null, } -impl<'filter> Default for &'filter EqFilter<'filter> { - fn default() -> Self { - &EqFilter(EqValue::Null) +impl EqValue { + pub(crate) fn try_from_duper( + value: DuperValue<'_>, + epsilon: Option, + ) -> Result { + match value.inner { + DuperInner::Object(_) => Err(TryFromDuperValueError::InvalidType("object")), + DuperInner::Array(_) => Err(TryFromDuperValueError::InvalidType("array")), + DuperInner::Tuple(tuple) => { + let vec: Result, _> = tuple + .into_inner() + .into_iter() + .map(|value| { + EqValue::try_from_duper(value, epsilon).map(|value| EqFilter(value)) + }) + .collect(); + Ok(EqValue::Tuple(vec?)) + } + DuperInner::String(string) => Ok(EqValue::String(string.into_inner().into_owned())), + DuperInner::Bytes(bytes) => Ok(EqValue::Bytes(bytes.into_inner().into_owned())), + DuperInner::Temporal(temporal) => match value.identifier { + Some(identifier) if identifier.as_ref() == "Instant" => Ok( + EqValue::TemporalInstant(Instant::from_str(temporal.as_ref())?), + ), + Some(identifier) if identifier.as_ref() == "ZonedDateTime" => { + Ok(EqValue::TemporalZonedDateTime(ZonedDateTime::from_utf8( + temporal.as_ref().as_bytes(), + Disambiguation::Compatible, + OffsetDisambiguation::Prefer, + )?)) + } + Some(identifier) if identifier.as_ref() == "PlainDate" => Ok( + EqValue::TemporalPlainDate(PlainDate::from_str(temporal.as_ref())?), + ), + Some(identifier) if identifier.as_ref() == "PlainTime" => Ok( + EqValue::TemporalPlainTime(PlainTime::from_str(temporal.as_ref())?), + ), + Some(identifier) if identifier.as_ref() == "PlainDateTime" => Ok( + EqValue::TemporalPlainDateTime(PlainDateTime::from_str(temporal.as_ref())?), + ), + Some(identifier) if identifier.as_ref() == "PlainYearMonth" => Ok( + EqValue::TemporalPlainYearMonth(PlainYearMonth::from_str(temporal.as_ref())?), + ), + Some(identifier) if identifier.as_ref() == "PlainMonthDay" => Ok( + EqValue::TemporalPlainMonthDay(PlainMonthDay::from_str(temporal.as_ref())?), + ), + Some(identifier) if identifier.as_ref() == "Duration" => Ok( + EqValue::TemporalDuration(Duration::from_str(temporal.as_ref())?), + ), + Some(_) | None => Err(TryFromDuperValueError::UnspecifiedTemporal), + }, + DuperInner::Integer(integer) => Ok(EqValue::Integer(integer)), + DuperInner::Float(float) => Ok(EqValue::Float(float, epsilon)), + DuperInner::Boolean(boolean) => Ok(EqValue::Boolean(boolean)), + DuperInner::Null => Ok(EqValue::Null), + } } } -pub(crate) struct EqFilter<'filter>(EqValue<'filter>); +pub(crate) struct EqFilter(pub(crate) EqValue); -impl<'filter> Filter for EqFilter<'filter> { - fn apply<'v>(&self, value: &'v DuperValue<'v>) -> bool { +impl DuperFilter for EqFilter { + fn filter<'v>(&self, value: &'v DuperValue<'v>) -> bool { match (&self.0, &value.inner) { (EqValue::Len(this), DuperInner::Object(that)) => *this == that.len(), (EqValue::Len(this), DuperInner::Array(that)) => *this == that.len(), @@ -184,13 +218,13 @@ impl<'filter> Filter for EqFilter<'filter> { if this.len() == that.len() { this.iter() .zip(that.iter()) - .all(|(this, that)| this.apply(that)) + .all(|(this, that)| this.filter(that)) } else { false } } - (EqValue::String(this), DuperInner::String(that)) => this.as_ref() == that.as_ref(), - (EqValue::Bytes(this), DuperInner::Bytes(that)) => this.as_ref() == that.as_ref(), + (EqValue::String(this), DuperInner::String(that)) => this == that.as_ref(), + (EqValue::Bytes(this), DuperInner::Bytes(that)) => this == that.as_ref(), (EqValue::TemporalInstant(this), DuperInner::Temporal(that)) => { Instant::from_str(that.as_ref()).is_ok_and(|that| *this == that) } @@ -221,7 +255,9 @@ impl<'filter> Filter for EqFilter<'filter> { Duration::from_str(that.as_ref()).is_ok_and(|that| *this == that) } (EqValue::Integer(this), DuperInner::Integer(that)) => this == that, - (EqValue::Float(this), DuperInner::Float(that)) => this == that, + (EqValue::Float(this, epsilon), DuperInner::Float(that)) => { + (this - that).abs() <= epsilon.unwrap_or(0.0).abs() + } (EqValue::Boolean(this), DuperInner::Boolean(that)) => this == that, (EqValue::Null, DuperInner::Null) => true, _ => false, @@ -229,10 +265,10 @@ impl<'filter> Filter for EqFilter<'filter> { } } -pub(crate) struct NeFilter<'filter>(EqValue<'filter>); +pub(crate) struct NeFilter(pub(crate) EqValue); -impl<'filter> Filter for NeFilter<'filter> { - fn apply<'v>(&self, value: &'v DuperValue<'v>) -> bool { +impl DuperFilter for NeFilter { + fn filter<'v>(&self, value: &'v DuperValue<'v>) -> bool { match (&self.0, &value.inner) { (EqValue::Len(this), DuperInner::Object(that)) => *this != that.len(), (EqValue::Len(this), DuperInner::Array(that)) => *this != that.len(), @@ -242,13 +278,13 @@ impl<'filter> Filter for NeFilter<'filter> { if this.len() == that.len() { this.iter() .zip(that.iter()) - .any(|(this, that)| !this.apply(that)) + .any(|(this, that)| !this.filter(that)) } else { true } } - (EqValue::String(this), DuperInner::String(that)) => this.as_ref() != that.as_ref(), - (EqValue::Bytes(this), DuperInner::Bytes(that)) => this.as_ref() != that.as_ref(), + (EqValue::String(this), DuperInner::String(that)) => this != that.as_ref(), + (EqValue::Bytes(this), DuperInner::Bytes(that)) => this != that.as_ref(), (EqValue::TemporalInstant(this), DuperInner::Temporal(that)) => { Instant::from_str(that.as_ref()) .ok() @@ -294,7 +330,9 @@ impl<'filter> Filter for NeFilter<'filter> { .is_none_or(|that| *this != that) } (EqValue::Integer(this), DuperInner::Integer(that)) => this != that, - (EqValue::Float(this), DuperInner::Float(that)) => this != that, + (EqValue::Float(this, epsilon), DuperInner::Float(that)) => { + (this - that).abs() > epsilon.unwrap_or(0.0).abs() + } (EqValue::Boolean(this), DuperInner::Boolean(that)) => this != that, (EqValue::Null, DuperInner::Null) => false, _ => true, @@ -302,7 +340,7 @@ impl<'filter> Filter for NeFilter<'filter> { } } -enum CmpValue { +pub(crate) enum CmpValue { Len(usize), TemporalInstant(Instant), TemporalZonedDateTime(ZonedDateTime), @@ -316,15 +354,64 @@ enum CmpValue { Float(f64), } +impl TryFrom> for CmpValue { + type Error = TryFromDuperValueError; + + fn try_from(value: DuperValue<'_>) -> Result { + match value.inner { + DuperInner::Object(_) => Err(TryFromDuperValueError::InvalidType("object")), + DuperInner::Array(_) => Err(TryFromDuperValueError::InvalidType("array")), + DuperInner::Tuple(_) => Err(TryFromDuperValueError::InvalidType("tuple")), + DuperInner::String(_) => Err(TryFromDuperValueError::InvalidType("string")), + DuperInner::Bytes(_) => Err(TryFromDuperValueError::InvalidType("bytes")), + DuperInner::Temporal(temporal) => match value.identifier { + Some(identifier) if identifier.as_ref() == "Instant" => Ok( + CmpValue::TemporalInstant(Instant::from_str(temporal.as_ref())?), + ), + Some(identifier) if identifier.as_ref() == "ZonedDateTime" => { + Ok(CmpValue::TemporalZonedDateTime(ZonedDateTime::from_utf8( + temporal.as_ref().as_bytes(), + Disambiguation::Compatible, + OffsetDisambiguation::Prefer, + )?)) + } + Some(identifier) if identifier.as_ref() == "PlainDate" => Ok( + CmpValue::TemporalPlainDate(PlainDate::from_str(temporal.as_ref())?), + ), + Some(identifier) if identifier.as_ref() == "PlainTime" => Ok( + CmpValue::TemporalPlainTime(PlainTime::from_str(temporal.as_ref())?), + ), + Some(identifier) if identifier.as_ref() == "PlainDateTime" => Ok( + CmpValue::TemporalPlainDateTime(PlainDateTime::from_str(temporal.as_ref())?), + ), + Some(identifier) if identifier.as_ref() == "PlainYearMonth" => Ok( + CmpValue::TemporalPlainYearMonth(PlainYearMonth::from_str(temporal.as_ref())?), + ), + Some(identifier) if identifier.as_ref() == "PlainMonthDay" => Ok( + CmpValue::TemporalPlainMonthDay(PlainMonthDay::from_str(temporal.as_ref())?), + ), + Some(identifier) if identifier.as_ref() == "Duration" => Ok( + CmpValue::TemporalDuration(Duration::from_str(temporal.as_ref())?), + ), + Some(_) | None => Err(TryFromDuperValueError::UnspecifiedTemporal), + }, + DuperInner::Integer(integer) => Ok(CmpValue::Integer(integer)), + DuperInner::Float(float) => Ok(CmpValue::Float(float)), + DuperInner::Boolean(_) => Err(TryFromDuperValueError::InvalidType("boolean")), + DuperInner::Null => Err(TryFromDuperValueError::InvalidType("null")), + } + } +} + macro_rules! cmp_filter { ( $filter:ident, $ord:pat ) => { - struct $filter(CmpValue); + pub(crate) struct $filter(pub(crate) CmpValue); - impl Filter for $filter { - fn apply<'v>(&self, value: &'v DuperValue<'v>) -> bool { + impl DuperFilter for $filter { + fn filter<'v>(&self, value: &'v DuperValue<'v>) -> bool { match (&self.0, &value.inner) { (CmpValue::Len(this), DuperInner::Object(that)) => { matches!(this.cmp(&that.len()), $ord) @@ -388,7 +475,7 @@ cmp_filter!(GtFilter, Ordering::Greater); cmp_filter!(LeFilter, Ordering::Less | Ordering::Equal); cmp_filter!(LtFilter, Ordering::Less); -enum IsFilter { +pub(crate) enum IsFilter { Object, Array, Tuple, @@ -410,8 +497,8 @@ enum IsFilter { Null, } -impl Filter for IsFilter { - fn apply<'v>(&self, value: &'v DuperValue<'v>) -> bool { +impl DuperFilter for IsFilter { + fn filter<'v>(&self, value: &'v DuperValue<'v>) -> bool { match (&self, &value.inner) { (IsFilter::Object, DuperInner::Object(_)) => true, (IsFilter::Array, DuperInner::Array(_)) => true, @@ -458,33 +545,23 @@ impl Filter for IsFilter { } } -pub(crate) struct RegexFilter(regex::Regex); +pub(crate) struct RegexFilter(pub(crate) regex::bytes::Regex); -impl Filter for RegexFilter { - fn apply<'v>(&self, value: &'v DuperValue<'v>) -> bool { - match &value.inner { - DuperInner::String(string) => self.0.find(string.as_ref()).is_some(), - DuperInner::Temporal(temporal) => self.0.find(temporal.as_ref()).is_some(), - _ => false, - } - } -} - -pub(crate) struct RegexBytesFilter(regex::bytes::Regex); - -impl Filter for RegexBytesFilter { - fn apply<'v>(&self, value: &'v DuperValue<'v>) -> bool { +impl DuperFilter for RegexFilter { + fn filter<'v>(&self, value: &'v DuperValue<'v>) -> bool { match &value.inner { + DuperInner::String(string) => self.0.find(string.as_ref().as_bytes()).is_some(), DuperInner::Bytes(bytes) => self.0.find(bytes.as_ref()).is_some(), + DuperInner::Temporal(temporal) => self.0.find(temporal.as_ref().as_bytes()).is_some(), _ => false, } } } -pub(crate) struct FieldExistsFilter<'filter>(Cow<'filter, str>); +pub(crate) struct FieldExistsFilter(pub(crate) String); -impl<'filter> Filter for FieldExistsFilter<'filter> { - fn apply<'v>(&self, value: &'v DuperValue<'_>) -> bool { +impl DuperFilter for FieldExistsFilter { + fn filter<'v>(&self, value: &'v DuperValue<'_>) -> bool { if let DuperInner::Object(object) = &value.inner { object .iter() @@ -495,3 +572,22 @@ impl<'filter> Filter for FieldExistsFilter<'filter> { } } } + +pub(crate) struct IsTruthyFilter; + +impl DuperFilter for IsTruthyFilter { + fn filter<'v>(&self, value: &'v DuperValue<'_>) -> bool { + match &value.inner { + DuperInner::Object(object) => !object.is_empty(), + DuperInner::Array(array) => !array.is_empty(), + DuperInner::Tuple(tuple) => !tuple.is_empty(), + DuperInner::String(string) => !string.is_empty(), + DuperInner::Bytes(bytes) => !bytes.is_empty(), + DuperInner::Temporal(_) => true, + DuperInner::Integer(integer) => *integer != 0, + DuperInner::Float(float) => *float != 0.0, + DuperInner::Boolean(boolean) => *boolean, + DuperInner::Null => false, + } + } +} diff --git a/duperq/src/lib.rs b/duperq/src/lib.rs index d447b1a..129e5d8 100644 --- a/duperq/src/lib.rs +++ b/duperq/src/lib.rs @@ -1,2 +1,3 @@ +mod accessor; mod filter; mod query; diff --git a/duperq/src/query.rs b/duperq/src/query.rs index ee43056..2ed27fd 100644 --- a/duperq/src/query.rs +++ b/duperq/src/query.rs @@ -2,20 +2,343 @@ // duperq 'metadata.tags[created_at >= Instant(2025-11-22T00:00:00-03:00)]' use chumsky::prelude::*; +use duper::{ + DuperInner, + parser::{duper_value, integer, object_key}, +}; -use crate::filter::{AndFilter, Filter}; +use crate::{ + accessor::{ + AnyAccessor, DuperAccessor, FieldAccessor, FlattenedAccessor, IndexAccessor, + RangeIndexAccessor, ReverseIndexAccessor, + }, + filter::{ + AccessorFilter, AndFilter, CmpValue, DuperFilter, EqFilter, EqValue, GeFilter, GtFilter, + IsFilter, IsTruthyFilter, LeFilter, LtFilter, NeFilter, NotFilter, OrFilter, RegexFilter, + TryFromDuperValueError, + }, +}; -fn query<'a>() -> impl Parser<'a, &'a str, (AndFilter<'a>, Option<()>), extra::Err>> -{ - filter() +fn query<'a>() +-> impl Parser<'a, &'a str, (Box, Option<()>), extra::Err>> { + choice((just("filter").padded().ignore_then(filter()),)) + .separated_by(just('|')) + .collect::() + .map(|filter| Box::new(filter) as Box) .padded() - .separated_by(just('|').padded()) - .collect::>() - .then(just('|').ignore_then(fmt().padded()).or_not()) + .then( + just('|') + .padded() + .ignore_then(just("format").padded()) + .ignore_then(fmt().padded()) + .or_not(), + ) +} + +fn filter<'a>() -> impl Parser<'a, &'a str, Box, extra::Err>> + Clone +{ + recursive(|filter| { + let atom = leaf_filter() + .or(filter.delimited_by(just('('), just(')'))) + .padded(); + + let unary = just('!') + .ignored() + .or(just("not").ignored()) + .padded() + .repeated() + .at_least(1) + .foldr(atom.clone(), |_, rhs| { + Box::new(NotFilter(rhs)) as Box + }) + .or(atom) + .padded(); + + let and = unary + .clone() + .separated_by(just("&&").or(just("and"))) + .at_least(2) + .collect::() + .map(|filter| Box::new(filter) as Box) + .or(unary) + .padded(); + + let or = and + .clone() + .separated_by(just("||").or(just("or"))) + .at_least(2) + .collect::() + .map(|filter| Box::new(filter) as Box) + .or(and) + .padded(); + + or + }) } -fn filter<'a>() -> impl Parser<'a, &'a str, &'a dyn Filter, extra::Err>> { - todo!() +fn accessor<'a>() +-> impl Parser<'a, &'a str, Box, extra::Err>> + Clone { + let access = just('.').or_not().ignore_then(choice(( + object_key().padded().map(|key: duper::DuperKey<'a>| { + Box::new(FieldAccessor(key.as_ref().into())) as Box + }), + integer() + .or_not() + .padded() + .then_ignore(just("..")) + .then(just('=').or_not()) + .then(integer().or_not().padded()) + .delimited_by(just('['), just(']')) + .padded() + .try_map(|((start, end_inclusive), end), span| match (start, end) { + (Some(start), _) if start < 0 => { + Err(Rich::custom(span, "range start must be positive")) + } + (_, Some(end)) if end < 0 => Err(Rich::custom(span, "range end must be positive")), + (None, None) => Ok(RangeIndexAccessor { + start: std::ops::Bound::Unbounded, + end: std::ops::Bound::Unbounded, + }), + (Some(start), None) => Ok(RangeIndexAccessor { + start: std::ops::Bound::Included(start as usize), + end: std::ops::Bound::Unbounded, + }), + (None, Some(end)) => Ok(RangeIndexAccessor { + start: std::ops::Bound::Unbounded, + end: if end_inclusive.is_some() { + std::ops::Bound::Included(end as usize) + } else { + std::ops::Bound::Excluded(end as usize) + }, + }), + (Some(start), Some(end)) => Ok(RangeIndexAccessor { + start: std::ops::Bound::Included(start as usize), + end: if end_inclusive.is_some() { + std::ops::Bound::Included(end as usize) + } else { + std::ops::Bound::Excluded(end as usize) + }, + }), + }) + .map(|accessor| Box::new(accessor) as Box), + integer() + .padded() + .delimited_by(just('['), just(']')) + .padded() + .map(|int| { + if int < 0 { + Box::new(ReverseIndexAccessor(int.unsigned_abs() as usize)) + as Box + } else { + Box::new(IndexAccessor(int as usize)) as Box + } + }), + text::whitespace() + .delimited_by(just('['), just(']')) + .padded() + .map(|_| Box::new(AnyAccessor) as Box), + ))); + + access + .clone() + .repeated() + .at_least(2) + .collect::>() + .map(|vec| Box::new(FlattenedAccessor(vec)) as Box) + .or(access) +} + +fn leaf_filter<'a>() +-> impl Parser<'a, &'a str, Box, extra::Err>> + Clone { + let accessor = accessor(); + + let eq_op = just("==").ignored().or(just('=').ignored()).padded(); + let ne_op = just("!=").ignored().or(just("<>").ignored()).padded(); + let lt_op = just("<").ignored().padded(); + let le_op = just("<=").ignored().padded(); + let gt_op = just(">").ignored().padded(); + let ge_op = just(">=").ignored().padded(); + let re_op = just("=~").ignored().padded(); + let is_op = just("is").ignored().padded(); + + just("len") + .ignore_then(accessor.clone().delimited_by(just('('), just(')'))) + .then(choice(( + eq_op + .clone() + .ignore_then(integer().padded()) + .try_map(|value, span| { + if value >= 0 { + Ok(Box::new(EqFilter(EqValue::Len(value as usize))) + as Box) + } else { + Err(Rich::custom( + span, + TryFromDuperValueError::InvalidSize(value), + )) + } + }), + ne_op + .clone() + .ignore_then(integer().padded()) + .try_map(|value, span| { + if value >= 0 { + Ok(Box::new(NeFilter(EqValue::Len(value as usize))) + as Box) + } else { + Err(Rich::custom( + span, + TryFromDuperValueError::InvalidSize(value), + )) + } + }), + lt_op + .clone() + .ignore_then(integer().padded()) + .try_map(|value, span| { + if value >= 0 { + Ok(Box::new(LtFilter(CmpValue::Len(value as usize))) + as Box) + } else { + Err(Rich::custom( + span, + TryFromDuperValueError::InvalidSize(value), + )) + } + }), + le_op + .clone() + .ignore_then(integer().padded()) + .try_map(|value, span| { + if value >= 0 { + Ok(Box::new(LeFilter(CmpValue::Len(value as usize))) + as Box) + } else { + Err(Rich::custom( + span, + TryFromDuperValueError::InvalidSize(value), + )) + } + }), + gt_op + .clone() + .ignore_then(integer().padded()) + .try_map(|value, span| { + if value >= 0 { + Ok(Box::new(GtFilter(CmpValue::Len(value as usize))) + as Box) + } else { + Err(Rich::custom( + span, + TryFromDuperValueError::InvalidSize(value), + )) + } + }), + ge_op + .clone() + .ignore_then(integer().padded()) + .try_map(|value, span| { + if value >= 0 { + Ok(Box::new(GeFilter(CmpValue::Len(value as usize))) + as Box) + } else { + Err(Rich::custom( + span, + TryFromDuperValueError::InvalidSize(value), + )) + } + }), + ))) + .or(accessor.clone().then(choice(( + eq_op + .ignore_then(duper_value().padded()) + .try_map(|value, span| { + EqValue::try_from_duper(value, None) + .map(|value| Box::new(EqFilter(value)) as Box) + .map_err(|error| Rich::custom(span, error)) + }), + ne_op + .ignore_then(duper_value().padded()) + .try_map(|value, span| { + EqValue::try_from_duper(value, None) + .map(|value| Box::new(NeFilter(value)) as Box) + .map_err(|error| Rich::custom(span, error)) + }), + lt_op + .ignore_then(duper_value().padded()) + .try_map(|value, span| { + CmpValue::try_from(value) + .map(|value| Box::new(LtFilter(value)) as Box) + .map_err(|error| Rich::custom(span, error)) + }), + le_op + .ignore_then(duper_value().padded()) + .try_map(|value, span| { + CmpValue::try_from(value) + .map(|value| Box::new(LeFilter(value)) as Box) + .map_err(|error| Rich::custom(span, error)) + }), + gt_op + .ignore_then(duper_value().padded()) + .try_map(|value, span| { + CmpValue::try_from(value) + .map(|value| Box::new(GtFilter(value)) as Box) + .map_err(|error| Rich::custom(span, error)) + }), + ge_op + .ignore_then(duper_value().padded()) + .try_map(|value, span| { + CmpValue::try_from(value) + .map(|value| Box::new(GeFilter(value)) as Box) + .map_err(|error| Rich::custom(span, error)) + }), + re_op + .ignore_then(duper_value().padded()) + .try_map(|value, span| match value.inner { + DuperInner::String(string) => regex::bytes::Regex::new(string.as_ref()) + .map(|regex| Box::new(RegexFilter(regex)) as Box) + .map_err(|error| Rich::custom(span, error)), + _ => Err(Rich::custom( + span, + "can only use regex operator =~ with string", + )), + }), + is_op + .ignore_then( + choice(( + just("Object").map(|_| IsFilter::Object), + just("Array").map(|_| IsFilter::Array), + just("Tuple").map(|_| IsFilter::Tuple), + just("String").map(|_| IsFilter::String), + just("Bytes").map(|_| IsFilter::Bytes), + just("Instant").map(|_| IsFilter::TemporalInstant), + just("ZonedDateTime").map(|_| IsFilter::TemporalZonedDateTime), + just("PlainDate").map(|_| IsFilter::TemporalPlainDate), + just("PlainTime").map(|_| IsFilter::TemporalPlainTime), + just("PlainDateTime").map(|_| IsFilter::TemporalPlainDateTime), + just("PlainYearMonth").map(|_| IsFilter::TemporalPlainYearMonth), + just("PlainMonthDay").map(|_| IsFilter::TemporalPlainMonthDay), + just("Duration").map(|_| IsFilter::TemporalDuration), + just("Temporal").map(|_| IsFilter::TemporalUnspecified), + just("Integer").map(|_| IsFilter::Integer), + just("Float").map(|_| IsFilter::Float), + just("Number").map(|_| IsFilter::Number), + just("Boolean").map(|_| IsFilter::Boolean), + just("Null").map(|_| IsFilter::Null), + )) + .padded(), + ) + .map(|value| Box::new(value) as Box), + )))) + .map(|(accessor, filter)| { + Box::new(AccessorFilter { accessor, filter }) as Box + }) + .or(accessor.map(|accessor| { + Box::new(AccessorFilter { + accessor, + filter: Box::new(IsTruthyFilter), + }) as Box + })) } fn fmt<'a>() -> impl Parser<'a, &'a str, (), extra::Err>> { -- 2.51.2