From bb7df97e6751102018bdd04b0428a066449a8e98 Mon Sep 17 00:00:00 2001 From: Eric Rodrigues Pires Date: Tue, 25 Nov 2025 08:07:50 -0300 Subject: [PATCH] Add duperq with initial filters --- Cargo.lock | 157 ++++++++++++ Cargo.toml | 2 +- duper_website/docs/spec.md | 6 +- duperq/Cargo.toml | 22 ++ duperq/LICENSE | 25 ++ duperq/README.md | 13 + duperq/src/filter.rs | 497 +++++++++++++++++++++++++++++++++++++ duperq/src/lib.rs | 2 + duperq/src/main.rs | 15 ++ duperq/src/query.rs | 26 ++ 10 files changed, 762 insertions(+), 3 deletions(-) create mode 100644 duperq/Cargo.toml create mode 100644 duperq/LICENSE create mode 100644 duperq/README.md create mode 100644 duperq/src/filter.rs create mode 100644 duperq/src/lib.rs create mode 100644 duperq/src/main.rs create mode 100644 duperq/src/query.rs diff --git a/Cargo.lock b/Cargo.lock index fece4d6..7c1f04a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -406,6 +406,16 @@ dependencies = [ "serde", ] +[[package]] +name = "calendrical_calculations" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a0b39595c6ee54a8d0900204ba4c401d0ab4eb45adaf07178e8d017541529e7" +dependencies = [ + "core_maths", + "displaydoc", +] + [[package]] name = "camino" version = "1.2.1" @@ -543,6 +553,16 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "concolor" version = "0.1.1" @@ -581,6 +601,15 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core_maths" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +dependencies = [ + "libm", +] + [[package]] name = "cranelift-bforest" version = "0.116.1" @@ -832,6 +861,21 @@ dependencies = [ "tree-sitter-duper", ] +[[package]] +name = "duperq" +version = "0.1.0" +dependencies = [ + "chumsky", + "clap", + "duper", + "owo-colors", + "regex", + "temporal_rs", + "tinyvec", + "tracing", + "tracing-subscriber", +] + [[package]] name = "either" version = "1.15.0" @@ -1196,6 +1240,28 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_calendar" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f0e52e009b6b16ba9c0693578796f2dd4aaa59a7f8f920423706714a89ac4e" +dependencies = [ + "calendrical_calculations", + "displaydoc", + "icu_calendar_data", + "icu_locale", + "icu_locale_core", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_calendar_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527f04223b17edfe0bd43baf14a0cb1b017830db65f3950dc00224860a9a446d" + [[package]] name = "icu_collections" version = "2.1.1" @@ -1209,6 +1275,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "icu_locale" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "532b11722e350ab6bf916ba6eb0efe3ee54b932666afec989465f9243fe6dd60" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_locale_data", + "icu_provider", + "potential_utf", + "tinystr", + "zerovec", +] + [[package]] name = "icu_locale_core" version = "2.1.1" @@ -1217,11 +1298,18 @@ checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", + "serde", "tinystr", "writeable", "zerovec", ] +[[package]] +name = "icu_locale_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f03e2fcaefecdf05619f3d6f91740e79ab969b4dd54f77cbf546b1d0d28e3147" + [[package]] name = "icu_normalizer" version = "2.1.1" @@ -1270,6 +1358,8 @@ checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", + "serde", + "stable_deref_trait", "writeable", "yoke", "zerofrom", @@ -1392,6 +1482,18 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "ixdtf" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84de9d95a6d2547d9b77ee3f25fa0ee32e3c3a6484d47a55adebc0439c077992" + +[[package]] +name = "jiff-tzdb" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1283705eb0a21404d2bfd6eef2a7593d240bc42a0bdb39db0ad6fa2ec026524" + [[package]] name = "js-sys" version = "0.3.77" @@ -1751,6 +1853,8 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ + "serde_core", + "writeable", "zerovec", ] @@ -2535,6 +2639,24 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "temporal_rs" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88afde3bd75d2fc68d77a914bece426aa08aa7649ffd0cdd4a11c3d4d33474d1" +dependencies = [ + "core_maths", + "iana-time-zone", + "icu_calendar", + "icu_locale", + "ixdtf", + "num-traits", + "timezone_provider", + "tinystr", + "web-time", + "writeable", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -2620,6 +2742,20 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "timezone_provider" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9ba0000e9e73862f3e7ca1ff159e2ddf915c9d8bb11e38a7874760f445d993" +dependencies = [ + "combine", + "jiff-tzdb", + "tinystr", + "tzif", + "zerotrie", + "zerovec", +] + [[package]] name = "tinystr" version = "0.8.2" @@ -2627,6 +2763,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", + "serde_core", "zerovec", ] @@ -2910,6 +3047,15 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4013970217383f67b18aef68f6fb2e8d409bc5755227092d32efb0422ba24b8" +[[package]] +name = "tzif" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0376dfa52cce372f3b095010fd064fb850a5d8fbfd5be8b0ffa3d64eeab5a5d" +dependencies = [ + "combine", +] + [[package]] name = "unicode-general-category" version = "1.1.0" @@ -3514,6 +3660,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "weedle2" version = "5.0.0" @@ -3966,6 +4122,7 @@ version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ + "serde", "yoke", "zerofrom", "zerovec-derive", diff --git a/Cargo.toml b/Cargo.toml index b595e9a..403ae8b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ members = [ "duper_website", "serde_duper", "serde_duper_macros", - "tree-sitter-duper", + "tree-sitter-duper", "duperq", ] [profile.release] diff --git a/duper_website/docs/spec.md b/duper_website/docs/spec.md index 5d77965..ca0e336 100644 --- a/duper_website/docs/spec.md +++ b/duper_website/docs/spec.md @@ -508,7 +508,7 @@ Arrays can span multiple lines. A trailing comma is permitted after the last val ## Tuples -Tuples are similar to arrays, although parsers may choose to handle them differently. They are surrounded by parenthesis `(` and `)`. +Tuples are similar to arrays, but are surrounded by parenthesis `(` and `)` instead. ```duper { @@ -518,7 +518,7 @@ Tuples are similar to arrays, although parsers may choose to handle them differe another_single_element: (1,), tuple_of_arrays: ([true, 1.0], ["x", "y", "z"]), array_of_tuples: [(1, null), (3, 4.0, 5)], - nested: (((), ("hi"))) + nested: (((), ("hi"))), multiline_tuple: ( "Vec", "Cow", @@ -533,6 +533,8 @@ Tuples are similar to arrays, although parsers may choose to handle them differe Any parenthesized expression must be interpreted as a tuple by parsers. +Parsers may choose to handle them differently from arrays. For example, in most programming languages, tuples might be treated as fixed-size values, while arrays equivalents are considered growable/shrinkable elements. On the other hand, it might make sense to index into an array freely, while tuples may get treated as a unit. + ## Identifiers Identifiers are type-like annotations that wrap any kind of value, providing semantic meaning or hinting at special handling during parsing/validation. Identified values are composed of the identifier name, followed by the value wrapped in parenthesis `(` and `)`. Whitespace and comments around the identifier name or its parenthesis is ignored. diff --git a/duperq/Cargo.toml b/duperq/Cargo.toml new file mode 100644 index 0000000..5376dbd --- /dev/null +++ b/duperq/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "duperq" +description = "LSP implementation for Duper." +version = "0.1.0" +edition = "2024" +rust-version = "1.88" +license = "MIT" +authors = ["Eric Rodrigues Pires "] +repository = "https://github.com/EpicEric/duper" +homepage = "https://duper.dev.br" +readme = "README.md" + +[dependencies] +chumsky = "0.11.2" +clap = { version = "4.5.53", features = ["derive"] } +duper = { version = "0.4.2", path = "../duper" } +owo-colors = "4.2.3" +regex = "1.12.2" +temporal_rs = "0.1.2" +tinyvec = { version = "1.10.0", features = ["alloc"] } +tracing = "0.1.41" +tracing-subscriber = "0.3.20" diff --git a/duperq/LICENSE b/duperq/LICENSE new file mode 100644 index 0000000..8c2d6f9 --- /dev/null +++ b/duperq/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) 2025 Eric Rodrigues Pires + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/duperq/README.md b/duperq/README.md new file mode 100644 index 0000000..5dadaf1 --- /dev/null +++ b/duperq/README.md @@ -0,0 +1,13 @@ +

+ The Duper logo, with a confident spectacled mole wearing a flailing blue cape.
+

+

duperq

+ +

+ Crates.io version + GitHub license +

+ +A high-performance Duper filter and processor. + +[Check out the official website for Duper.](https://duper.dev.br) diff --git a/duperq/src/filter.rs b/duperq/src/filter.rs new file mode 100644 index 0000000..b549df4 --- /dev/null +++ b/duperq/src/filter.rs @@ -0,0 +1,497 @@ +use std::{borrow::Cow, cmp::Ordering, str::FromStr}; + +use duper::{DuperInner, DuperValue}; +use temporal_rs::{ + Duration, Instant, PlainDate, PlainDateTime, PlainMonthDay, PlainTime, PlainYearMonth, + ZonedDateTime, + options::{Disambiguation, OffsetDisambiguation}, +}; +use tinyvec::TinyVec; + +pub(crate) trait Filter { + fn apply<'v>(&self, value: &'v DuperValue<'_>) -> bool; +} + +impl<'filter> Default for &'filter dyn Filter { + fn default() -> Self { + &FalseFilter + } +} + +// Branchless filters + +pub(crate) struct FalseFilter; + +impl Filter for FalseFilter { + fn apply<'v>(&self, _: &'v DuperValue<'v>) -> bool { + false + } +} + +pub(crate) struct TrueFilter; + +impl Filter for TrueFilter { + fn apply<'v>(&self, _: &'v DuperValue<'v>) -> bool { + true + } +} + +// Container filters + +#[derive(Default)] +pub(crate) struct AndFilter<'filter>(TinyVec<[&'filter dyn Filter; 4]>); + +impl<'filter> Filter for AndFilter<'filter> { + fn apply<'v>(&self, value: &'v DuperValue<'v>) -> bool { + self.0.iter().all(|inner| inner.apply(value)) + } +} + +impl<'filter> FromIterator<&'filter dyn Filter> for AndFilter<'filter> { + 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) { + self.0.push(item); + } +} + +#[derive(Default)] +pub(crate) struct OrFilter<'filter>(TinyVec<[&'filter dyn Filter; 4]>); + +impl<'filter> Filter for OrFilter<'filter> { + fn apply<'v>(&self, value: &'v DuperValue<'v>) -> bool { + self.0.iter().any(|inner| inner.apply(value)) + } +} + +impl<'filter> FromIterator<&'filter dyn Filter> for OrFilter<'filter> { + 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) { + self.0.push(item); + } +} + +pub(crate) struct NotFilter<'filter>(&'filter dyn Filter); + +impl<'filter> Filter for NotFilter<'filter> { + fn apply<'v>(&self, value: &'v DuperValue<'v>) -> bool { + !self.0.apply(value) + } +} + +// Access filters + +pub(crate) struct FieldAccessFilter<'filter>(Cow<'filter, str>, &'filter dyn Filter); + +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 + } + } +} + +pub(crate) struct IndexAccessFilter<'filter>(usize, &'filter dyn Filter); + +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 + } + } +} + +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 + } + } +} + +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 + } + } +} + +// Leaf filters + +enum EqValue<'filter> { + Len(usize), + Tuple(TinyVec<[&'filter EqFilter<'filter>; 4]>), + String(Cow<'filter, str>), + Bytes(Cow<'filter, [u8]>), + TemporalInstant(Instant), + TemporalZonedDateTime(ZonedDateTime), + TemporalPlainDate(PlainDate), + TemporalPlainTime(PlainTime), + TemporalPlainDateTime(PlainDateTime), + TemporalPlainYearMonth(PlainYearMonth), + TemporalPlainMonthDay(PlainMonthDay), + TemporalDuration(Duration), + Integer(i64), + Float(f64), + Boolean(bool), + Null, +} + +impl<'filter> Default for &'filter EqFilter<'filter> { + fn default() -> Self { + &EqFilter(EqValue::Null) + } +} + +pub(crate) struct EqFilter<'filter>(EqValue<'filter>); + +impl<'filter> Filter for EqFilter<'filter> { + fn apply<'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(), + (EqValue::Len(this), DuperInner::String(that)) => *this == that.as_ref().len(), + (EqValue::Len(this), DuperInner::Bytes(that)) => *this == that.as_ref().len(), + (EqValue::Tuple(this), DuperInner::Tuple(that)) => { + if this.len() == that.len() { + this.iter() + .zip(that.iter()) + .all(|(this, that)| this.apply(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::TemporalInstant(this), DuperInner::Temporal(that)) => { + Instant::from_str(that.as_ref()).is_ok_and(|that| *this == that) + } + (EqValue::TemporalZonedDateTime(this), DuperInner::Temporal(that)) => { + ZonedDateTime::from_utf8( + that.as_ref().as_bytes(), + Disambiguation::Compatible, + OffsetDisambiguation::Prefer, + ) + .is_ok_and(|that| this.compare_instant(&that).is_eq()) + } + (EqValue::TemporalPlainDate(this), DuperInner::Temporal(that)) => { + PlainDate::from_str(that.as_ref()).is_ok_and(|that| *this == that) + } + (EqValue::TemporalPlainTime(this), DuperInner::Temporal(that)) => { + PlainTime::from_str(that.as_ref()).is_ok_and(|that| *this == that) + } + (EqValue::TemporalPlainDateTime(this), DuperInner::Temporal(that)) => { + PlainDateTime::from_str(that.as_ref()).is_ok_and(|that| *this == that) + } + (EqValue::TemporalPlainYearMonth(this), DuperInner::Temporal(that)) => { + PlainYearMonth::from_str(that.as_ref()).is_ok_and(|that| *this == that) + } + (EqValue::TemporalPlainMonthDay(this), DuperInner::Temporal(that)) => { + PlainMonthDay::from_str(that.as_ref()).is_ok_and(|that| *this == that) + } + (EqValue::TemporalDuration(this), DuperInner::Temporal(that)) => { + 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::Boolean(this), DuperInner::Boolean(that)) => this == that, + (EqValue::Null, DuperInner::Null) => true, + _ => false, + } + } +} + +pub(crate) struct NeFilter<'filter>(EqValue<'filter>); + +impl<'filter> Filter for NeFilter<'filter> { + fn apply<'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(), + (EqValue::Len(this), DuperInner::String(that)) => *this != that.as_ref().len(), + (EqValue::Len(this), DuperInner::Bytes(that)) => *this != that.as_ref().len(), + (EqValue::Tuple(this), DuperInner::Tuple(that)) => { + if this.len() == that.len() { + this.iter() + .zip(that.iter()) + .any(|(this, that)| !this.apply(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::TemporalInstant(this), DuperInner::Temporal(that)) => { + Instant::from_str(that.as_ref()) + .ok() + .is_none_or(|that| *this != that) + } + (EqValue::TemporalZonedDateTime(this), DuperInner::Temporal(that)) => { + ZonedDateTime::from_utf8( + that.as_ref().as_bytes(), + Disambiguation::Compatible, + OffsetDisambiguation::Prefer, + ) + .ok() + .is_none_or(|that| this.compare_instant(&that).is_ne()) + } + (EqValue::TemporalPlainDate(this), DuperInner::Temporal(that)) => { + PlainDate::from_str(that.as_ref()) + .ok() + .is_none_or(|that| *this != that) + } + (EqValue::TemporalPlainTime(this), DuperInner::Temporal(that)) => { + PlainTime::from_str(that.as_ref()) + .ok() + .is_none_or(|that| *this != that) + } + (EqValue::TemporalPlainDateTime(this), DuperInner::Temporal(that)) => { + PlainDateTime::from_str(that.as_ref()) + .ok() + .is_none_or(|that| *this != that) + } + (EqValue::TemporalPlainYearMonth(this), DuperInner::Temporal(that)) => { + PlainYearMonth::from_str(that.as_ref()) + .ok() + .is_none_or(|that| *this != that) + } + (EqValue::TemporalPlainMonthDay(this), DuperInner::Temporal(that)) => { + PlainMonthDay::from_str(that.as_ref()) + .ok() + .is_none_or(|that| *this != that) + } + (EqValue::TemporalDuration(this), DuperInner::Temporal(that)) => { + Duration::from_str(that.as_ref()) + .ok() + .is_none_or(|that| *this != that) + } + (EqValue::Integer(this), DuperInner::Integer(that)) => this != that, + (EqValue::Float(this), DuperInner::Float(that)) => this != that, + (EqValue::Boolean(this), DuperInner::Boolean(that)) => this != that, + (EqValue::Null, DuperInner::Null) => false, + _ => true, + } + } +} + +enum CmpValue { + Len(usize), + TemporalInstant(Instant), + TemporalZonedDateTime(ZonedDateTime), + TemporalPlainDate(PlainDate), + TemporalPlainTime(PlainTime), + TemporalPlainDateTime(PlainDateTime), + TemporalPlainYearMonth(PlainYearMonth), + TemporalPlainMonthDay(PlainMonthDay), + TemporalDuration(Duration), + Integer(i64), + Float(f64), +} + +macro_rules! cmp_filter { + ( + $filter:ident, + $ord:pat + ) => { + struct $filter(CmpValue); + + impl Filter for $filter { + fn apply<'v>(&self, value: &'v DuperValue<'v>) -> bool { + match (&self.0, &value.inner) { + (CmpValue::Len(this), DuperInner::Object(that)) => { + matches!(this.cmp(&that.len()), $ord) + } + (CmpValue::Len(this), DuperInner::Array(that)) => { + matches!(this.cmp(&that.len()), $ord) + } + (CmpValue::Len(this), DuperInner::String(that)) => { + matches!(this.cmp(&that.as_ref().len()), $ord) + } + (CmpValue::Len(this), DuperInner::Bytes(that)) => { + matches!(this.cmp(&that.as_ref().len()), $ord) + } + (CmpValue::TemporalInstant(this), DuperInner::Temporal(that)) => { + Instant::from_str(that.as_ref()) + .is_ok_and(|that| matches!(this.cmp(&that), $ord)) + } + (CmpValue::TemporalZonedDateTime(this), DuperInner::Temporal(that)) => { + ZonedDateTime::from_utf8( + that.as_ref().as_bytes(), + Disambiguation::Compatible, + OffsetDisambiguation::Prefer, + ) + .is_ok_and(|that| matches!(this.compare_instant(&that), $ord)) + } + (CmpValue::TemporalPlainDate(this), DuperInner::Temporal(that)) => { + PlainDate::from_str(that.as_ref()) + .is_ok_and(|that| matches!(this.compare_iso(&that), $ord)) + } + (CmpValue::TemporalPlainTime(this), DuperInner::Temporal(that)) => { + PlainTime::from_str(that.as_ref()) + .is_ok_and(|that| matches!(this.cmp(&that), $ord)) + } + (CmpValue::TemporalPlainDateTime(this), DuperInner::Temporal(that)) => { + PlainDateTime::from_str(that.as_ref()) + .is_ok_and(|that| matches!(this.compare_iso(&that), $ord)) + } + (CmpValue::TemporalPlainYearMonth(this), DuperInner::Temporal(that)) => { + PlainYearMonth::from_str(that.as_ref()) + .is_ok_and(|that| matches!(this.compare_iso(&that), $ord)) + } + (CmpValue::TemporalDuration(this), DuperInner::Temporal(that)) => { + Duration::from_str(that.as_ref()) + .is_ok_and(|that| matches!(this.partial_cmp(&that), Some($ord))) + } + (CmpValue::Integer(this), DuperInner::Integer(that)) => { + matches!(this.cmp(that), $ord) + } + (CmpValue::Float(this), DuperInner::Float(that)) => { + matches!(this.partial_cmp(that), Some($ord)) + } + _ => false, + } + } + } + }; +} + +cmp_filter!(GeFilter, Ordering::Greater | Ordering::Equal); +cmp_filter!(GtFilter, Ordering::Greater); +cmp_filter!(LeFilter, Ordering::Less | Ordering::Equal); +cmp_filter!(LtFilter, Ordering::Less); + +enum IsFilter { + Object, + Array, + Tuple, + String, + Bytes, + TemporalInstant, + TemporalZonedDateTime, + TemporalPlainDate, + TemporalPlainTime, + TemporalPlainDateTime, + TemporalPlainYearMonth, + TemporalPlainMonthDay, + TemporalDuration, + TemporalUnspecified, + Integer, + Float, + Number, + Boolean, + Null, +} + +impl Filter for IsFilter { + fn apply<'v>(&self, value: &'v DuperValue<'v>) -> bool { + match (&self, &value.inner) { + (IsFilter::Object, DuperInner::Object(_)) => true, + (IsFilter::Array, DuperInner::Array(_)) => true, + (IsFilter::Tuple, DuperInner::Tuple(_)) => true, + (IsFilter::String, DuperInner::String(_)) => true, + (IsFilter::Bytes, DuperInner::Bytes(_)) => true, + (IsFilter::TemporalInstant, DuperInner::Temporal(that)) => { + Instant::from_str(that.as_ref()).is_ok() + } + (IsFilter::TemporalZonedDateTime, DuperInner::Temporal(that)) => { + ZonedDateTime::from_utf8( + that.as_ref().as_bytes(), + Disambiguation::Compatible, + OffsetDisambiguation::Prefer, + ) + .is_ok() + } + (IsFilter::TemporalPlainDate, DuperInner::Temporal(that)) => { + PlainDate::from_str(that.as_ref()).is_ok() + } + (IsFilter::TemporalPlainTime, DuperInner::Temporal(that)) => { + PlainTime::from_str(that.as_ref()).is_ok() + } + (IsFilter::TemporalPlainDateTime, DuperInner::Temporal(that)) => { + PlainDateTime::from_str(that.as_ref()).is_ok() + } + (IsFilter::TemporalPlainYearMonth, DuperInner::Temporal(that)) => { + PlainYearMonth::from_str(that.as_ref()).is_ok() + } + (IsFilter::TemporalPlainMonthDay, DuperInner::Temporal(that)) => { + PlainMonthDay::from_str(that.as_ref()).is_ok() + } + (IsFilter::TemporalDuration, DuperInner::Temporal(that)) => { + Duration::from_str(that.as_ref()).is_ok() + } + (IsFilter::TemporalUnspecified, DuperInner::Temporal(_)) => true, + (IsFilter::Integer, DuperInner::Integer(_)) => true, + (IsFilter::Float, DuperInner::Float(_)) => true, + (IsFilter::Number, DuperInner::Integer(_) | DuperInner::Float(_)) => true, + (IsFilter::Boolean, DuperInner::Boolean(_)) => true, + (IsFilter::Null, DuperInner::Null) => true, + _ => false, + } + } +} + +pub(crate) struct RegexFilter(regex::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 { + match &value.inner { + DuperInner::Bytes(bytes) => self.0.find(bytes.as_ref()).is_some(), + _ => false, + } + } +} + +pub(crate) struct FieldExistsFilter<'filter>(Cow<'filter, str>); + +impl<'filter> Filter for FieldExistsFilter<'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() + } else { + false + } + } +} diff --git a/duperq/src/lib.rs b/duperq/src/lib.rs new file mode 100644 index 0000000..d447b1a --- /dev/null +++ b/duperq/src/lib.rs @@ -0,0 +1,2 @@ +mod filter; +mod query; diff --git a/duperq/src/main.rs b/duperq/src/main.rs new file mode 100644 index 0000000..b52bbaf --- /dev/null +++ b/duperq/src/main.rs @@ -0,0 +1,15 @@ +use clap::Parser; + +#[derive(Parser)] +#[command(version, about, long_about = None)] +struct Cli { + /// Query to run. + query: String, + + /// Files to read from. If missing, defaults to stdin. + blob: Option, +} + +fn main() { + println!("Hello, world!"); +} diff --git a/duperq/src/query.rs b/duperq/src/query.rs new file mode 100644 index 0000000..ee43056 --- /dev/null +++ b/duperq/src/query.rs @@ -0,0 +1,26 @@ +// duperq 'span.tagged && span[0]name == sp0001 | "[${level}] ${span[0]time} - ${span[0]status} ${telemetry.duration:ms}"' +// duperq 'metadata.tags[created_at >= Instant(2025-11-22T00:00:00-03:00)]' + +use chumsky::prelude::*; + +use crate::filter::{AndFilter, Filter}; + +fn query<'a>() -> impl Parser<'a, &'a str, (AndFilter<'a>, Option<()>), extra::Err>> +{ + filter() + .padded() + .separated_by(just('|').padded()) + .collect::>() + .then(just('|').ignore_then(fmt().padded()).or_not()) +} + +fn filter<'a>() -> impl Parser<'a, &'a str, &'a dyn Filter, extra::Err>> { + todo!() +} + +fn fmt<'a>() -> impl Parser<'a, &'a str, (), extra::Err>> { + any() + .repeated() + .delimited_by(just('"'), just('"')) + .ignored() +} -- 2.51.2