From 86b2032a94d8e060867bea0856f29bebf0a29893 Mon Sep 17 00:00:00 2001 From: squidfunk Date: Mon, 23 Mar 2026 14:38:45 +0100 Subject: [PATCH 01/23] docs: clarify docs Signed-off-by: squidfunk --- crates/zrx-id/src/id/specificity/convert.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/zrx-id/src/id/specificity/convert.rs b/crates/zrx-id/src/id/specificity/convert.rs index d7da07b..1af885b 100644 --- a/crates/zrx-id/src/id/specificity/convert.rs +++ b/crates/zrx-id/src/id/specificity/convert.rs @@ -41,7 +41,7 @@ use super::Specificity; // Traits // ---------------------------------------------------------------------------- -/// Computes the [`Specificity`]. +/// Compute the [`Specificity`] of a value. pub trait ToSpecificity { /// Computes the specificity of the value. fn to_specificity(&self) -> Specificity; From e6124f5f41d4727cf183170ccdbdfbe7e0914c4b Mon Sep 17 00:00:00 2001 From: squidfunk Date: Mon, 23 Mar 2026 15:11:36 +0100 Subject: [PATCH 02/23] docs: harmonize docs and tests Signed-off-by: squidfunk --- crates/zrx-id/src/id/filter/builder.rs | 12 +++--- crates/zrx-id/src/id/filter/candidates.rs | 22 +++++----- crates/zrx-id/src/id/filter/condition.rs | 32 +++++++------- .../zrx-id/src/id/filter/condition/builder.rs | 42 +++++++++---------- crates/zrx-id/src/id/filter/expression.rs | 4 +- .../src/id/filter/expression/builder.rs | 8 ++-- 6 files changed, 60 insertions(+), 60 deletions(-) diff --git a/crates/zrx-id/src/id/filter/builder.rs b/crates/zrx-id/src/id/filter/builder.rs index 4a3c468..c926fa7 100644 --- a/crates/zrx-id/src/id/filter/builder.rs +++ b/crates/zrx-id/src/id/filter/builder.rs @@ -119,8 +119,8 @@ impl Builder { /// // Create filter builder and insert expression /// let mut builder = Filter::builder(); /// builder.insert(Expression::any(|expr| { - /// expr.with(selector!(location = "**/*.png")?)? - /// .with(selector!(location = "**/*.jpg")?) + /// expr.with(selector!(location = "**/*.jpg")?)? + /// .with(selector!(location = "**/*.png")?) /// })?); /// # Ok(()) /// # } @@ -146,8 +146,8 @@ impl Builder { /// // Create filter builder and insert expression /// let mut builder = Filter::builder(); /// builder.insert(Expression::any(|expr| { - /// expr.with(selector!(location = "**/*.png")?)? - /// .with(selector!(location = "**/*.jpg")?) + /// expr.with(selector!(location = "**/*.jpg")?)? + /// .with(selector!(location = "**/*.png")?) /// })?); /// /// // Remove expression @@ -178,8 +178,8 @@ impl Builder { /// // Create filter builder and insert expression /// let mut builder = Filter::builder(); /// builder.insert(Expression::any(|expr| { - /// expr.with(selector!(location = "**/*.png")?)? - /// .with(selector!(location = "**/*.jpg")?) + /// expr.with(selector!(location = "**/*.jpg")?)? + /// .with(selector!(location = "**/*.png")?) /// })?); /// /// // Create filter from builder diff --git a/crates/zrx-id/src/id/filter/candidates.rs b/crates/zrx-id/src/id/filter/candidates.rs index bee55d0..d7e10fd 100644 --- a/crates/zrx-id/src/id/filter/candidates.rs +++ b/crates/zrx-id/src/id/filter/candidates.rs @@ -200,8 +200,8 @@ mod tests { fn handles_any() -> Result { let mut builder = Filter::builder(); let _ = builder.insert(Expression::any(|expr| { - expr.with(selector!(location = "**/*.png")?)? - .with(selector!(location = "**/*.jpg")?) + expr.with(selector!(location = "**/*.jpg")?)? + .with(selector!(location = "**/*.png")?) })?); let filter = builder.build()?; for (id, check) in [ @@ -242,14 +242,14 @@ mod tests { fn handles_not() -> Result { let mut builder = Filter::builder(); let _ = builder.insert(Expression::not(|expr| { - expr.with(selector!(location = "**/*.png")?)? - .with(selector!(location = "**/*.jpg")?) + expr.with(selector!(location = "**/*.jpg")?)? + .with(selector!(location = "**/*.png")?) })?); let filter = builder.build()?; for (id, check) in [ ("zri:file:::docs:index.md:", vec![0]), - ("zri:file:::docs:image.png:", vec![]), ("zri:file:::docs:image.jpg:", vec![]), + ("zri:file:::docs:image.png:", vec![]), ] { assert_eq!( filter.candidates(&id)?.collect::>(), // fmt @@ -265,8 +265,8 @@ mod tests { let _ = builder.insert(Expression::all(|expr| { expr.with(selector!(provider = "file")?)? .with(Expression::any(|expr| { - expr.with(selector!(location = "**/*.png")?)? - .with(selector!(location = "**/*.jpg")?) + expr.with(selector!(location = "**/*.jpg")?)? + .with(selector!(location = "**/*.png")?) })) })?); let filter = builder.build()?; @@ -294,8 +294,8 @@ mod tests { .with(Expression::any(|expr| { expr.with(selector!(context = "docs")?)? // fmt .with(Expression::not(|expr| { - expr.with(selector!(location = "**/*.png")?)? - .with(selector!(location = "**/*.jpg")?) + expr.with(selector!(location = "**/*.jpg")?)? + .with(selector!(location = "**/*.png")?) }), ) })) @@ -303,11 +303,11 @@ mod tests { let filter = builder.build()?; for (id, check) in [ ("zri:file:::docs:index.md:", vec![0]), - ("zri:file:::docs:image.png:", vec![0]), ("zri:file:::docs:image.jpg:", vec![0]), + ("zri:file:::docs:image.png:", vec![0]), ("zri:file:::docs:image.gif:", vec![0]), - ("zri:git:::docs:image.png:", vec![]), ("zri:git:::docs:image.jpg:", vec![]), + ("zri:git:::docs:image.png:", vec![]), ] { assert_eq!( filter.candidates(&id)?.collect::>(), // fmt diff --git a/crates/zrx-id/src/id/filter/condition.rs b/crates/zrx-id/src/id/filter/condition.rs index 7dbcef6..ea93681 100644 --- a/crates/zrx-id/src/id/filter/condition.rs +++ b/crates/zrx-id/src/id/filter/condition.rs @@ -142,8 +142,8 @@ mod tests { #[test] fn handles_any() -> Result { let expr = Expression::any(|expr| { - expr.with(selector!(location = "**/*.png")?)? - .with(selector!(location = "**/*.jpg")?) + expr.with(selector!(location = "**/*.jpg")?)? + .with(selector!(location = "**/*.png")?) })?; let condition = Condition::builder(expr).build(); for (matches, check) in [ @@ -162,8 +162,8 @@ mod tests { #[test] fn handles_any_optimized() -> Result { let expr = Expression::any(|expr| { - expr.with(selector!(location = "**/*.png")?)? - .with(selector!(location = "**/*.jpg")?) + expr.with(selector!(location = "**/*.jpg")?)? + .with(selector!(location = "**/*.png")?) })?; let condition = Condition::builder(expr).optimize().build(); for (matches, check) in [ @@ -222,8 +222,8 @@ mod tests { #[test] fn handles_not() -> Result { let expr = Expression::not(|expr| { - expr.with(selector!(location = "**/*.png")?)? - .with(selector!(location = "**/*.jpg")?) + expr.with(selector!(location = "**/*.jpg")?)? + .with(selector!(location = "**/*.png")?) })?; let condition = Condition::builder(expr).build(); for (matches, check) in [ @@ -242,8 +242,8 @@ mod tests { #[test] fn handles_not_optimized() -> Result { let expr = Expression::not(|expr| { - expr.with(selector!(location = "**/*.png")?)? - .with(selector!(location = "**/*.jpg")?) + expr.with(selector!(location = "**/*.jpg")?)? + .with(selector!(location = "**/*.png")?) })?; let condition = Condition::builder(expr).optimize().build(); for (matches, check) in [ @@ -264,8 +264,8 @@ mod tests { let expr = Expression::all(|expr| { expr.with(selector!(provider = "file")?)? .with(Expression::any(|expr| { - expr.with(selector!(location = "**/*.png")?)? - .with(selector!(location = "**/*.jpg")?) + expr.with(selector!(location = "**/*.jpg")?)? + .with(selector!(location = "**/*.png")?) })) })?; let condition = Condition::builder(expr).build(); @@ -289,8 +289,8 @@ mod tests { let expr = Expression::all(|expr| { expr.with(selector!(provider = "file")?)? .with(Expression::any(|expr| { - expr.with(selector!(location = "**/*.png")?)? - .with(selector!(location = "**/*.jpg")?) + expr.with(selector!(location = "**/*.jpg")?)? + .with(selector!(location = "**/*.png")?) })) })?; let condition = Condition::builder(expr).optimize().build(); @@ -316,8 +316,8 @@ mod tests { .with(Expression::any(|expr| { expr.with(selector!(context = "docs")?)? // fmt .with(Expression::not(|expr| { - expr.with(selector!(location = "**/*.png")?)? - .with(selector!(location = "**/*.jpg")?) + expr.with(selector!(location = "**/*.jpg")?)? + .with(selector!(location = "**/*.png")?) }), ) })) @@ -347,8 +347,8 @@ mod tests { .with(Expression::any(|expr| { expr.with(selector!(context = "docs")?)? // fmt .with(Expression::not(|expr| { - expr.with(selector!(location = "**/*.png")?)? - .with(selector!(location = "**/*.jpg")?) + expr.with(selector!(location = "**/*.jpg")?)? + .with(selector!(location = "**/*.png")?) }), ) })) diff --git a/crates/zrx-id/src/id/filter/condition/builder.rs b/crates/zrx-id/src/id/filter/condition/builder.rs index 50fd388..be159c0 100644 --- a/crates/zrx-id/src/id/filter/condition/builder.rs +++ b/crates/zrx-id/src/id/filter/condition/builder.rs @@ -247,15 +247,15 @@ mod tests { #[test] fn handles_expression() -> Result { let expr = Expression::any(|expr| { - expr.with(selector!(location = "**/*.png")?)? - .with(selector!(location = "**/*.jpg")?) + expr.with(selector!(location = "**/*.jpg")?)? + .with(selector!(location = "**/*.png")?) })?; let builder = Condition::builder(expr); assert_eq!( builder.terms, [ - Term::from(selector!(location = "**/*.png")?), Term::from(selector!(location = "**/*.jpg")?), + Term::from(selector!(location = "**/*.png")?), ] ); match builder.group { @@ -312,8 +312,8 @@ mod tests { #[test] fn handles_any() -> Result { let expr = Expression::any(|expr| { - expr.with(selector!(location = "**/*.png")?)? - .with(selector!(location = "**/*.jpg")?) + expr.with(selector!(location = "**/*.jpg")?)? + .with(selector!(location = "**/*.png")?) })?; let builder = Condition::builder(expr).optimize(); assert_eq!( @@ -327,8 +327,8 @@ mod tests { fn handles_any_any() -> Result { let expr = Expression::any(|expr| { expr.with(Expression::any(|expr| { - expr.with(selector!(location = "**/*.png")?)? - .with(selector!(location = "**/*.jpg")?) + expr.with(selector!(location = "**/*.jpg")?)? + .with(selector!(location = "**/*.png")?) })?) })?; let builder = Condition::builder(expr).optimize(); @@ -342,9 +342,9 @@ mod tests { #[test] fn handles_any_any_mixed() -> Result { let expr = Expression::any(|expr| { - expr.with(selector!(location = "**/*.png")?)? // fmt + expr.with(selector!(location = "**/*.jpg")?)? // fmt .with(Expression::any(|expr| { - expr.with(selector!(location = "**/*.jpg")?) + expr.with(selector!(location = "**/*.png")?) })?) })?; let builder = Condition::builder(expr).optimize(); @@ -359,8 +359,8 @@ mod tests { fn handles_all_all() -> Result { let expr = Expression::all(|expr| { expr.with(Expression::all(|expr| { - expr.with(selector!(location = "**/*.png")?)? - .with(selector!(location = "**/*.jpg")?) + expr.with(selector!(location = "**/*.jpg")?)? + .with(selector!(location = "**/*.png")?) })?) })?; let builder = Condition::builder(expr).optimize(); @@ -377,9 +377,9 @@ mod tests { #[test] fn handles_all_all_mixed() -> Result { let expr = Expression::all(|expr| { - expr.with(selector!(location = "**/*.png")?)? // fmt + expr.with(selector!(location = "**/*.jpg")?)? // fmt .with(Expression::all(|expr| { - expr.with(selector!(location = "**/*.jpg")?) + expr.with(selector!(location = "**/*.png")?) })?) })?; let builder = Condition::builder(expr).optimize(); @@ -397,8 +397,8 @@ mod tests { fn handles_not_not() -> Result { let expr = Expression::not(|expr| { expr.with(Expression::not(|expr| { - expr.with(selector!(location = "**/*.png")?)? - .with(selector!(location = "**/*.jpg")?) + expr.with(selector!(location = "**/*.jpg")?)? + .with(selector!(location = "**/*.png")?) })?) })?; let builder = Condition::builder(expr).optimize(); @@ -418,9 +418,9 @@ mod tests { #[test] fn handles_not_not_mixed() -> Result { let expr = Expression::not(|expr| { - expr.with(selector!(location = "**/*.png")?)? // fmt + expr.with(selector!(location = "**/*.jpg")?)? // fmt .with(Expression::not(|expr| { - expr.with(selector!(location = "**/*.jpg")?) + expr.with(selector!(location = "**/*.png")?) })?) })?; let builder = Condition::builder(expr).optimize(); @@ -444,8 +444,8 @@ mod tests { fn handles_all_any() -> Result { let expr = Expression::all(|expr| { expr.with(Expression::any(|expr| { - expr.with(selector!(location = "**/*.png")?)? - .with(selector!(location = "**/*.jpg")?) + expr.with(selector!(location = "**/*.jpg")?)? + .with(selector!(location = "**/*.png")?) })) })?; let builder = Condition::builder(expr).optimize(); @@ -467,8 +467,8 @@ mod tests { let expr = Expression::all(|expr| { expr.with(selector!(provider = "file")?)? .with(Expression::any(|expr| { - expr.with(selector!(location = "**/*.png")?)? - .with(selector!(location = "**/*.jpg")?) + expr.with(selector!(location = "**/*.jpg")?)? + .with(selector!(location = "**/*.png")?) })) })?; let builder = Condition::builder(expr).optimize(); diff --git a/crates/zrx-id/src/id/filter/expression.rs b/crates/zrx-id/src/id/filter/expression.rs index b05626c..1bfe590 100644 --- a/crates/zrx-id/src/id/filter/expression.rs +++ b/crates/zrx-id/src/id/filter/expression.rs @@ -134,8 +134,8 @@ impl IntoIterator for Expression { /// /// // Create expression /// let expr = Expression::any(|expr| { - /// expr.with(selector!(location = "**/*.png")?)? - /// .with(selector!(location = "**/*.jpg")?) + /// expr.with(selector!(location = "**/*.jpg")?)? + /// .with(selector!(location = "**/*.png")?) /// })?; /// /// // Create iterator over expression diff --git a/crates/zrx-id/src/id/filter/expression/builder.rs b/crates/zrx-id/src/id/filter/expression/builder.rs index d847f22..a7cd2a1 100644 --- a/crates/zrx-id/src/id/filter/expression/builder.rs +++ b/crates/zrx-id/src/id/filter/expression/builder.rs @@ -64,8 +64,8 @@ impl Expression { /// /// // Create expression /// let expr = Expression::any(|expr| { - /// expr.with(selector!(location = "**/*.png")?)? - /// .with(selector!(location = "**/*.jpg")?) + /// expr.with(selector!(location = "**/*.jpg")?)? + /// .with(selector!(location = "**/*.png")?) /// })?; /// # Ok(()) /// # } @@ -176,8 +176,8 @@ impl Builder { /// /// // Create expression /// let expr = Expression::any(|expr| { - /// expr.with(selector!(location = "**/*.png")?)? - /// .with(selector!(location = "**/*.jpg")?) + /// expr.with(selector!(location = "**/*.jpg")?)? + /// .with(selector!(location = "**/*.png")?) /// })?; /// # Ok(()) /// # } From 971dfe987e006a0ef3f44dcb5ff9017bb480dcc4 Mon Sep 17 00:00:00 2001 From: squidfunk Date: Mon, 23 Mar 2026 15:57:25 +0100 Subject: [PATCH 03/23] docs: add examples Signed-off-by: squidfunk --- .../zrx-id/src/id/matcher/selector/builder.rs | 4 +- crates/zrx-id/src/id/specificity.rs | 42 +++++++- crates/zrx-id/src/id/specificity/convert.rs | 102 +++++++++++++++++- .../src/id/specificity/segment/convert.rs | 10 ++ .../src/id/specificity/segment/segments.rs | 15 +++ crates/zrx-id/src/id/specificity/tokens.rs | 2 +- 6 files changed, 168 insertions(+), 7 deletions(-) diff --git a/crates/zrx-id/src/id/matcher/selector/builder.rs b/crates/zrx-id/src/id/matcher/selector/builder.rs index 226d773..c6a0dd9 100644 --- a/crates/zrx-id/src/id/matcher/selector/builder.rs +++ b/crates/zrx-id/src/id/matcher/selector/builder.rs @@ -83,11 +83,11 @@ impl Selector { /// let selector: Selector = "zrs:::::**/*.md:".parse()?; /// /// // Create selector builder - /// let builder = selector.to_builder().location("**/index.md"); + /// let builder = selector.to_builder().location("index.md"); /// /// // Create selector from builder /// let selector = builder.build()?; - /// assert_eq!(selector.as_str(), "zrs:::::**/index.md:"); + /// assert_eq!(selector.as_str(), "zrs:::::index.md:"); /// # Ok(()) /// # } /// ``` diff --git a/crates/zrx-id/src/id/specificity.rs b/crates/zrx-id/src/id/specificity.rs index 1fbf85c..11f40c6 100644 --- a/crates/zrx-id/src/id/specificity.rs +++ b/crates/zrx-id/src/id/specificity.rs @@ -84,10 +84,34 @@ where } } +impl From<(u16, u16, u16, u16)> for Specificity { + /// Creates a specificity from a tuple. + #[inline] + fn from((a, b, c, l): (u16, u16, u16, u16)) -> Self { + Self(a, b, c, l) + } +} + // ---------------------------------------------------------------------------- impl PartialOrd for Specificity { /// Orders two specificities. + /// + /// # Examples + /// + /// ``` + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// use zrx_id::specificity::convert::ToSpecificity; + /// use zrx_id::selector; + /// + /// // Create selector and compute specificity + /// let a = selector!(location = "**/*.md")?; + /// let b = selector!(location = "*.md")?; + /// assert!(a.to_specificity() < b.to_specificity()); + /// # Ok(()) + /// # } + /// ``` #[inline] fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) @@ -96,13 +120,29 @@ impl PartialOrd for Specificity { impl Ord for Specificity { /// Orders two specificities. + /// + /// # Examples + /// + /// ``` + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// use zrx_id::specificity::convert::ToSpecificity; + /// use zrx_id::selector; + /// + /// // Create selector and compute specificity + /// let a = selector!(location = "**/*.md")?; + /// let b = selector!(location = "*.md")?; + /// assert!(a.to_specificity() < b.to_specificity()); + /// # Ok(()) + /// # } + /// ``` #[inline] fn cmp(&self, other: &Self) -> Ordering { let Specificity(a1, b1, c1, l1) = self; let Specificity(a2, b2, c2, l2) = other; a1.cmp(a2) .then(b1.cmp(b2)) - .then(c2.cmp(c1)) // reversed: fewer ** = more specific + .then(c2.cmp(c1)) // reversed, fewer ** = more specific .then(l1.cmp(l2)) } } diff --git a/crates/zrx-id/src/id/specificity/convert.rs b/crates/zrx-id/src/id/specificity/convert.rs index 1af885b..9b0939d 100644 --- a/crates/zrx-id/src/id/specificity/convert.rs +++ b/crates/zrx-id/src/id/specificity/convert.rs @@ -23,7 +23,7 @@ // ---------------------------------------------------------------------------- -//! Specificity conversion. +//! Specificity computation. use std::cmp; @@ -41,7 +41,7 @@ use super::Specificity; // Traits // ---------------------------------------------------------------------------- -/// Compute the [`Specificity`] of a value. +/// Computation of [`Specificity`]. pub trait ToSpecificity { /// Computes the specificity of the value. fn to_specificity(&self) -> Specificity; @@ -53,6 +53,28 @@ pub trait ToSpecificity { impl ToSpecificity for Expression { /// Computes the specificity of the expression. + /// + /// # Examples + /// + /// ``` + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// use zrx_id::filter::Expression; + /// use zrx_id::specificity::convert::ToSpecificity; + /// use zrx_id::selector; + /// + /// // Create expression and compute specificity + /// let expr = Expression::any(|expr| { + /// expr.with(selector!(location = "**/*.jpg")?)? + /// .with(selector!(location = "**/*.png")?) + /// })?; + /// assert_eq!( + /// expr.to_specificity(), + /// (0, 1, 1, 4).into() + /// ); + /// # Ok(()) + /// # } + /// ``` #[inline] fn to_specificity(&self) -> Specificity { let iter = self.operands().iter().map(ToSpecificity::to_specificity); @@ -66,6 +88,25 @@ impl ToSpecificity for Expression { impl ToSpecificity for Term { /// Computes the specificity of the term. + /// + /// # Examples + /// + /// ``` + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// use zrx_id::filter::expression::Term; + /// use zrx_id::specificity::convert::ToSpecificity; + /// use zrx_id::selector; + /// + /// // Create term and compute specificity + /// let term = Term::from(selector!(location = "**/*.{jpg,png}")?); + /// assert_eq!( + /// term.to_specificity(), + /// (0, 1, 1, 4).into() + /// ); + /// # Ok(()) + /// # } + /// ``` #[inline] fn to_specificity(&self) -> Specificity { match self { @@ -77,6 +118,25 @@ impl ToSpecificity for Term { impl ToSpecificity for Operand { /// Computes the specificity of the operand. + /// + /// # Examples + /// + /// ``` + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// use zrx_id::filter::expression::Operand; + /// use zrx_id::specificity::convert::ToSpecificity; + /// use zrx_id::selector; + /// + /// // Create operand and compute specificity + /// let operand = Operand::from(selector!(location = "**/*.{jpg,png}")?); + /// assert_eq!( + /// operand.to_specificity(), + /// (0, 1, 1, 4).into() + /// ); + /// # Ok(()) + /// # } + /// ``` #[inline] fn to_specificity(&self) -> Specificity { match self { @@ -90,6 +150,24 @@ impl ToSpecificity for Operand { impl ToSpecificity for Id { /// Computes the specificity of the identifier. + /// + /// # Examples + /// + /// ``` + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// use zrx_id::specificity::convert::ToSpecificity; + /// use zrx_id::id; + /// + /// // Create identifier and compute specificity + /// let id = id!(provider = "file", context = ".", location = "index.md")?; + /// assert_eq!( + /// id.to_specificity(), + /// (3, 0, 0, 13).into() + /// ); + /// # Ok(()) + /// # } + /// ``` #[inline] fn to_specificity(&self) -> Specificity { let iter = 1..7; @@ -101,6 +179,24 @@ impl ToSpecificity for Id { impl ToSpecificity for Selector { /// Computes the specificity of the selector. + /// + /// # Examples + /// + /// ``` + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// use zrx_id::specificity::convert::ToSpecificity; + /// use zrx_id::selector; + /// + /// // Create selector and compute specificity + /// let selector = selector!(location = "**/*.{jpg,png}")?; + /// assert_eq!( + /// selector.to_specificity(), + /// (0, 1, 1, 4).into() + /// ); + /// # Ok(()) + /// # } + /// ``` #[inline] fn to_specificity(&self) -> Specificity { let iter = 1..7; @@ -169,7 +265,7 @@ impl ToSpecificity for Wildcard { } impl ToSpecificity for Character<'_> { - /// Computes the specificity of the character. + /// Computes the specificity of the character class. #[inline] fn to_specificity(&self) -> Specificity { Specificity(0, 1, 0, 1) diff --git a/crates/zrx-id/src/id/specificity/segment/convert.rs b/crates/zrx-id/src/id/specificity/segment/convert.rs index b196cbc..4c82e98 100644 --- a/crates/zrx-id/src/id/specificity/segment/convert.rs +++ b/crates/zrx-id/src/id/specificity/segment/convert.rs @@ -52,6 +52,16 @@ where T: AsTokens, { /// Converts tokens to a segments set. + /// + /// # Examples + /// + /// ``` + /// use zrx_id::id::specificity::convert::ToSegments; + /// + /// // Create segment set from string + /// let segments = "**/*.md".to_segments(); + /// assert_eq!(segments.len(), 2); + /// ``` #[inline] fn to_segments(&self) -> Segments<'_> { parse(&mut self.as_tokens().peekable(), false) diff --git a/crates/zrx-id/src/id/specificity/segment/segments.rs b/crates/zrx-id/src/id/specificity/segment/segments.rs index db0fbfb..729ad6a 100644 --- a/crates/zrx-id/src/id/specificity/segment/segments.rs +++ b/crates/zrx-id/src/id/specificity/segment/segments.rs @@ -58,6 +58,21 @@ impl Segments<'_> { } } +#[allow(clippy::must_use_candidate)] +impl Segments<'_> { + /// Returns the number of segments. + #[inline] + pub fn len(&self) -> usize { + self.inner.len() + } + + /// Returns whether there are any segments. + #[inline] + pub fn is_empty(&self) -> bool { + self.inner.is_empty() + } +} + // ---------------------------------------------------------------------------- // Trait implementations // ---------------------------------------------------------------------------- diff --git a/crates/zrx-id/src/id/specificity/tokens.rs b/crates/zrx-id/src/id/specificity/tokens.rs index 0c744a3..7fda6b4 100644 --- a/crates/zrx-id/src/id/specificity/tokens.rs +++ b/crates/zrx-id/src/id/specificity/tokens.rs @@ -152,7 +152,7 @@ impl<'a> Iterator for Tokens<'a> { b',' => Some(Token::Comma), b'/' => Some(Token::Separator), - // Consume a `*` or `**` + // Consume `*` or `**` b'*' => { if self.index < value.len() && value[self.index] == b'*' { self.index += 1; From d8cf0675c4bb2e0cbb510287091a9500c66bc452 Mon Sep 17 00:00:00 2001 From: squidfunk Date: Mon, 23 Mar 2026 19:11:46 +0100 Subject: [PATCH 04/23] chore: fix doctests Signed-off-by: squidfunk --- crates/zrx-id/src/id/matcher/component/builder.rs | 1 - crates/zrx-id/src/id/specificity.rs | 2 +- crates/zrx-id/src/id/specificity/segment/convert.rs | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/zrx-id/src/id/matcher/component/builder.rs b/crates/zrx-id/src/id/matcher/component/builder.rs index 9f8bcb9..4a09128 100644 --- a/crates/zrx-id/src/id/matcher/component/builder.rs +++ b/crates/zrx-id/src/id/matcher/component/builder.rs @@ -84,7 +84,6 @@ impl Builder { /// Returns [`Error::Glob`][] if the [`GlobSet`][] can't be built. /// /// [`Error::Glob`]: crate::id::matcher::error::Error::Glob - /// /// [`GlobSet`]: globset::GlobSet pub fn build(self) -> Result { Ok(Component { diff --git a/crates/zrx-id/src/id/specificity.rs b/crates/zrx-id/src/id/specificity.rs index 11f40c6..08affdf 100644 --- a/crates/zrx-id/src/id/specificity.rs +++ b/crates/zrx-id/src/id/specificity.rs @@ -60,7 +60,7 @@ impl Specificity { cmp::min(self, other) } - /// Computes the minimum of both specificities while summing their lengths. + /// Computes the minimum of both specificities, summing their lengths. #[inline] fn min_sum_len(self, other: Self) -> Self { let mut spec = cmp::min(self, other); diff --git a/crates/zrx-id/src/id/specificity/segment/convert.rs b/crates/zrx-id/src/id/specificity/segment/convert.rs index 4c82e98..593e528 100644 --- a/crates/zrx-id/src/id/specificity/segment/convert.rs +++ b/crates/zrx-id/src/id/specificity/segment/convert.rs @@ -56,7 +56,7 @@ where /// # Examples /// /// ``` - /// use zrx_id::id::specificity::convert::ToSegments; + /// use zrx_id::specificity::segment::convert::ToSegments; /// /// // Create segment set from string /// let segments = "**/*.md".to_segments(); From d2ac737ecdaee95b998c3a793f1b47ccc011ac1a Mon Sep 17 00:00:00 2001 From: squidfunk Date: Mon, 23 Mar 2026 19:22:17 +0100 Subject: [PATCH 05/23] refactor: move `convert` exports to containing modules Signed-off-by: squidfunk --- crates/zrx-id/src/id/specificity.rs | 8 ++++---- crates/zrx-id/src/id/specificity/convert.rs | 13 ++++++------- crates/zrx-id/src/id/specificity/segment.rs | 3 ++- crates/zrx-id/src/id/specificity/segment/convert.rs | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/crates/zrx-id/src/id/specificity.rs b/crates/zrx-id/src/id/specificity.rs index 08affdf..dd023ce 100644 --- a/crates/zrx-id/src/id/specificity.rs +++ b/crates/zrx-id/src/id/specificity.rs @@ -27,11 +27,11 @@ use std::cmp::{self, Ordering}; -pub mod convert; +mod convert; pub mod segment; mod tokens; -use convert::ToSpecificity; +pub use convert::ToSpecificity; // ---------------------------------------------------------------------------- // Structs @@ -102,7 +102,7 @@ impl PartialOrd for Specificity { /// ``` /// # use std::error::Error; /// # fn main() -> Result<(), Box> { - /// use zrx_id::specificity::convert::ToSpecificity; + /// use zrx_id::specificity::ToSpecificity; /// use zrx_id::selector; /// /// // Create selector and compute specificity @@ -126,7 +126,7 @@ impl Ord for Specificity { /// ``` /// # use std::error::Error; /// # fn main() -> Result<(), Box> { - /// use zrx_id::specificity::convert::ToSpecificity; + /// use zrx_id::specificity::ToSpecificity; /// use zrx_id::selector; /// /// // Create selector and compute specificity diff --git a/crates/zrx-id/src/id/specificity/convert.rs b/crates/zrx-id/src/id/specificity/convert.rs index 9b0939d..9b39056 100644 --- a/crates/zrx-id/src/id/specificity/convert.rs +++ b/crates/zrx-id/src/id/specificity/convert.rs @@ -33,8 +33,7 @@ use crate::id::matcher::selector::Selector; use crate::id::Id; use super::segment::atom::{Character, Wildcard}; -use super::segment::convert::ToSegments; -use super::segment::{Atom, Segment, Segments}; +use super::segment::{Atom, Segment, Segments, ToSegments}; use super::Specificity; // ---------------------------------------------------------------------------- @@ -60,8 +59,8 @@ impl ToSpecificity for Expression { /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// use zrx_id::filter::Expression; - /// use zrx_id::specificity::convert::ToSpecificity; /// use zrx_id::selector; + /// use zrx_id::specificity::ToSpecificity; /// /// // Create expression and compute specificity /// let expr = Expression::any(|expr| { @@ -95,8 +94,8 @@ impl ToSpecificity for Term { /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// use zrx_id::filter::expression::Term; - /// use zrx_id::specificity::convert::ToSpecificity; /// use zrx_id::selector; + /// use zrx_id::specificity::ToSpecificity; /// /// // Create term and compute specificity /// let term = Term::from(selector!(location = "**/*.{jpg,png}")?); @@ -125,8 +124,8 @@ impl ToSpecificity for Operand { /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// use zrx_id::filter::expression::Operand; - /// use zrx_id::specificity::convert::ToSpecificity; /// use zrx_id::selector; + /// use zrx_id::specificity::ToSpecificity; /// /// // Create operand and compute specificity /// let operand = Operand::from(selector!(location = "**/*.{jpg,png}")?); @@ -156,8 +155,8 @@ impl ToSpecificity for Id { /// ``` /// # use std::error::Error; /// # fn main() -> Result<(), Box> { - /// use zrx_id::specificity::convert::ToSpecificity; /// use zrx_id::id; + /// use zrx_id::specificity::ToSpecificity; /// /// // Create identifier and compute specificity /// let id = id!(provider = "file", context = ".", location = "index.md")?; @@ -185,8 +184,8 @@ impl ToSpecificity for Selector { /// ``` /// # use std::error::Error; /// # fn main() -> Result<(), Box> { - /// use zrx_id::specificity::convert::ToSpecificity; /// use zrx_id::selector; + /// use zrx_id::specificity::ToSpecificity; /// /// // Create selector and compute specificity /// let selector = selector!(location = "**/*.{jpg,png}")?; diff --git a/crates/zrx-id/src/id/specificity/segment.rs b/crates/zrx-id/src/id/specificity/segment.rs index da852b6..0cba4ce 100644 --- a/crates/zrx-id/src/id/specificity/segment.rs +++ b/crates/zrx-id/src/id/specificity/segment.rs @@ -29,10 +29,11 @@ use std::fmt::{self, Display}; use std::slice::Iter; pub mod atom; -pub mod convert; +mod convert; mod segments; pub use atom::Atom; +pub use convert::ToSegments; pub use segments::Segments; // ---------------------------------------------------------------------------- diff --git a/crates/zrx-id/src/id/specificity/segment/convert.rs b/crates/zrx-id/src/id/specificity/segment/convert.rs index 593e528..28206aa 100644 --- a/crates/zrx-id/src/id/specificity/segment/convert.rs +++ b/crates/zrx-id/src/id/specificity/segment/convert.rs @@ -56,7 +56,7 @@ where /// # Examples /// /// ``` - /// use zrx_id::specificity::segment::convert::ToSegments; + /// use zrx_id::specificity::segment::ToSegments; /// /// // Create segment set from string /// let segments = "**/*.md".to_segments(); From 6c02f57703d61091a843bb0920f8ad30c203e972 Mon Sep 17 00:00:00 2001 From: squidfunk Date: Mon, 23 Mar 2026 19:29:47 +0100 Subject: [PATCH 06/23] style: re-organize exports Signed-off-by: squidfunk --- crates/zrx-id/src/lib.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/zrx-id/src/lib.rs b/crates/zrx-id/src/lib.rs index 53b394c..8eef91c 100644 --- a/crates/zrx-id/src/lib.rs +++ b/crates/zrx-id/src/lib.rs @@ -29,8 +29,7 @@ mod id; -pub use id::filter::expression::Expression; -pub use id::filter::{self, Filter}; +pub use id::filter::{self, Expression, Filter}; pub use id::format; pub use id::matcher::selector::{Selector, TryToSelector}; pub use id::matcher::{self, Matcher, Matches}; From f17629fba869efbafeec01677290db33f1da83a3 Mon Sep 17 00:00:00 2001 From: squidfunk Date: Mon, 23 Mar 2026 19:45:07 +0100 Subject: [PATCH 07/23] docs: add doctests Signed-off-by: squidfunk --- crates/zrx-id/src/id/filter/candidates.rs | 6 ++-- crates/zrx-id/src/id/filter/terms.rs | 25 +++++++++++++++++ crates/zrx-id/src/id/matcher/selector.rs | 2 +- crates/zrx-id/src/id/specificity/convert.rs | 2 +- .../src/id/specificity/segment/convert.rs | 4 +-- .../src/id/specificity/segment/segments.rs | 28 +++++++++++++++++++ 6 files changed, 60 insertions(+), 7 deletions(-) diff --git a/crates/zrx-id/src/id/filter/candidates.rs b/crates/zrx-id/src/id/filter/candidates.rs index d7e10fd..b239904 100644 --- a/crates/zrx-id/src/id/filter/candidates.rs +++ b/crates/zrx-id/src/id/filter/candidates.rs @@ -205,8 +205,8 @@ mod tests { })?); let filter = builder.build()?; for (id, check) in [ - ("zri:file:::docs:image.png:", vec![0]), ("zri:file:::docs:image.jpg:", vec![0]), + ("zri:file:::docs:image.png:", vec![0]), ("zri:file:::docs:image.gif:", vec![]), ] { assert_eq!( @@ -272,11 +272,11 @@ mod tests { let filter = builder.build()?; for (id, check) in [ ("zri:file:::docs:index.md:", vec![]), - ("zri:file:::docs:image.png:", vec![0]), ("zri:file:::docs:image.jpg:", vec![0]), + ("zri:file:::docs:image.png:", vec![0]), ("zri:file:::docs:image.gif:", vec![]), - ("zri:git:::docs:image.png:", vec![]), ("zri:git:::docs:image.jpg:", vec![]), + ("zri:git:::docs:image.png:", vec![]), ] { assert_eq!( filter.candidates(&id)?.collect::>(), // fmt diff --git a/crates/zrx-id/src/id/filter/terms.rs b/crates/zrx-id/src/id/filter/terms.rs index aa7b9c8..c053d44 100644 --- a/crates/zrx-id/src/id/filter/terms.rs +++ b/crates/zrx-id/src/id/filter/terms.rs @@ -49,6 +49,31 @@ pub struct Terms<'a> { impl Filter { /// Creates an iterator over the terms. + /// + /// # Examples + /// + /// ``` + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// use zrx_id::{selector, Expression, Filter}; + /// + /// // Create filter builder and insert expression + /// let mut builder = Filter::builder(); + /// builder.insert(Expression::any(|expr| { + /// expr.with(selector!(location = "**/*.jpg")?)? + /// .with(selector!(location = "**/*.png")?) + /// })?); + /// + /// // Create filter from builder + /// let filter = builder.build()?; + /// + /// // Create iterator over terms + /// for term in filter.terms() { + /// println!("{term:?}"); + /// } + /// # Ok(()) + /// # } + /// ``` #[inline] #[must_use] pub fn terms(&self) -> Terms<'_> { diff --git a/crates/zrx-id/src/id/matcher/selector.rs b/crates/zrx-id/src/id/matcher/selector.rs index 840fc08..53a93bc 100644 --- a/crates/zrx-id/src/id/matcher/selector.rs +++ b/crates/zrx-id/src/id/matcher/selector.rs @@ -52,7 +52,7 @@ pub use convert::TryToSelector; /// /// Selectors are used to match identifiers. Like identifiers, they consist of /// six components, which can be set to specific values or left empty to act as -/// wildcards. Each components can be set to a glob as supported by [`globset`], +/// wildcards. Each component can be set to a glob as supported by [`globset`], /// which allows for powerful matching capabilities. /// /// Selectors are no means to an end, but rather a building block to associate diff --git a/crates/zrx-id/src/id/specificity/convert.rs b/crates/zrx-id/src/id/specificity/convert.rs index 9b39056..65ff725 100644 --- a/crates/zrx-id/src/id/specificity/convert.rs +++ b/crates/zrx-id/src/id/specificity/convert.rs @@ -208,7 +208,7 @@ impl ToSpecificity for Selector { // ---------------------------------------------------------------------------- impl ToSpecificity for Segments<'_> { - /// Computes the specificity of the segments set. + /// Computes the specificity of the segment set. #[inline] fn to_specificity(&self) -> Specificity { self.iter() diff --git a/crates/zrx-id/src/id/specificity/segment/convert.rs b/crates/zrx-id/src/id/specificity/segment/convert.rs index 28206aa..1ecb5ff 100644 --- a/crates/zrx-id/src/id/specificity/segment/convert.rs +++ b/crates/zrx-id/src/id/specificity/segment/convert.rs @@ -39,7 +39,7 @@ use super::Segment; /// Conversion to [`Segments`]. pub trait ToSegments { - /// Converts to a segments set. + /// Converts to a segment set. fn to_segments(&self) -> Segments<'_>; } @@ -51,7 +51,7 @@ impl ToSegments for T where T: AsTokens, { - /// Converts tokens to a segments set. + /// Converts tokens to a segment set. /// /// # Examples /// diff --git a/crates/zrx-id/src/id/specificity/segment/segments.rs b/crates/zrx-id/src/id/specificity/segment/segments.rs index 729ad6a..9e60ea2 100644 --- a/crates/zrx-id/src/id/specificity/segment/segments.rs +++ b/crates/zrx-id/src/id/specificity/segment/segments.rs @@ -52,6 +52,20 @@ pub struct Segments<'a> { impl Segments<'_> { /// Creates an iterator over the segment set. + /// + /// # Examples + /// + /// ``` + /// use zrx_id::specificity::segment::ToSegments; + /// + /// // Create segment set from string + /// let segments = "**/*.md".to_segments(); + /// + /// // Create iterator over segment set + /// for segment in segments.iter() { + /// println!("{segment:?}"); + /// } + /// ``` #[inline] pub fn iter(&self) -> Iter<'_, Segment<'_>> { self.inner.iter() @@ -95,6 +109,20 @@ impl<'a> IntoIterator for &'a Segments<'a> { type IntoIter = Iter<'a, Segment<'a>>; /// Creates an iterator over the segment set. + /// + /// # Examples + /// + /// ``` + /// use zrx_id::specificity::segment::ToSegments; + /// + /// // Create segment set from string + /// let segments = "**/*.md".to_segments(); + /// + /// // Create iterator over segment set + /// for segment in &segments { + /// println!("{segment:?}"); + /// } + /// ``` #[inline] fn into_iter(self) -> Self::IntoIter { self.iter() From f91316c96a156f1bf412cfbe3567747a5f9018b2 Mon Sep 17 00:00:00 2001 From: squidfunk Date: Mon, 23 Mar 2026 21:21:52 +0100 Subject: [PATCH 08/23] docs: add doc comment to `Specificity` Signed-off-by: squidfunk --- crates/zrx-id/src/id/matcher/builder.rs | 1 - crates/zrx-id/src/id/specificity.rs | 88 +++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/crates/zrx-id/src/id/matcher/builder.rs b/crates/zrx-id/src/id/matcher/builder.rs index 1821223..934c59e 100644 --- a/crates/zrx-id/src/id/matcher/builder.rs +++ b/crates/zrx-id/src/id/matcher/builder.rs @@ -168,7 +168,6 @@ impl Builder { /// Returns [`Error::Glob`][] if a component's [`GlobSet`][] can't be built. /// /// [`Error::Glob`]: crate::id::matcher::error::Error::Glob - /// /// [`GlobSet`]: globset::GlobSet /// /// # Examples diff --git a/crates/zrx-id/src/id/specificity.rs b/crates/zrx-id/src/id/specificity.rs index dd023ce..a0ad3eb 100644 --- a/crates/zrx-id/src/id/specificity.rs +++ b/crates/zrx-id/src/id/specificity.rs @@ -38,6 +38,94 @@ pub use convert::ToSpecificity; // ---------------------------------------------------------------------------- /// Specificity. +/// +/// Specificity is an ordering and tie-breaking concept borrowed from CSS, where +/// more specific selectors take precedence over less specific ones. Specificity +/// is computable for the likes of [`Expression`][], [`Term`][], [`Operand`][], +/// [`Id`][], [`Selector`][], and [`Glob`][]. +/// +/// # Representation +/// +/// Specificity is represented as a 4-tuple `(a, b, c, l)`, which is compared +/// in lexicographic order, meaning that components are compared in sequence: +/// +/// - `a` – number of segments with literals only, e.g. `src`, `main.rs`. +/// - `b` – number of segments with single-wildcards, e.g. `*`, `?`, `[abc]`. +/// - `c` – number of segments with double-wildcards, compared in reverse. +/// - `l` – number of literals across all segments. +/// +/// # Atoms +/// +/// In a [`Segment`][], atoms are combined with [`Specificity::min_sum_len`] - +/// the structural component `a`, `b`, or `c` is assigned by taking the minimum +/// across all atoms in the segment, whereas the length component `l` receives +/// the sum across all atoms in the segment. +/// +/// # Ids and selectors +/// +/// The specificity of an [`Id`][] or [`Selector`][] is computed by summing the +/// specificities of its components, where the specificity of each component is +/// computed individually and then combined with [`Specificity::sum`][]. Empty +/// components receive the [`Specificity::default`], which is `(0, 0, 0, 0)`. +/// +/// ``` sh +/// zrs:{git,file}:::{docs}:index.md: # (3, 0, 0, 15) +/// zrs::::docs:{index,about}.md: # (2, 0, 0, 12) +/// zrs:::::index.{md,rst}: # (1, 0, 0, 8) +/// zrs:::::{*}: # (0, 1, 0, 0) +/// ``` +/// +/// # Expressions +/// +/// An [`Expression`][] is a combination of multiple [`Id`][] and [`Selector`][] +/// terms, with its specificity computed according to its [`Operator`][]: +/// +/// - [`Expression::any`][]: takes the minimum. The expression is as specific +/// as its least specific operand, since any operand can match. +/// +/// - [`Expression::all`][]: sums specificities. The expression is as specific +/// as the combination of all its operands, since all operands must match. +/// +/// - [`Expression::not`][]: contributes nothing, i.e., `(0, 0, 0, 0)`, since +/// a negation is a guard that filters matches but does not select them. +/// +/// Alternate groups, e.g. `{jpg,png}`, are equivalent to [`Expression::any`][] +/// at the [`Atom`][] level and follow the same rules. +/// +/// [`Atom`]: crate::id::specificity::segment::Atom +/// [`Expression`]: crate::id::filter::Expression +/// [`Expression::all`]: crate::id::filter::Expression::all +/// [`Expression::any`]: crate::id::filter::Expression::any +/// [`Expression::not`]: crate::id::filter::Expression::not +/// [`Glob`]: globset::Glob +/// [`Id`]: crate::id::Id +/// [`Operand`]: crate::id::filter::expression::Operand +/// [`Operator`]: crate::id::filter::expression::Operator +/// [`Segment`]: crate::id::specificity::segment::Segment +/// [`Selector`]: crate::id::matcher::selector::Selector +/// [`Term`]: crate::id::filter::Term +/// +/// # Examples +/// +/// ``` +/// # use std::error::Error; +/// # fn main() -> Result<(), Box> { +/// use zrx_id::filter::Expression; +/// use zrx_id::selector; +/// use zrx_id::specificity::ToSpecificity; +/// +/// // Create expression and compute specificity +/// let expr = Expression::any(|expr| { +/// expr.with(selector!(location = "**/*.jpg")?)? +/// .with(selector!(location = "**/*.png")?) +/// })?; +/// assert_eq!( +/// expr.to_specificity(), +/// (0, 1, 1, 4).into() +/// ); +/// # Ok(()) +/// # } +/// ``` #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct Specificity(u16, u16, u16, u16); From f81e1ba6afe701b706112e4cae136841ba7d1d5d Mon Sep 17 00:00:00 2001 From: squidfunk Date: Mon, 23 Mar 2026 21:39:26 +0100 Subject: [PATCH 09/23] chore: update dependencies Signed-off-by: squidfunk --- Cargo.lock | 12 ++++++------ Cargo.toml | 5 ++--- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4893ba0..efd036f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -229,9 +229,9 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "syn" @@ -246,18 +246,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 9bd1b8d..1f5a5e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,7 +39,6 @@ pedantic = { level = "warn", priority = -1 } [workspace.dependencies] zrx-diagnostic = { version = "0.0.1", path = "crates/zrx-diagnostic" } -zrx-event = { version = "0.0.1", path = "crates/zrx-event" } zrx-executor = { version = "0.0.3", path = "crates/zrx-executor" } zrx-graph = { version = "0.0.10", path = "crates/zrx-graph" } zrx-id = { version = "0.0.11", path = "crates/zrx-id" } @@ -54,7 +53,7 @@ file-id = "0.2.3" globset = "0.4.18" notify = "8.2.0" percent-encoding = "2.3.2" -slab = "0.4.11" -thiserror = "2.0.17" +slab = "0.4.12" +thiserror = "2.0.18" tracing = "0.1.41" walkdir = "2.5.0" From eb372f622c645f1f6ae10c06f96cfc06a4db5afb Mon Sep 17 00:00:00 2001 From: squidfunk Date: Mon, 23 Mar 2026 21:48:14 +0100 Subject: [PATCH 10/23] docs: fix doc comments Signed-off-by: squidfunk --- crates/zrx-id/src/id/filter.rs | 2 +- crates/zrx-id/src/id/filter/candidates.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/zrx-id/src/id/filter.rs b/crates/zrx-id/src/id/filter.rs index 5f53c6a..be647ae 100644 --- a/crates/zrx-id/src/id/filter.rs +++ b/crates/zrx-id/src/id/filter.rs @@ -71,7 +71,7 @@ pub use terms::Terms; /// /// // Create filter builder and insert expression /// let mut builder = Filter::builder(); -/// builder.insert(Expression::any(|expr| { +/// builder.insert(Expression::all(|expr| { /// expr.with(selector!(location = "**/*.md")?)? /// .with(selector!(provider = "file")?) /// })?); diff --git a/crates/zrx-id/src/id/filter/candidates.rs b/crates/zrx-id/src/id/filter/candidates.rs index b239904..9f995d9 100644 --- a/crates/zrx-id/src/id/filter/candidates.rs +++ b/crates/zrx-id/src/id/filter/candidates.rs @@ -81,7 +81,7 @@ impl Filter { /// /// // Create filter builder and insert expression /// let mut builder = Filter::builder(); - /// builder.insert(Expression::any(|expr| { + /// builder.insert(Expression::all(|expr| { /// expr.with(selector!(location = "**/*.md")?)? /// .with(selector!(provider = "file")?) /// })?); From a817004a21d188264940bbfd5073910d95583dd2 Mon Sep 17 00:00:00 2001 From: squidfunk Date: Tue, 24 Mar 2026 11:33:12 +0100 Subject: [PATCH 11/23] feature: add `Specified` for ordering values by `Specificity` Signed-off-by: squidfunk --- crates/zrx-id/src/id/specificity.rs | 11 +- crates/zrx-id/src/id/specificity/convert.rs | 31 +-- crates/zrx-id/src/id/specificity/specified.rs | 220 ++++++++++++++++++ 3 files changed, 233 insertions(+), 29 deletions(-) create mode 100644 crates/zrx-id/src/id/specificity/specified.rs diff --git a/crates/zrx-id/src/id/specificity.rs b/crates/zrx-id/src/id/specificity.rs index a0ad3eb..4dfbb02 100644 --- a/crates/zrx-id/src/id/specificity.rs +++ b/crates/zrx-id/src/id/specificity.rs @@ -29,9 +29,11 @@ use std::cmp::{self, Ordering}; mod convert; pub mod segment; +mod specified; mod tokens; pub use convert::ToSpecificity; +pub use specified::Specified; // ---------------------------------------------------------------------------- // Structs @@ -119,10 +121,7 @@ pub use convert::ToSpecificity; /// expr.with(selector!(location = "**/*.jpg")?)? /// .with(selector!(location = "**/*.png")?) /// })?; -/// assert_eq!( -/// expr.to_specificity(), -/// (0, 1, 1, 4).into() -/// ); +/// assert_eq!(expr.to_specificity(), (0, 1, 1, 4).into()); /// # Ok(()) /// # } /// ``` @@ -193,7 +192,7 @@ impl PartialOrd for Specificity { /// use zrx_id::specificity::ToSpecificity; /// use zrx_id::selector; /// - /// // Create selector and compute specificity + /// // Create and compare selectors by specificity /// let a = selector!(location = "**/*.md")?; /// let b = selector!(location = "*.md")?; /// assert!(a.to_specificity() < b.to_specificity()); @@ -217,7 +216,7 @@ impl Ord for Specificity { /// use zrx_id::specificity::ToSpecificity; /// use zrx_id::selector; /// - /// // Create selector and compute specificity + /// // Create and compare selectors by specificity /// let a = selector!(location = "**/*.md")?; /// let b = selector!(location = "*.md")?; /// assert!(a.to_specificity() < b.to_specificity()); diff --git a/crates/zrx-id/src/id/specificity/convert.rs b/crates/zrx-id/src/id/specificity/convert.rs index 65ff725..d738ac0 100644 --- a/crates/zrx-id/src/id/specificity/convert.rs +++ b/crates/zrx-id/src/id/specificity/convert.rs @@ -67,10 +67,7 @@ impl ToSpecificity for Expression { /// expr.with(selector!(location = "**/*.jpg")?)? /// .with(selector!(location = "**/*.png")?) /// })?; - /// assert_eq!( - /// expr.to_specificity(), - /// (0, 1, 1, 4).into() - /// ); + /// assert_eq!(expr.to_specificity(), (0, 1, 1, 4).into()); /// # Ok(()) /// # } /// ``` @@ -98,11 +95,8 @@ impl ToSpecificity for Term { /// use zrx_id::specificity::ToSpecificity; /// /// // Create term and compute specificity - /// let term = Term::from(selector!(location = "**/*.{jpg,png}")?); - /// assert_eq!( - /// term.to_specificity(), - /// (0, 1, 1, 4).into() - /// ); + /// let term = Term::from(selector!(location = "**/*.md")?); + /// assert_eq!(term.to_specificity(), (0, 1, 1, 3).into()); /// # Ok(()) /// # } /// ``` @@ -128,11 +122,8 @@ impl ToSpecificity for Operand { /// use zrx_id::specificity::ToSpecificity; /// /// // Create operand and compute specificity - /// let operand = Operand::from(selector!(location = "**/*.{jpg,png}")?); - /// assert_eq!( - /// operand.to_specificity(), - /// (0, 1, 1, 4).into() - /// ); + /// let operand = Operand::from(selector!(location = "**/*.md")?); + /// assert_eq!(operand.to_specificity(), (0, 1, 1, 3).into()); /// # Ok(()) /// # } /// ``` @@ -160,10 +151,7 @@ impl ToSpecificity for Id { /// /// // Create identifier and compute specificity /// let id = id!(provider = "file", context = ".", location = "index.md")?; - /// assert_eq!( - /// id.to_specificity(), - /// (3, 0, 0, 13).into() - /// ); + /// assert_eq!(id.to_specificity(), (3, 0, 0, 13).into()); /// # Ok(()) /// # } /// ``` @@ -188,11 +176,8 @@ impl ToSpecificity for Selector { /// use zrx_id::specificity::ToSpecificity; /// /// // Create selector and compute specificity - /// let selector = selector!(location = "**/*.{jpg,png}")?; - /// assert_eq!( - /// selector.to_specificity(), - /// (0, 1, 1, 4).into() - /// ); + /// let selector = selector!(location = "**/*.md")?; + /// assert_eq!(selector.to_specificity(), (0, 1, 1, 3).into()); /// # Ok(()) /// # } /// ``` diff --git a/crates/zrx-id/src/id/specificity/specified.rs b/crates/zrx-id/src/id/specificity/specified.rs new file mode 100644 index 0000000..4bc3eb4 --- /dev/null +++ b/crates/zrx-id/src/id/specificity/specified.rs @@ -0,0 +1,220 @@ +// Copyright (c) 2025-2026 Zensical and contributors + +// SPDX-License-Identifier: MIT +// All contributions are certified under the DCO + +// 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 NON-INFRINGEMENT. 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. + +// ---------------------------------------------------------------------------- + +//! Specified value. + +use std::cmp::Ordering; +use std::ops::Deref; + +use super::convert::ToSpecificity; +use super::Specificity; + +// ---------------------------------------------------------------------------- +// Structs +// ---------------------------------------------------------------------------- + +/// Specified value. +/// +/// This data type is a thin wrapper around a value of type `T`, augmenting the +/// value with its computed [`Specificity`], so it can be efficiently ordered. +#[derive(Clone, Debug)] +pub struct Specified { + /// Inner value. + inner: T, + /// Specificity. + specificity: Specificity, +} + +// ---------------------------------------------------------------------------- +// Implementations +// ---------------------------------------------------------------------------- + +impl Specified { + /// Returns the inner value, consuming the specified value. + /// + /// # Examples + /// + /// ``` + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// use zrx_id::selector; + /// use zrx_id::specificity::Specified; + /// + /// // Create selector + /// let selector = selector!(location = "**/*.md")?; + /// + /// // Create specified value from selector + /// let value = Specified::from(selector.clone()); + /// assert_eq!(value.into_inner(), selector); + /// # Ok(()) + /// # } + /// ``` + #[inline] + pub fn into_inner(self) -> T { + self.inner + } +} + +#[allow(clippy::must_use_candidate)] +impl Specified { + /// Returns the specificity. + #[inline] + pub fn specificity(self) -> Specificity { + self.specificity + } +} + +// ---------------------------------------------------------------------------- +// Trait implementations +// ---------------------------------------------------------------------------- + +impl From for Specified +where + T: ToSpecificity, +{ + /// Creates a specified value from a value. + /// + /// # Examples + /// + /// ``` + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// use zrx_id::selector; + /// use zrx_id::specificity::Specified; + /// + /// // Create selector + /// let selector = selector!(location = "**/*.md")?; + /// + /// // Create specified value from selector + /// let value = Specified::from(selector); + /// assert_eq!(value.specificity(), (0, 1, 1, 3).into()); + /// # Ok(()) + /// # } + /// ``` + #[inline] + fn from(inner: T) -> Self { + let specificity = inner.to_specificity(); + Specified { inner, specificity } + } +} + +// ---------------------------------------------------------------------------- + +impl Deref for Specified { + type Target = T; + + /// Dereferences to the inner value. + #[inline] + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +// ---------------------------------------------------------------------------- + +impl PartialEq for Specified +where + T: PartialEq, +{ + /// Compares two specified values for equality. + /// + /// # Examples + /// + /// ``` + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// use zrx_id::selector; + /// use zrx_id::specificity::Specified; + /// + /// // Create and compare selectors + /// let a = Specified::from(selector!(location = "*.md")?); + /// let b = Specified::from(selector!(location = "*.md")?); + /// assert_eq!(a, b); + /// # Ok(()) + /// # } + /// ``` + #[inline] + fn eq(&self, other: &Self) -> bool { + self.inner == other.inner + } +} + +impl Eq for Specified where T: Eq {} + +// ---------------------------------------------------------------------------- + +impl PartialOrd for Specified +where + T: Eq, +{ + /// Orders two values by specificity. + /// + /// # Examples + /// + /// ``` + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// use zrx_id::selector; + /// use zrx_id::specificity::Specified; + /// + /// // Create and compare selectors by specificity + /// let a = Specified::from(selector!(location = "**/*.md")?); + /// let b = Specified::from(selector!(location = "*.md")?); + /// assert!(a < b); + /// # Ok(()) + /// # } + /// ``` + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Specified +where + T: Eq, +{ + /// Orders two values by specificity. + /// + /// # Examples + /// + /// ``` + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// use zrx_id::selector; + /// use zrx_id::specificity::Specified; + /// + /// // Create and compare selectors by specificity + /// let a = Specified::from(selector!(location = "**/*.md")?); + /// let b = Specified::from(selector!(location = "*.md")?); + /// assert!(a < b); + /// # Ok(()) + /// # } + /// ``` + #[inline] + fn cmp(&self, other: &Self) -> Ordering { + self.specificity.cmp(&other.specificity) + } +} From 155315ed330c056d39fc474c6735ebefe1c23fa4 Mon Sep 17 00:00:00 2001 From: squidfunk Date: Tue, 24 Mar 2026 11:34:27 +0100 Subject: [PATCH 12/23] docs: improve doc comments Signed-off-by: squidfunk --- crates/zrx-graph/src/graph/topology.rs | 2 +- .../src/store/comparator/comparable.rs | 26 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/crates/zrx-graph/src/graph/topology.rs b/crates/zrx-graph/src/graph/topology.rs index a3683b7..2ca19f0 100644 --- a/crates/zrx-graph/src/graph/topology.rs +++ b/crates/zrx-graph/src/graph/topology.rs @@ -169,7 +169,7 @@ impl PartialEq for Topology { /// builder.add_edge(a, b)?; /// builder.add_edge(b, c)?; /// - /// // Create topology + /// // Create and compare topologies /// let topology = Topology::new(builder.len(), builder.edges()); /// assert_eq!(topology, topology.clone()); /// # Ok(()) diff --git a/crates/zrx-store/src/store/comparator/comparable.rs b/crates/zrx-store/src/store/comparator/comparable.rs index 04f3832..6024f4b 100644 --- a/crates/zrx-store/src/store/comparator/comparable.rs +++ b/crates/zrx-store/src/store/comparator/comparable.rs @@ -51,8 +51,8 @@ use super::{Ascending, Comparator}; /// use zrx_store::comparator::Comparable; /// /// // Create and compare values -/// let a: Comparable<_> = 42.into(); -/// let b: Comparable<_> = 84.into(); +/// let a = Comparable::from(42); +/// let b = Comparable::from(84); /// assert!(a < b); /// ``` #[derive(Clone)] @@ -109,7 +109,7 @@ impl From for Comparable { /// use zrx_store::comparator::Comparable; /// /// // Create comparable value - /// let value: Comparable<_> = 42.into(); + /// let value = Comparable::from(42); /// assert_eq!(*value, 42); /// ``` #[inline] @@ -123,7 +123,7 @@ impl From for Comparable { impl Deref for Comparable { type Target = T; - /// Dereferences to the wrapped value. + /// Dereferences to the inner value. #[inline] fn deref(&self) -> &Self::Target { &self.0 @@ -136,7 +136,7 @@ impl PartialEq for Comparable where T: PartialEq, { - /// Compares two values for equality. + /// Compares two comparable values for equality. /// /// # Examples /// @@ -144,8 +144,8 @@ where /// use zrx_store::comparator::Comparable; /// /// // Create and compare values - /// let a: Comparable<_> = 42.into(); - /// let b: Comparable<_> = 42.into(); + /// let a = Comparable::from(42); + /// let b = Comparable::from(42); /// assert_eq!(a, b); /// ``` #[inline] @@ -163,7 +163,7 @@ where T: Eq, C: Comparator, { - /// Orders two values. + /// Orders two comparable values. /// /// # Examples /// @@ -171,8 +171,8 @@ where /// use zrx_store::comparator::Comparable; /// /// // Create and compare values - /// let a: Comparable<_> = 42.into(); - /// let b: Comparable<_> = 84.into(); + /// let a = Comparable::from(42); + /// let b = Comparable::from(84); /// assert!(a < b); /// ``` #[inline] @@ -186,7 +186,7 @@ where T: Eq, C: Comparator, { - /// Orders two values. + /// Orders two comparable values. /// /// # Examples /// @@ -194,8 +194,8 @@ where /// use zrx_store::comparator::Comparable; /// /// // Create and compare values - /// let a: Comparable<_> = 42.into(); - /// let b: Comparable<_> = 84.into(); + /// let a = Comparable::from(42); + /// let b = Comparable::from(84); /// assert!(a < b); /// ``` #[inline] From 1fd37da0a65dd6bae7b4fb1abadd410c52b714b3 Mon Sep 17 00:00:00 2001 From: squidfunk Date: Tue, 24 Mar 2026 11:42:38 +0100 Subject: [PATCH 13/23] fix: `Specificity` is consumed by `Specified::specificity` accessor Signed-off-by: squidfunk --- crates/zrx-id/src/id/specificity/specified.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/zrx-id/src/id/specificity/specified.rs b/crates/zrx-id/src/id/specificity/specified.rs index 4bc3eb4..f607597 100644 --- a/crates/zrx-id/src/id/specificity/specified.rs +++ b/crates/zrx-id/src/id/specificity/specified.rs @@ -81,7 +81,7 @@ impl Specified { impl Specified { /// Returns the specificity. #[inline] - pub fn specificity(self) -> Specificity { + pub fn specificity(&self) -> Specificity { self.specificity } } From 499d66bae567bec8d5d34265c23b54f7c6e42ba3 Mon Sep 17 00:00:00 2001 From: squidfunk Date: Tue, 24 Mar 2026 12:02:15 +0100 Subject: [PATCH 14/23] feature: add `PartialEq` and `Eq` impls to `Expression` and `Operand` Signed-off-by: squidfunk --- crates/zrx-id/src/id/filter/expression.rs | 2 +- crates/zrx-id/src/id/filter/expression/operand.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/zrx-id/src/id/filter/expression.rs b/crates/zrx-id/src/id/filter/expression.rs index 1bfe590..b2141d0 100644 --- a/crates/zrx-id/src/id/filter/expression.rs +++ b/crates/zrx-id/src/id/filter/expression.rs @@ -71,7 +71,7 @@ pub use operand::{Operand, Operator, Term}; /// # Ok(()) /// # } /// ``` -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Expression { /// Expression operator. operator: Operator, diff --git a/crates/zrx-id/src/id/filter/expression/operand.rs b/crates/zrx-id/src/id/filter/expression/operand.rs index e624a23..bf3722a 100644 --- a/crates/zrx-id/src/id/filter/expression/operand.rs +++ b/crates/zrx-id/src/id/filter/expression/operand.rs @@ -42,7 +42,7 @@ pub use term::Term; // ---------------------------------------------------------------------------- /// Operand. -#[derive(Clone)] +#[derive(Clone, PartialEq, Eq)] pub enum Operand { /// Expression. Expression(Expression), From fadb8c0fa9b6d184c17a238a65e97f51528a468d Mon Sep 17 00:00:00 2001 From: squidfunk Date: Tue, 24 Mar 2026 13:12:46 +0100 Subject: [PATCH 15/23] docs: add note on ordering to `Specified` Signed-off-by: squidfunk --- crates/zrx-id/src/id/specificity/specified.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/zrx-id/src/id/specificity/specified.rs b/crates/zrx-id/src/id/specificity/specified.rs index f607597..961e1d8 100644 --- a/crates/zrx-id/src/id/specificity/specified.rs +++ b/crates/zrx-id/src/id/specificity/specified.rs @@ -39,6 +39,11 @@ use super::Specificity; /// /// This data type is a thin wrapper around a value of type `T`, augmenting the /// value with its computed [`Specificity`], so it can be efficiently ordered. +/// +/// Note that specifities are ordered from lowest to highest, with the least +/// specific value being the first in order. It would be natural to reverse +/// this order, but this would lead to more complex implementations for when +/// a specified value is wrapped in an [`Option`]. #[derive(Clone, Debug)] pub struct Specified { /// Inner value. From ba22db99b5181491a58dc837117d50a3055f8304 Mon Sep 17 00:00:00 2001 From: squidfunk Date: Tue, 24 Mar 2026 14:15:04 +0100 Subject: [PATCH 16/23] feature: add `Default` impl for `Expression` Signed-off-by: squidfunk --- crates/zrx-id/src/id/filter/expression.rs | 43 +++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/crates/zrx-id/src/id/filter/expression.rs b/crates/zrx-id/src/id/filter/expression.rs index b2141d0..8760b14 100644 --- a/crates/zrx-id/src/id/filter/expression.rs +++ b/crates/zrx-id/src/id/filter/expression.rs @@ -96,6 +96,18 @@ impl Expression { pub fn operands(&self) -> &[Operand] { &self.operands } + + /// Returns the number of operands. + #[inline] + pub fn len(&self) -> usize { + self.operands.len() + } + + /// Returns whether there are any operands. + #[inline] + pub fn is_empty(&self) -> bool { + self.operands.is_empty() + } } // ---------------------------------------------------------------------------- @@ -150,3 +162,34 @@ impl IntoIterator for Expression { self.operands.into_iter() } } + +// ---------------------------------------------------------------------------- + +impl Default for Expression { + /// Creates an expression that matches everything. + /// + /// While it may seem counterintuitive to have the default expression match + /// everything, it is designed this way to align with the concept of vacuous + /// truth in logic. An expression with no operands is considered to be true, + /// as there are no conditions to violate it. + /// + /// In our implementation, we use an empty expression with a logical `NOT` + /// operator to represent this concept, to be distinguishable as a marker. + /// + /// # Examples + /// + /// ``` + /// use zrx_id::Expression; + /// + /// // Create empty expression + /// let expr = Expression::default(); + /// assert!(expr.is_empty()); + /// ``` + #[inline] + fn default() -> Self { + Expression { + operator: Operator::Not, + operands: Vec::new(), + } + } +} From e092ce43d743f5b83dbd92e265a4637f64967d60 Mon Sep 17 00:00:00 2001 From: squidfunk Date: Tue, 24 Mar 2026 15:49:31 +0100 Subject: [PATCH 17/23] fix: always order all-zero `Specificity` first Signed-off-by: squidfunk --- crates/zrx-id/src/id/specificity.rs | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/crates/zrx-id/src/id/specificity.rs b/crates/zrx-id/src/id/specificity.rs index 4dfbb02..0511aee 100644 --- a/crates/zrx-id/src/id/specificity.rs +++ b/crates/zrx-id/src/id/specificity.rs @@ -138,7 +138,12 @@ impl Specificity { fn sum(self, other: Self) -> Self { let Specificity(a1, b1, c1, l1) = self; let Specificity(a2, b2, c2, l2) = other; - Self(a1 + a2, b1 + b2, c1 + c2, l1 + l2) + Self( + a1.saturating_add(a2), + b1.saturating_add(b2), + c1.saturating_add(c2), + l1.saturating_add(l2), + ) } /// Computes the minimum of both specificities. @@ -189,8 +194,8 @@ impl PartialOrd for Specificity { /// ``` /// # use std::error::Error; /// # fn main() -> Result<(), Box> { - /// use zrx_id::specificity::ToSpecificity; /// use zrx_id::selector; + /// use zrx_id::specificity::ToSpecificity; /// /// // Create and compare selectors by specificity /// let a = selector!(location = "**/*.md")?; @@ -213,8 +218,8 @@ impl Ord for Specificity { /// ``` /// # use std::error::Error; /// # fn main() -> Result<(), Box> { - /// use zrx_id::specificity::ToSpecificity; /// use zrx_id::selector; + /// use zrx_id::specificity::ToSpecificity; /// /// // Create and compare selectors by specificity /// let a = selector!(location = "**/*.md")?; @@ -227,9 +232,21 @@ impl Ord for Specificity { fn cmp(&self, other: &Self) -> Ordering { let Specificity(a1, b1, c1, l1) = self; let Specificity(a2, b2, c2, l2) = other; + + // An all-zero specificity is the least specific and must always be the + // first in order, so check if this applies to any of the specificities + if a1 | b1 | c1 | l1 == 0 { + return Ordering::Less; + } + if a2 | b2 | c2 | l2 == 0 { + return Ordering::Greater; + } + + // Otherwise, compare each component, where `c` is reversed since fewer + // double-wildcards is more specific than more double-wildcards a1.cmp(a2) - .then(b1.cmp(b2)) - .then(c2.cmp(c1)) // reversed, fewer ** = more specific - .then(l1.cmp(l2)) + .then_with(|| b1.cmp(b2)) + .then_with(|| c2.cmp(c1)) + .then_with(|| l1.cmp(l2)) } } From 6293bde8f0ee5605c12eddb932e1f13e30dd4a5e Mon Sep 17 00:00:00 2001 From: squidfunk Date: Tue, 24 Mar 2026 19:01:45 +0100 Subject: [PATCH 18/23] chore: replace `cmp::min` with `Specificity::min` Signed-off-by: squidfunk --- crates/zrx-id/src/id/specificity/convert.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/zrx-id/src/id/specificity/convert.rs b/crates/zrx-id/src/id/specificity/convert.rs index d738ac0..83901a0 100644 --- a/crates/zrx-id/src/id/specificity/convert.rs +++ b/crates/zrx-id/src/id/specificity/convert.rs @@ -25,8 +25,6 @@ //! Specificity computation. -use std::cmp; - use crate::id::filter::expression::{Operand, Operator}; use crate::id::filter::{Expression, Term}; use crate::id::matcher::selector::Selector; @@ -230,7 +228,7 @@ impl ToSpecificity for Atom<'_> { Atom::Group(data) => data .iter() .map(ToSpecificity::to_specificity) - .reduce(cmp::min) + .reduce(Specificity::min) .unwrap_or_default(), } } From 11b62f1ae669e03f35d112cd606d808964e31fb8 Mon Sep 17 00:00:00 2001 From: squidfunk Date: Wed, 25 Mar 2026 16:55:18 +0100 Subject: [PATCH 19/23] feature: add `Default` impl for `Specified` Signed-off-by: squidfunk --- crates/zrx-id/src/id/specificity/specified.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/zrx-id/src/id/specificity/specified.rs b/crates/zrx-id/src/id/specificity/specified.rs index 961e1d8..413ef0c 100644 --- a/crates/zrx-id/src/id/specificity/specified.rs +++ b/crates/zrx-id/src/id/specificity/specified.rs @@ -44,7 +44,7 @@ use super::Specificity; /// specific value being the first in order. It would be natural to reverse /// this order, but this would lead to more complex implementations for when /// a specified value is wrapped in an [`Option`]. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] pub struct Specified { /// Inner value. inner: T, From 624c4dc02289fdb426c2632559c6b97313f0762a Mon Sep 17 00:00:00 2001 From: squidfunk Date: Wed, 25 Mar 2026 18:44:43 +0100 Subject: [PATCH 20/23] docs: fix comment Signed-off-by: squidfunk --- crates/zrx-id/src/id/filter/builder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/zrx-id/src/id/filter/builder.rs b/crates/zrx-id/src/id/filter/builder.rs index c926fa7..59f5353 100644 --- a/crates/zrx-id/src/id/filter/builder.rs +++ b/crates/zrx-id/src/id/filter/builder.rs @@ -46,7 +46,7 @@ use super::Filter; /// after all modifications were made. #[derive(Debug, Default)] pub struct Builder { - /// Conditions. + /// Condition set. conditions: Slab, } From 2522c1741cfd85f2b1710172f67d69714fa322cc Mon Sep 17 00:00:00 2001 From: squidfunk Date: Sun, 29 Mar 2026 20:44:56 +0200 Subject: [PATCH 21/23] refactor: store `Character` class as raw string slices Signed-off-by: squidfunk --- .../id/specificity/segment/atom/character.rs | 33 ++++++++++++------- .../src/id/specificity/segment/convert.rs | 12 +------ crates/zrx-id/src/id/specificity/tokens.rs | 8 ++--- 3 files changed, 25 insertions(+), 28 deletions(-) diff --git a/crates/zrx-id/src/id/specificity/segment/atom/character.rs b/crates/zrx-id/src/id/specificity/segment/atom/character.rs index 43d4004..538b148 100644 --- a/crates/zrx-id/src/id/specificity/segment/atom/character.rs +++ b/crates/zrx-id/src/id/specificity/segment/atom/character.rs @@ -32,29 +32,40 @@ use std::fmt::{self, Display, Write}; // ---------------------------------------------------------------------------- /// Character class. +/// +/// For our case, we do not need to know the exact structure of the character +/// class, as we'll score it as a single character anyway, same as `*`. We also +/// don't care whether it's negated or not, as that doesn't affect scoring as +/// well. Therefore, we can just store the string slices. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Character<'a> { - /// Negation marker. - pub negate: bool, - /// Characters. - pub values: Vec<&'a str>, + /// String slices. + values: Vec<&'a str>, } // ---------------------------------------------------------------------------- // Trait implementations // ---------------------------------------------------------------------------- +impl<'a> FromIterator<&'a str> for Character<'a> { + /// Creates a character class from an iterator. + #[inline] + fn from_iter(iter: T) -> Self + where + T: IntoIterator, + { + Character { + values: iter.into_iter().collect(), + } + } +} + +// ---------------------------------------------------------------------------- + impl Display for Character<'_> { /// Formats the character class for display. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_char('[')?; - - // Write negation marker - if self.negate { - f.write_char('!')?; - } - - // Write characters for value in &self.values { f.write_str(value)?; } diff --git a/crates/zrx-id/src/id/specificity/segment/convert.rs b/crates/zrx-id/src/id/specificity/segment/convert.rs index 1ecb5ff..27738ba 100644 --- a/crates/zrx-id/src/id/specificity/segment/convert.rs +++ b/crates/zrx-id/src/id/specificity/segment/convert.rs @@ -125,16 +125,6 @@ fn parse_segment<'a>(iter: &mut Iter<'a>, group: bool) -> Segment<'a> { fn parse_character<'a>(iter: &mut Iter<'a>) -> Character<'a> { let mut values = Vec::new(); - // Consume negation marker if present - let negate = match iter.next() { - Some(Token::Exclamation) => true, - None => false, - Some(token) => { - values.push(token.as_str()); - false - } - }; - // Consume tokens until character class end for token in iter.by_ref() { values.push(match token { @@ -144,7 +134,7 @@ fn parse_character<'a>(iter: &mut Iter<'a>) -> Character<'a> { } // Return character class - Character { negate, values } + Character::from_iter(values) } /// Parses a sequence of tokens into a group of segments. diff --git a/crates/zrx-id/src/id/specificity/tokens.rs b/crates/zrx-id/src/id/specificity/tokens.rs index 7fda6b4..4e3e65e 100644 --- a/crates/zrx-id/src/id/specificity/tokens.rs +++ b/crates/zrx-id/src/id/specificity/tokens.rs @@ -50,8 +50,6 @@ pub enum Token<'a> { CharacterStart, /// Character class end - `]` CharacterEnd, - /// Exclamation mark - `!` - Exclamation, /// Alternate group start - `{` GroupStart, /// Alternate group end - `}` @@ -98,7 +96,6 @@ impl<'a> Token<'a> { Token::StarStar => "**", Token::CharacterStart => "[", Token::CharacterEnd => "]", - Token::Exclamation => "!", Token::GroupStart => "{", Token::GroupEnd => "}", Token::Comma => ",", @@ -110,7 +107,7 @@ impl<'a> Token<'a> { // ---------------------------------------------------------------------------- impl<'a> From<&'a str> for Tokens<'a> { - /// Creates an iterator over the tokens of a pattern. + /// Creates an iterator over tokens from a string slice. #[inline] fn from(value: &'a str) -> Self { Self { value, index: 0 } @@ -146,7 +143,6 @@ impl<'a> Iterator for Tokens<'a> { b'?' => Some(Token::Any), b'[' => Some(Token::CharacterStart), b']' => Some(Token::CharacterEnd), - b'!' => Some(Token::Exclamation), b'{' => Some(Token::GroupStart), b'}' => Some(Token::GroupEnd), b',' => Some(Token::Comma), @@ -185,6 +181,6 @@ impl<'a> Iterator for Tokens<'a> { fn is_special(char: u8) -> bool { matches!( char, - b'.' | b'?' | b'*' | b'[' | b']' | b'!' | b'{' | b'}' | b',' | b'/' + b'.' | b'?' | b'*' | b'[' | b']' | b'{' | b'}' | b',' | b'/' ) } From aa9c9371dfc54faa11a19d2283c405a30e856dec Mon Sep 17 00:00:00 2001 From: squidfunk Date: Sun, 29 Mar 2026 20:54:06 +0200 Subject: [PATCH 22/23] refactor: replace `u32` cast with explicit conversion Signed-off-by: squidfunk --- crates/zrx-id/src/id/filter/builder.rs | 5 ++--- crates/zrx-id/src/id/filter/error.rs | 5 ++++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/zrx-id/src/id/filter/builder.rs b/crates/zrx-id/src/id/filter/builder.rs index 59f5353..79d385c 100644 --- a/crates/zrx-id/src/id/filter/builder.rs +++ b/crates/zrx-id/src/id/filter/builder.rs @@ -187,7 +187,6 @@ impl Builder { /// # Ok(()) /// # } /// ``` - #[allow(clippy::cast_possible_truncation)] pub fn build(self) -> Result { let mut builder = Matcher::builder(); @@ -198,7 +197,7 @@ impl Builder { // Add all terms of each condition to the mapping and matcher for (index, condition) in &self.conditions { for term in condition.terms() { - mapping.push(index as u32); + mapping.push(u32::try_from(index)?); match term { Term::Id(id) => builder.add(id)?, Term::Selector(selector) => builder.add(selector)?, @@ -209,7 +208,7 @@ impl Builder { // to the list of negations, so it's always checked when matching let mut iter = condition.instructions().iter(); if iter.any(|instruction| instruction.operator() == Operator::Not) { - negations.push(index as u32); + negations.push(u32::try_from(index)?); } } diff --git a/crates/zrx-id/src/id/filter/error.rs b/crates/zrx-id/src/id/filter/error.rs index 5787b79..5d9f66d 100644 --- a/crates/zrx-id/src/id/filter/error.rs +++ b/crates/zrx-id/src/id/filter/error.rs @@ -25,7 +25,7 @@ //! Filter error. -use std::result; +use std::{num, result}; use thiserror::Error; use crate::id::matcher; @@ -39,6 +39,9 @@ use super::expression; /// Filter error. #[derive(Debug, Error)] pub enum Error { + /// Numeric conversion error. + #[error(transparent)] + Numeric(#[from] num::TryFromIntError), /// Expression error. #[error(transparent)] Expression(#[from] expression::Error), From 941b5ce6c60211158e7584bf1c71e028e8807a84 Mon Sep 17 00:00:00 2001 From: squidfunk Date: Mon, 30 Mar 2026 16:49:27 +0200 Subject: [PATCH 23/23] docs: improve doc comments Signed-off-by: squidfunk --- crates/zrx-store/src/store/collection.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/zrx-store/src/store/collection.rs b/crates/zrx-store/src/store/collection.rs index 56af5f8..af52d4e 100644 --- a/crates/zrx-store/src/store/collection.rs +++ b/crates/zrx-store/src/store/collection.rs @@ -100,7 +100,7 @@ pub trait Collection: Any + Debug { // ---------------------------------------------------------------------------- impl dyn Collection { - /// Attempts to downcast the collection to a reference of `T`. + /// Attempts to downcast to a reference of `T`. #[inline] #[must_use] pub fn downcast_ref(&self) -> Option<&T> @@ -110,7 +110,7 @@ impl dyn Collection { (self as &dyn Any).downcast_ref() } - /// Attempts to downcast the collection to a mutable reference of `T`. + /// Attempts to downcast to a mutable reference of `T`. #[inline] #[must_use] pub fn downcast_mut(&mut self) -> Option<&mut T>