From b596f7aca68cf4f1006b590760cdbcb94adfe243 Mon Sep 17 00:00:00 2001 From: squidfunk Date: Thu, 19 Mar 2026 18:45:19 +0100 Subject: [PATCH 01/15] docs: improve docs for `Traversal::converge` Signed-off-by: squidfunk --- crates/zrx-graph/src/graph/traversal.rs | 37 +++++++++++++++++-------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/crates/zrx-graph/src/graph/traversal.rs b/crates/zrx-graph/src/graph/traversal.rs index 6996218..209bca2 100644 --- a/crates/zrx-graph/src/graph/traversal.rs +++ b/crates/zrx-graph/src/graph/traversal.rs @@ -267,12 +267,25 @@ impl Traversal { Ok(()) } - /// Attempts to converge this traversal with another traversal. + /// Attempts to converge with the given traversal. /// - /// This method attempts to combine both traversals into a single traversal, - /// which is possible when they have common descendants. This is useful for - /// deduplication of computations that need to be carried out for traversals - /// that originate from different sources, but converge at some point. + /// This method attempts to merge both traversals into a single traversal, + /// which is possible when they have common descendants, a condition that + /// is always true for directed acyclic graphs with a single component. + /// There are several cases to consider when converging two traversals: + /// + /// - If traversals start from the same set of source nodes, they already + /// converged, so we just restart the traversal at these source nodes. + /// + /// - If traversals start from different source nodes, yet both have common + /// descendants, we merge those at the first layer of common descendants, + /// as those are the initial nodes that must be visited in both of them. + /// Ancestors of the common descendants that have already been visited in + /// either traversal don't need to be revisited, and thus are carried over + /// from both traversals in their current state. + /// + /// - If traversals are disjoint, they can't be converged, so we return the + /// given traversal back to the caller wrapped in [`Error::Disjoint`]. /// /// # Errors /// @@ -327,15 +340,15 @@ impl Traversal { topology: self.topology.clone(), }; - // If there are no common descendants, the traversals are disjoint, and - // we can't converge them, so we return the traversal to the caller + // If there are no common descendants, the traversals are disjoint and + // can't converge, so we return the given traversal back to the caller let mut iter = graph.common_descendants(&initial); let Some(common) = iter.next() else { return Err(Error::Disjoint(other)); }; // Create the combined traversal, and mark all already visited nodes - // that are ancestors of the common descendants as visited as well + // that are ancestors of the common descendants as visited let prior = mem::replace(self, Self::new(&self.topology, initial)); // Compute the visitable nodes for the combined traversal, which is the @@ -347,10 +360,10 @@ impl Traversal { let p = prior.dependencies[node]; let o = other.dependencies[node]; - // If the node has been visited in either traversal, and is not - // part of the common descendants, mark it as visited as well in - // the combined traversal, since we don't need to revisit nodes - // that are ancestors of the common descendants + // If the node has been visited in either traversal, and is not part + // of the first layer of common descendants, mark it as visited in + // the combined traversal, since we don't need to revisit ancestors + // that have already been visited in either traversal if (p == u8::MAX || o == u8::MAX) && !common.contains(&node) { self.complete(node)?; } else { From 41b3e498f458cef89e6a4dbe8c44dda9f00640d7 Mon Sep 17 00:00:00 2001 From: squidfunk Date: Thu, 19 Mar 2026 20:01:57 +0100 Subject: [PATCH 02/15] fix: comparison of `Id` and `Selector` susceptible to hash collisions Signed-off-by: squidfunk --- crates/zrx-id/src/id.rs | 4 +++- crates/zrx-id/src/id/matcher/selector.rs | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/zrx-id/src/id.rs b/crates/zrx-id/src/id.rs index 3ff647a..852e30e 100644 --- a/crates/zrx-id/src/id.rs +++ b/crates/zrx-id/src/id.rs @@ -367,7 +367,9 @@ impl PartialEq for Id { /// ``` #[inline] fn eq(&self, other: &Self) -> bool { - self.hash == other.hash + // We first compare the precomputed hashes, which is extremly fast, as + // it saves us the comparison when the identifiers are different + self.hash == other.hash && self.format == other.format } } diff --git a/crates/zrx-id/src/id/matcher/selector.rs b/crates/zrx-id/src/id/matcher/selector.rs index a5aad64..ea14a61 100644 --- a/crates/zrx-id/src/id/matcher/selector.rs +++ b/crates/zrx-id/src/id/matcher/selector.rs @@ -343,7 +343,9 @@ impl PartialEq for Selector { /// ``` #[inline] fn eq(&self, other: &Self) -> bool { - self.hash == other.hash + // We first compare the precomputed hashes, which is extremely fast, as + // it saves us the comparison when the identifiers are different + self.hash == other.hash && self.format == other.format } } From b2870f5a2f96d2b2c374c73abf220d2273feb2c1 Mon Sep 17 00:00:00 2001 From: squidfunk Date: Thu, 19 Mar 2026 21:07:46 +0100 Subject: [PATCH 03/15] performance: remove branching in `PartialEq` impl for `Id` Signed-off-by: squidfunk --- crates/zrx-id/src/id.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/crates/zrx-id/src/id.rs b/crates/zrx-id/src/id.rs index 852e30e..82fc14c 100644 --- a/crates/zrx-id/src/id.rs +++ b/crates/zrx-id/src/id.rs @@ -419,13 +419,7 @@ impl Ord for Id { /// ``` #[inline] fn cmp(&self, other: &Self) -> Ordering { - // Fast path - first, we compare for equality by using the precomputed - // hashes, as it's a simple and extremely fast integer comparison - if self.eq(other) { - Ordering::Equal - } else { - self.format.cmp(&other.format) - } + self.format.cmp(&other.format) } } From dad5ed4c724ef957f57a2d498be8f684c0785dea Mon Sep 17 00:00:00 2001 From: squidfunk Date: Thu, 19 Mar 2026 21:08:54 +0100 Subject: [PATCH 04/15] performance: provide dedicated `PartialEq` impl for `Selector` Signed-off-by: squidfunk --- crates/zrx-id/src/id/matcher/selector.rs | 51 +++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/crates/zrx-id/src/id/matcher/selector.rs b/crates/zrx-id/src/id/matcher/selector.rs index ea14a61..a6dcfb6 100644 --- a/crates/zrx-id/src/id/matcher/selector.rs +++ b/crates/zrx-id/src/id/matcher/selector.rs @@ -27,6 +27,7 @@ use ahash::AHasher; use std::borrow::Cow; +use std::cmp::Ordering; use std::fmt::{self, Debug, Display}; use std::hash::{Hash, Hasher}; use std::str::FromStr; @@ -104,7 +105,7 @@ pub use convert::TryToSelector; /// # Ok(()) /// # } /// ``` -#[derive(Clone, PartialOrd, Ord)] +#[derive(Clone)] pub struct Selector { /// Formatted string. format: Arc>, @@ -353,6 +354,54 @@ impl Eq for Selector {} // ---------------------------------------------------------------------------- +impl PartialOrd for Selector { + /// Orders two selectors. + /// + /// # Examples + /// + /// ``` + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// use zrx_id::Selector; + /// + /// // Create and compare selectors + /// let a: Selector = "zrs:::::**/*.md:".parse()?; + /// let b: Selector = "zrs:::::**/*.rs:".parse()?; + /// assert!(a < b); + /// # Ok(()) + /// # } + /// ``` + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Selector { + /// Orders two selectors. + /// + /// # Examples + /// + /// ``` + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// use zrx_id::Selector; + /// + /// // Create and compare selectors + /// let a: Selector = "zrs:::::**/*.md:".parse()?; + /// let b: Selector = "zrs:::::**/*.rs:".parse()?; + /// assert!(a < b); + /// # Ok(()) + /// # } + /// ``` + #[inline] + fn cmp(&self, other: &Self) -> Ordering { + self.format.cmp(&other.format) + } +} + +// ---------------------------------------------------------------------------- + impl Display for Selector { /// Formats the selector for display. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { From 595a4d7be3be8330ed64bb0e1043d888e390c46a Mon Sep 17 00:00:00 2001 From: squidfunk Date: Thu, 19 Mar 2026 22:06:36 +0100 Subject: [PATCH 05/15] docs: adjust comment Signed-off-by: squidfunk --- crates/zrx-graph/src/graph/traversal.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/zrx-graph/src/graph/traversal.rs b/crates/zrx-graph/src/graph/traversal.rs index 209bca2..fb7bb54 100644 --- a/crates/zrx-graph/src/graph/traversal.rs +++ b/crates/zrx-graph/src/graph/traversal.rs @@ -278,8 +278,8 @@ impl Traversal { /// converged, so we just restart the traversal at these source nodes. /// /// - If traversals start from different source nodes, yet both have common - /// descendants, we merge those at the first layer of common descendants, - /// as those are the initial nodes that must be visited in both of them. + /// descendants, we converge at the first layer of common descendants, as + /// all descendants of them must be revisited in the combined traversal. /// Ancestors of the common descendants that have already been visited in /// either traversal don't need to be revisited, and thus are carried over /// from both traversals in their current state. From dd4e3f9c7e7038ad4a34560e07edda0782fe6c3c Mon Sep 17 00:00:00 2001 From: squidfunk Date: Fri, 20 Mar 2026 11:44:15 +0100 Subject: [PATCH 06/15] feature: add `Traversal::initial` impl to obtain initial nodes Signed-off-by: squidfunk --- crates/zrx-graph/src/graph.rs | 2 +- crates/zrx-graph/src/graph/traversal.rs | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/zrx-graph/src/graph.rs b/crates/zrx-graph/src/graph.rs index 499a9a5..f811633 100644 --- a/crates/zrx-graph/src/graph.rs +++ b/crates/zrx-graph/src/graph.rs @@ -190,7 +190,7 @@ impl Graph { #[allow(clippy::must_use_candidate)] impl Graph { - /// Returns the graph topology. + /// Returns a reference to the graph topology. #[inline] pub fn topology(&self) -> &Topology { &self.topology diff --git a/crates/zrx-graph/src/graph/traversal.rs b/crates/zrx-graph/src/graph/traversal.rs index fb7bb54..2127bc1 100644 --- a/crates/zrx-graph/src/graph/traversal.rs +++ b/crates/zrx-graph/src/graph/traversal.rs @@ -381,12 +381,18 @@ impl Traversal { #[allow(clippy::must_use_candidate)] impl Traversal { - /// Returns the graph topology. + /// Returns a reference to the graph topology. #[inline] pub fn topology(&self) -> &Topology { &self.topology } + /// Returns a reference to the initial nodes. + #[inline] + pub fn initial(&self) -> &[usize] { + &self.initial + } + /// Returns the number of visitable nodes. #[inline] pub fn len(&self) -> usize { From a69615de80b917d8665cc57592fde962ad6f29e5 Mon Sep 17 00:00:00 2001 From: squidfunk Date: Fri, 20 Mar 2026 20:51:39 +0100 Subject: [PATCH 07/15] feature!: replace `Graph::with` with `Graph::adjacent` accessor Signed-off-by: squidfunk --- crates/zrx-graph/src/graph/operator.rs | 2 +- .../graph/operator/{with.rs => adjacent.rs} | 40 ++++++++----------- 2 files changed, 17 insertions(+), 25 deletions(-) rename crates/zrx-graph/src/graph/operator/{with.rs => adjacent.rs} (83%) diff --git a/crates/zrx-graph/src/graph/operator.rs b/crates/zrx-graph/src/graph/operator.rs index c4dfcfc..e4d9bf0 100644 --- a/crates/zrx-graph/src/graph/operator.rs +++ b/crates/zrx-graph/src/graph/operator.rs @@ -25,5 +25,5 @@ //! Graph operators. +mod adjacent; mod map; -mod with; diff --git a/crates/zrx-graph/src/graph/operator/with.rs b/crates/zrx-graph/src/graph/operator/adjacent.rs similarity index 83% rename from crates/zrx-graph/src/graph/operator/with.rs rename to crates/zrx-graph/src/graph/operator/adjacent.rs index 0d35eec..5557d96 100644 --- a/crates/zrx-graph/src/graph/operator/with.rs +++ b/crates/zrx-graph/src/graph/operator/adjacent.rs @@ -23,7 +23,7 @@ // ---------------------------------------------------------------------------- -//! With operator. +//! Adjacent operator. use crate::graph::Graph; @@ -44,7 +44,7 @@ pub struct Adjacent<'a> { // ---------------------------------------------------------------------------- impl Graph { - /// Retrieve a reference to a node's data. + /// Retrieve a reference to a node and its adjacent nodes. /// /// # Examples /// @@ -63,24 +63,20 @@ impl Graph { /// builder.add_edge(a, b)?; /// builder.add_edge(b, c)?; /// - /// // Create graph from builder and retrieve nodes + /// // Create graph from builder and retrieve node /// let graph = builder.build(); - /// for node in &graph { - /// graph.with(node, |name, _| { - /// println!("{name:?}"); - /// }); - /// } + /// + /// // Obtain reference to node and adjacent nodes + /// let (data, adj) = graph.adjacent(a); /// # Ok(()) /// # } /// ``` #[inline] - pub fn with(&self, node: usize, f: F) -> R - where - F: FnOnce(&T, Adjacent) -> R, - { + #[must_use] + pub fn adjacent(&self, node: usize) -> (&'_ T, Adjacent<'_>) { let incoming = self.topology.incoming(); let outgoing = self.topology.outgoing(); - f( + ( &self.data[node], Adjacent { incoming: &incoming[node], @@ -89,7 +85,7 @@ impl Graph { ) } - /// Retrieve a mutable reference to a node's data. + /// Retrieve a mutable reference to a node and its adjacent nodes. /// /// # Examples /// @@ -110,22 +106,18 @@ impl Graph { /// /// // Create graph from builder and retrieve node /// let mut graph = builder.build(); - /// for node in &graph { - /// graph.with_mut(node, |name, _| { - /// println!("{name:?}"); - /// }); - /// } + /// + /// // Obtain mutable reference to node and adjacent nodes + /// let (data, adj) = graph.adjacent_mut(a); /// # Ok(()) /// # } /// ``` #[inline] - pub fn with_mut(&mut self, node: usize, f: F) -> R - where - F: FnOnce(&mut T, Adjacent) -> R, - { + #[must_use] + pub fn adjacent_mut(&mut self, node: usize) -> (&'_ mut T, Adjacent<'_>) { let incoming = self.topology.incoming(); let outgoing = self.topology.outgoing(); - f( + ( &mut self.data[node], Adjacent { incoming: &incoming[node], From ed036dae6caf9e9892d6b31eced8a4d165ac0ff3 Mon Sep 17 00:00:00 2001 From: squidfunk Date: Sat, 21 Mar 2026 11:49:35 +0100 Subject: [PATCH 08/15] chore: clarify test cases Signed-off-by: squidfunk --- crates/zrx-graph/src/graph/traversal.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/crates/zrx-graph/src/graph/traversal.rs b/crates/zrx-graph/src/graph/traversal.rs index 2127bc1..9238027 100644 --- a/crates/zrx-graph/src/graph/traversal.rs +++ b/crates/zrx-graph/src/graph/traversal.rs @@ -522,11 +522,10 @@ mod tests { (vec![6], vec![7], vec![6, 7, 8]), (vec![8], vec![8], vec![8]), ] { - let mut a = graph.traverse(i); - let b = graph.traverse(j); - assert!(a.converge(b).is_ok()); + let mut traversal = graph.traverse(i); + assert!(traversal.converge(graph.traverse(j)).is_ok()); assert_eq!( - a.into_iter().collect::>(), // fmt + traversal.into_iter().collect::>(), // fmt descendants ); } @@ -556,11 +555,10 @@ mod tests { (vec![6], vec![7], vec![6, 7, 8]), (vec![8], vec![8], vec![8]), ] { - let mut a = graph.traverse(i); - let b = graph.traverse(j); - assert!(a.converge(b).is_ok()); + let mut traversal = graph.traverse(i); + assert!(traversal.converge(graph.traverse(j)).is_ok()); assert_eq!( - a.into_iter().collect::>(), // fmt + traversal.into_iter().collect::>(), // fmt descendants ); } From 23b817067d07684f3a5489101208bf2e81c29e81 Mon Sep 17 00:00:00 2001 From: squidfunk Date: Mon, 23 Mar 2026 01:06:41 +0100 Subject: [PATCH 09/15] chore: improve signatures of filter `Builder` Signed-off-by: squidfunk --- crates/zrx-id/src/id/filter/builder.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/zrx-id/src/id/filter/builder.rs b/crates/zrx-id/src/id/filter/builder.rs index bd7ef1a..4a3c468 100644 --- a/crates/zrx-id/src/id/filter/builder.rs +++ b/crates/zrx-id/src/id/filter/builder.rs @@ -97,7 +97,7 @@ impl Filter { // ---------------------------------------------------------------------------- impl Builder { - /// Inserts an expression into the filter. + /// Inserts an expression into the filter and returns its index. /// /// This method adds an [`Expression`][] to the filter builder, and returns /// the index of the inserted condition, which can be used to remove it. @@ -156,8 +156,8 @@ impl Builder { /// # } /// ``` #[inline] - pub fn remove(&mut self, expr: usize) { - self.conditions.remove(expr); + pub fn remove(&mut self, index: usize) { + self.conditions.remove(index); } /// Builds the filter. From a4b1fd3e222f0c67aa845206d7d3b65c7e65d7e4 Mon Sep 17 00:00:00 2001 From: squidfunk Date: Mon, 23 Mar 2026 01:07:01 +0100 Subject: [PATCH 10/15] feature: add `Specificity` impl for computing glob ordering Signed-off-by: squidfunk --- crates/zrx-id/src/id.rs | 1 + crates/zrx-id/src/id/specificity.rs | 107 ++++++++++ crates/zrx-id/src/id/specificity/convert.rs | 125 ++++++++++++ crates/zrx-id/src/id/specificity/segment.rs | 89 ++++++++ .../zrx-id/src/id/specificity/segment/atom.rs | 88 ++++++++ .../id/specificity/segment/atom/character.rs | 63 ++++++ .../id/specificity/segment/atom/wildcard.rs | 58 ++++++ .../src/id/specificity/segment/convert.rs | 156 ++++++++++++++ .../src/id/specificity/segment/segments.rs | 94 +++++++++ crates/zrx-id/src/id/specificity/tokens.rs | 190 ++++++++++++++++++ .../src/id/specificity/tokens/convert.rs | 68 +++++++ crates/zrx-id/src/lib.rs | 1 + 12 files changed, 1040 insertions(+) create mode 100644 crates/zrx-id/src/id/specificity.rs create mode 100644 crates/zrx-id/src/id/specificity/convert.rs create mode 100644 crates/zrx-id/src/id/specificity/segment.rs create mode 100644 crates/zrx-id/src/id/specificity/segment/atom.rs create mode 100644 crates/zrx-id/src/id/specificity/segment/atom/character.rs create mode 100644 crates/zrx-id/src/id/specificity/segment/atom/wildcard.rs create mode 100644 crates/zrx-id/src/id/specificity/segment/convert.rs create mode 100644 crates/zrx-id/src/id/specificity/segment/segments.rs create mode 100644 crates/zrx-id/src/id/specificity/tokens.rs create mode 100644 crates/zrx-id/src/id/specificity/tokens/convert.rs diff --git a/crates/zrx-id/src/id.rs b/crates/zrx-id/src/id.rs index 82fc14c..f7d6418 100644 --- a/crates/zrx-id/src/id.rs +++ b/crates/zrx-id/src/id.rs @@ -43,6 +43,7 @@ pub mod filter; pub mod format; mod macros; pub mod matcher; +pub mod specificity; pub mod uri; pub use builder::Builder; diff --git a/crates/zrx-id/src/id/specificity.rs b/crates/zrx-id/src/id/specificity.rs new file mode 100644 index 0000000..2c1e91c --- /dev/null +++ b/crates/zrx-id/src/id/specificity.rs @@ -0,0 +1,107 @@ +// 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. + +// ---------------------------------------------------------------------------- + +//! Specificity. + +use std::cmp::{self, Ordering}; + +pub mod convert; +pub mod segment; +mod tokens; + +use convert::IntoSpecificity; +use tokens::AsTokens; + +// ---------------------------------------------------------------------------- +// Structs +// ---------------------------------------------------------------------------- + +/// Specificity. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct Specificity(u16, u16, u16, u16); + +// ---------------------------------------------------------------------------- +// Implementations +// ---------------------------------------------------------------------------- + +impl Specificity { + /// Creates the sum of both specificities. + #[inline] + fn sum(self, other: Self) -> Self { + Self( + self.0 + other.0, + self.1 + other.1, + self.2 + other.2, + self.3 + other.3, + ) + } + + /// Creates a specificity by taking the minimum of both. + #[inline] + fn min(mut self, other: Self) -> Self { + let spec = cmp::min(self, other); + self.0 = spec.0; + self.1 = spec.1; + self.2 = spec.2; + self.3 = self.3.saturating_add(other.3); + self + } +} + +// ---------------------------------------------------------------------------- +// Trait implementations +// ---------------------------------------------------------------------------- + +impl From for Specificity +where + T: AsTokens, +{ + fn from(value: T) -> Self { + value.into_specificity() + } +} + +// ---------------------------------------------------------------------------- + +impl PartialOrd for Specificity { + /// Orders two specificities. + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Specificity { + /// Orders two specificities. + #[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(l1.cmp(l2)) + } +} diff --git a/crates/zrx-id/src/id/specificity/convert.rs b/crates/zrx-id/src/id/specificity/convert.rs new file mode 100644 index 0000000..a6f0072 --- /dev/null +++ b/crates/zrx-id/src/id/specificity/convert.rs @@ -0,0 +1,125 @@ +// 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. + +// ---------------------------------------------------------------------------- + +//! Specificity conversion. + +use std::cmp; + +use super::segment::atom::{Character, Wildcard}; +use super::segment::convert::ToSegments; +use super::segment::{Atom, Segment, Segments}; +use super::Specificity; + +// ---------------------------------------------------------------------------- +// Traits +// ---------------------------------------------------------------------------- + +/// Computes the [`Specificity`]. +pub trait IntoSpecificity { + /// Computes the specificity of the value. + fn into_specificity(self) -> Specificity; +} + +// ---------------------------------------------------------------------------- +// Trait implementations +// ---------------------------------------------------------------------------- + +impl IntoSpecificity for Segments<'_> { + /// Computes the specificity of the segments set. + #[inline] + fn into_specificity(self) -> Specificity { + self.into_iter() + .map(IntoSpecificity::into_specificity) + .reduce(Specificity::sum) + .unwrap_or_default() + } +} + +impl IntoSpecificity for Segment<'_> { + /// Computes the specificity of the segment. + #[inline] + fn into_specificity(self) -> Specificity { + self.into_iter() + .map(IntoSpecificity::into_specificity) + .reduce(Specificity::min) + .unwrap_or_default() + } +} + +// ---------------------------------------------------------------------------- + +impl IntoSpecificity for Atom<'_> { + /// Computes the specificity of the atom. + #[inline] + fn into_specificity(self) -> Specificity { + match self { + Atom::Literal(literal) => { + let len = u16::try_from(literal.len()).unwrap_or(u16::MAX); + Specificity(1, 0, 0, len) + } + Atom::Wildcard(wildcard) => wildcard.into_specificity(), + Atom::Character(character) => character.into_specificity(), + Atom::Group(data) => data + .into_iter() + .map(IntoSpecificity::into_specificity) + .reduce(cmp::min) + .unwrap_or_default(), + } + } +} + +impl IntoSpecificity for Wildcard { + /// Computes the specificity of the wildcard. + #[inline] + fn into_specificity(self) -> Specificity { + match self { + Wildcard::Character => Specificity(0, 1, 0, 0), + Wildcard::Sequence => Specificity(0, 1, 0, 0), + Wildcard::Traversal => Specificity(0, 0, 1, 0), + } + } +} + +impl IntoSpecificity for Character<'_> { + /// Computes the specificity of the character. + #[inline] + fn into_specificity(self) -> Specificity { + Specificity(0, 1, 0, 1) + } +} + +// ---------------------------------------------------------------------------- +// Blanket implementations +// ---------------------------------------------------------------------------- + +impl IntoSpecificity for T +where + T: ToSegments, +{ + #[inline] + fn into_specificity(self) -> Specificity { + self.to_segments().into_specificity() + } +} diff --git a/crates/zrx-id/src/id/specificity/segment.rs b/crates/zrx-id/src/id/specificity/segment.rs new file mode 100644 index 0000000..c3913a6 --- /dev/null +++ b/crates/zrx-id/src/id/specificity/segment.rs @@ -0,0 +1,89 @@ +// 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. + +// ---------------------------------------------------------------------------- + +//! Segment. + +use std::fmt::{self, Display}; +use std::vec::IntoIter; + +pub mod atom; +pub mod convert; +mod segments; + +pub use atom::Atom; +pub use segments::Segments; + +// ---------------------------------------------------------------------------- +// Structs +// ---------------------------------------------------------------------------- + +/// Segment. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Segment<'a> { + /// Atoms. + atoms: Vec>, +} + +// ---------------------------------------------------------------------------- +// Trait implementations +// ---------------------------------------------------------------------------- + +impl<'a> FromIterator> for Segment<'a> { + /// Creates a segment from an iterator. + #[inline] + fn from_iter(iter: T) -> Self + where + T: IntoIterator>, + { + Self { + atoms: iter.into_iter().collect(), + } + } +} + +impl<'a> IntoIterator for Segment<'a> { + type Item = Atom<'a>; + type IntoIter = IntoIter; + + /// Creates a consuming iterator over the segment. + #[inline] + fn into_iter(self) -> Self::IntoIter { + self.atoms.into_iter() + } +} + +// ---------------------------------------------------------------------------- + +impl Display for Segment<'_> { + /// Formats the segment for display. + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + for atom in &self.atoms { + Display::fmt(atom, f)?; + } + + // No errors occurred + Ok(()) + } +} diff --git a/crates/zrx-id/src/id/specificity/segment/atom.rs b/crates/zrx-id/src/id/specificity/segment/atom.rs new file mode 100644 index 0000000..c37c185 --- /dev/null +++ b/crates/zrx-id/src/id/specificity/segment/atom.rs @@ -0,0 +1,88 @@ +// 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. + +// ---------------------------------------------------------------------------- + +//! Atom. + +use std::fmt::{self, Display, Write}; + +use super::segments::Segments; + +mod character; +mod wildcard; + +pub use character::Character; +pub use wildcard::Wildcard; + +// ---------------------------------------------------------------------------- +// Enums +// ---------------------------------------------------------------------------- + +/// Atom. +/// +/// Atoms are the basic building blocks of [`Segments`], representing literals, +/// wildcards, character classes and groups of alternatives. Each [`Segment`][] +/// contains a set of atoms that define which [`Specificity`][] the segment has, +/// where specificity is determined by the least specific atom in the segment. +/// +/// [`Segment`]: crate::id::specificity::segment::Segment +/// [`Specificity`]: crate::id::specificity::Specificity +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Atom<'a> { + /// Literal, e.g., `main.rs` + Literal(&'a str), + /// Wildcard, i.e., `?`, `*`, or `**`. + Wildcard(Wildcard), + /// Character class, e.g., `[xyz]`. + Character(Character<'a>), + /// Alternate group, e.g., `{*.rs,*.md}`. + Group(Vec>), +} + +// ---------------------------------------------------------------------------- +// Trait implementations +// ---------------------------------------------------------------------------- + +impl Display for Atom<'_> { + /// Formats the atom for display. + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Atom::Literal(literal) => Display::fmt(literal, f), + Atom::Wildcard(wildcard) => Display::fmt(wildcard, f), + Atom::Character(character) => Display::fmt(character, f), + Atom::Group(group) => { + f.write_char('{')?; + for (i, segments) in group.iter().enumerate() { + Display::fmt(&segments, f)?; + + // Write comma if not last + if i < group.len() - 1 { + f.write_char(',')?; + } + } + f.write_char('}') + } + } + } +} diff --git a/crates/zrx-id/src/id/specificity/segment/atom/character.rs b/crates/zrx-id/src/id/specificity/segment/atom/character.rs new file mode 100644 index 0000000..43d4004 --- /dev/null +++ b/crates/zrx-id/src/id/specificity/segment/atom/character.rs @@ -0,0 +1,63 @@ +// 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. + +// ---------------------------------------------------------------------------- + +//! Character class. + +use std::fmt::{self, Display, Write}; + +// ---------------------------------------------------------------------------- +// Enums +// ---------------------------------------------------------------------------- + +/// Character class. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Character<'a> { + /// Negation marker. + pub negate: bool, + /// Characters. + pub values: Vec<&'a str>, +} + +// ---------------------------------------------------------------------------- +// Trait implementations +// ---------------------------------------------------------------------------- + +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)?; + } + f.write_char(']') + } +} diff --git a/crates/zrx-id/src/id/specificity/segment/atom/wildcard.rs b/crates/zrx-id/src/id/specificity/segment/atom/wildcard.rs new file mode 100644 index 0000000..2fe189b --- /dev/null +++ b/crates/zrx-id/src/id/specificity/segment/atom/wildcard.rs @@ -0,0 +1,58 @@ +// 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. + +// ---------------------------------------------------------------------------- + +//! Wildcard. + +use std::fmt::{self, Display, Write}; + +// ---------------------------------------------------------------------------- +// Enums +// ---------------------------------------------------------------------------- + +/// Wildcard. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Wildcard { + /// Character, i.e. `?`. + Character, + /// Character sequence, i.e. `*`. + Sequence, + /// Traversal across path segments, i.e. `**`. + Traversal, +} + +// ---------------------------------------------------------------------------- +// Trait implementations +// ---------------------------------------------------------------------------- + +impl Display for Wildcard { + /// Formats the wildcard for display. + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Wildcard::Character => f.write_char('?'), + Wildcard::Sequence => f.write_char('*'), + Wildcard::Traversal => f.write_str("**"), + } + } +} diff --git a/crates/zrx-id/src/id/specificity/segment/convert.rs b/crates/zrx-id/src/id/specificity/segment/convert.rs new file mode 100644 index 0000000..8e11fc7 --- /dev/null +++ b/crates/zrx-id/src/id/specificity/segment/convert.rs @@ -0,0 +1,156 @@ +// 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. + +// ---------------------------------------------------------------------------- + +//! Segment set conversions. + +use std::iter::Peekable; + +use crate::id::specificity::tokens::{AsTokens, Token, Tokens}; + +use super::atom::{Atom, Character, Wildcard}; +use super::segments::Segments; +use super::Segment; + +// ---------------------------------------------------------------------------- +// Traits +// ---------------------------------------------------------------------------- + +/// Conversion to [`Segments`]. +pub trait ToSegments { + /// Converts to a segments set. + fn to_segments(&self) -> Segments<'_>; +} + +// ---------------------------------------------------------------------------- +// Blanket implementations +// ---------------------------------------------------------------------------- + +impl ToSegments for T +where + T: AsTokens, +{ + #[inline] + fn to_segments(&self) -> Segments<'_> { + parse(&mut self.as_tokens().peekable(), false) + } +} + +// ---------------------------------------------------------------------------- +// Type aliases +// ---------------------------------------------------------------------------- + +/// Peekable iterator over tokens. +type Iter<'a> = Peekable>; + +// ---------------------------------------------------------------------------- +// Functions +// ---------------------------------------------------------------------------- + +/// Parses a sequence of tokens into a segment set. +fn parse<'a>(iter: &mut Iter<'a>, group: bool) -> Segments<'a> { + let mut segments = Vec::new(); + + // Consume tokens until comma or group end if in group + while let Some(token) = iter.peek() { + match token { + Token::Comma | Token::GroupEnd if group => break, + _ => segments.push(parse_segment(iter, group)), + } + } + + // Return segment set + Segments::from_iter(segments) +} + +/// Parses a sequence of tokens into a segment. +fn parse_segment<'a>(iter: &mut Iter<'a>, group: bool) -> Segment<'a> { + let mut atoms = Vec::new(); + + // Consume tokens until comma or group end if in group + while let Some(token) = iter.peek() { + if group && matches!(token, Token::Comma | Token::GroupEnd) { + break; + } + + // Consume tokens until separator + atoms.push(match iter.next().expect("invariant") { + Token::Any => Atom::Wildcard(Wildcard::Character), + Token::Star => Atom::Wildcard(Wildcard::Sequence), + Token::StarStar => Atom::Wildcard(Wildcard::Traversal), + Token::CharacterStart => Atom::Character(parse_character(iter)), + Token::GroupStart => Atom::Group(parse_group(iter)), + Token::Separator => break, + other => Atom::Literal(other.as_str()), + }); + } + + // Return segment + Segment::from_iter(atoms) +} + +/// Parses a sequence of tokens into a character class. +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 { + Token::CharacterEnd => break, + other => other.as_str(), + }); + } + + // Return character class + Character { negate, values } +} + +/// Parses a sequence of tokens into a group of segments. +fn parse_group<'a>(iter: &mut Iter<'a>) -> Vec> { + let mut group = Vec::new(); + loop { + group.push(Segments::from_iter(parse(iter, true))); + match iter.peek() { + Some(Token::Comma) => iter.next(), + Some(Token::GroupEnd) => { + iter.next(); + break; + } + _ => break, + }; + } + + // Return group of segments + group +} diff --git a/crates/zrx-id/src/id/specificity/segment/segments.rs b/crates/zrx-id/src/id/specificity/segment/segments.rs new file mode 100644 index 0000000..793a1a9 --- /dev/null +++ b/crates/zrx-id/src/id/specificity/segment/segments.rs @@ -0,0 +1,94 @@ +// 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. + +// ---------------------------------------------------------------------------- + +//! Segment set. + +use std::fmt::{self, Display, Write}; +use std::vec::IntoIter; + +use super::Segment; + +// ---------------------------------------------------------------------------- +// Structs +// ---------------------------------------------------------------------------- + +/// Segment set. +/// +/// Segment sets are derived from strings that contain [`Glob`][] expressions, +/// where each [`Segment`] is separated by a `/`, and consists of atoms. +/// +/// [`Glob`]: globset::Glob +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Segments<'a> { + /// Vector of segments. + inner: Vec>, +} + +// ---------------------------------------------------------------------------- +// Trait implementations +// ---------------------------------------------------------------------------- + +impl<'a> FromIterator> for Segments<'a> { + /// Creates a segment set from an iterator. + #[inline] + fn from_iter(iter: T) -> Self + where + T: IntoIterator>, + { + Self { + inner: iter.into_iter().collect(), + } + } +} + +impl<'a> IntoIterator for Segments<'a> { + type Item = Segment<'a>; + type IntoIter = IntoIter; + + /// Creates a consuming iterator over the segment set. + #[inline] + fn into_iter(self) -> Self::IntoIter { + self.inner.into_iter() + } +} + +// ---------------------------------------------------------------------------- + +impl Display for Segments<'_> { + /// Formats the segment set for display. + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + for (i, segment) in self.inner.iter().enumerate() { + Display::fmt(segment, f)?; + + // Write separator if not last + if i < self.inner.len() - 1 { + f.write_char('/')?; + } + } + + // No errors occurred + Ok(()) + } +} diff --git a/crates/zrx-id/src/id/specificity/tokens.rs b/crates/zrx-id/src/id/specificity/tokens.rs new file mode 100644 index 0000000..0c744a3 --- /dev/null +++ b/crates/zrx-id/src/id/specificity/tokens.rs @@ -0,0 +1,190 @@ +// 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. + +// ---------------------------------------------------------------------------- + +//! Iterator over tokens. + +mod convert; + +pub use convert::AsTokens; + +// ---------------------------------------------------------------------------- +// Enums +// ---------------------------------------------------------------------------- + +/// Token. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Token<'a> { + /// Literal - `foo` + Literal(&'a str), + /// Dot - `.` + Dot, + /// Any character - `?` + Any, + /// Single asterisk - `*` + Star, + /// Double asterisk - `**` + StarStar, + /// Character class start - `[` + CharacterStart, + /// Character class end - `]` + CharacterEnd, + /// Exclamation mark - `!` + Exclamation, + /// Alternate group start - `{` + GroupStart, + /// Alternate group end - `}` + GroupEnd, + /// Comma - `,` + Comma, + /// Separator - `/` + Separator, +} + +// ---------------------------------------------------------------------------- +// Structs +// ---------------------------------------------------------------------------- + +/// Iterator over tokens. +/// +/// This data type provides an iterator over the tokens of a pattern string, +/// allowing to parse and analyze the structure of a [`Glob`][] to compute the +/// [`Specificity`][] for tie-breaking. Note that the validity of the pattern +/// is not checked, as this is the responsibility of the caller. +/// +/// [`Glob`]: globset::Glob +/// [`Specificity`]: crate::id::specificity::Specificity +pub struct Tokens<'a> { + /// Pattern. + value: &'a str, + /// Current index. + index: usize, +} + +// ---------------------------------------------------------------------------- +// Trait implementations +// ---------------------------------------------------------------------------- + +impl<'a> Token<'a> { + /// Returns the string representation. + #[inline] + pub fn as_str(&self) -> &'a str { + match self { + Token::Literal(literal) => literal, + Token::Dot => ".", + Token::Any => "?", + Token::Star => "*", + Token::StarStar => "**", + Token::CharacterStart => "[", + Token::CharacterEnd => "]", + Token::Exclamation => "!", + Token::GroupStart => "{", + Token::GroupEnd => "}", + Token::Comma => ",", + Token::Separator => "/", + } + } +} + +// ---------------------------------------------------------------------------- + +impl<'a> From<&'a str> for Tokens<'a> { + /// Creates an iterator over the tokens of a pattern. + #[inline] + fn from(value: &'a str) -> Self { + Self { value, index: 0 } + } +} + +// ---------------------------------------------------------------------------- + +impl<'a> Iterator for Tokens<'a> { + type Item = Token<'a>; + + /// Returns the next token. + /// + /// Note that this parser does not check the validity of a [`Glob`][] - it + /// assumes that the pattern has been parsed and is considered valid. This + /// means that specificity should only be computed for valid patterns, as + /// invalid patterns may lead to unexpected results. + /// + /// [`Glob`]: globset::Glob + fn next(&mut self) -> Option { + let value = self.value.as_bytes(); + + // Handle end of pattern + let start = self.index; + if start == value.len() { + return None; + } + + // Handle current character + self.index += 1; + match value[start] { + b'.' => Some(Token::Dot), + 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), + b'/' => Some(Token::Separator), + + // Consume a `*` or `**` + b'*' => { + if self.index < value.len() && value[self.index] == b'*' { + self.index += 1; + Some(Token::StarStar) + } else { + Some(Token::Star) + } + } + + // Consume a literal + _ => { + while self.index < value.len() { + if is_special(value[self.index]) { + break; + } + self.index += 1; + } + Some(Token::Literal(&self.value[start..self.index])) + } + } + } +} + +// ---------------------------------------------------------------------------- +// Functions +// ---------------------------------------------------------------------------- + +/// Returns whether the given character is a special character. +#[inline] +fn is_special(char: u8) -> bool { + matches!( + char, + b'.' | b'?' | b'*' | b'[' | b']' | b'!' | b'{' | b'}' | b',' | b'/' + ) +} diff --git a/crates/zrx-id/src/id/specificity/tokens/convert.rs b/crates/zrx-id/src/id/specificity/tokens/convert.rs new file mode 100644 index 0000000..f47aed1 --- /dev/null +++ b/crates/zrx-id/src/id/specificity/tokens/convert.rs @@ -0,0 +1,68 @@ +// 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. + +// ---------------------------------------------------------------------------- + +//! Iterator over tokens conversions. + +use std::borrow::Cow; + +use super::Tokens; + +// ---------------------------------------------------------------------------- +// Traits +// ---------------------------------------------------------------------------- + +/// Borrow as [`Tokens`]. +pub trait AsTokens { + /// Borrows as an iterator over tokens. + fn as_tokens(&self) -> Tokens<'_>; +} + +// ---------------------------------------------------------------------------- +// Trait implementations +// ---------------------------------------------------------------------------- + +impl AsTokens for &str { + /// Borrows as an iterator over tokens. + #[inline] + fn as_tokens(&self) -> Tokens<'_> { + Tokens::from(*self) + } +} + +impl AsTokens for String { + /// Borrows as an iterator over tokens. + #[inline] + fn as_tokens(&self) -> Tokens<'_> { + Tokens::from(self.as_str()) + } +} + +impl AsTokens for Cow<'_, str> { + /// Borrows as an iterator over tokens. + #[inline] + fn as_tokens(&self) -> Tokens<'_> { + Tokens::from(self.as_ref()) + } +} diff --git a/crates/zrx-id/src/lib.rs b/crates/zrx-id/src/lib.rs index 1d9fba7..53b394c 100644 --- a/crates/zrx-id/src/lib.rs +++ b/crates/zrx-id/src/lib.rs @@ -34,5 +34,6 @@ pub use id::filter::{self, Filter}; pub use id::format; pub use id::matcher::selector::{Selector, TryToSelector}; pub use id::matcher::{self, Matcher, Matches}; +pub use id::specificity::{self, Specificity}; pub use id::uri; pub use id::{Builder, Error, Id, Result, TryToId}; From c6fd98ec86e7f259222b40768144ee5ed9213d77 Mon Sep 17 00:00:00 2001 From: squidfunk Date: Mon, 23 Mar 2026 12:24:44 +0100 Subject: [PATCH 11/15] refactor: change `Specificity` computation to non-consuming Signed-off-by: squidfunk --- crates/zrx-id/src/id/specificity.rs | 20 +++--- crates/zrx-id/src/id/specificity/convert.rs | 61 ++++++++++++------- crates/zrx-id/src/id/specificity/segment.rs | 24 ++++++-- .../src/id/specificity/segment/convert.rs | 3 +- .../src/id/specificity/segment/segments.rs | 24 ++++++-- 5 files changed, 85 insertions(+), 47 deletions(-) diff --git a/crates/zrx-id/src/id/specificity.rs b/crates/zrx-id/src/id/specificity.rs index 2c1e91c..c821b73 100644 --- a/crates/zrx-id/src/id/specificity.rs +++ b/crates/zrx-id/src/id/specificity.rs @@ -31,8 +31,7 @@ pub mod convert; pub mod segment; mod tokens; -use convert::IntoSpecificity; -use tokens::AsTokens; +use convert::ToSpecificity; // ---------------------------------------------------------------------------- // Structs @@ -60,13 +59,10 @@ impl Specificity { /// Creates a specificity by taking the minimum of both. #[inline] - fn min(mut self, other: Self) -> Self { - let spec = cmp::min(self, other); - self.0 = spec.0; - self.1 = spec.1; - self.2 = spec.2; - self.3 = self.3.saturating_add(other.3); - self + fn min(self, other: Self) -> Self { + let mut spec = cmp::min(self, other); + spec.3 = self.3.saturating_add(other.3); + spec } } @@ -76,10 +72,12 @@ impl Specificity { impl From for Specificity where - T: AsTokens, + T: ToSpecificity, { + /// Creates a specificity from a value. + #[inline] fn from(value: T) -> Self { - value.into_specificity() + value.to_specificity() } } diff --git a/crates/zrx-id/src/id/specificity/convert.rs b/crates/zrx-id/src/id/specificity/convert.rs index a6f0072..2ce47b3 100644 --- a/crates/zrx-id/src/id/specificity/convert.rs +++ b/crates/zrx-id/src/id/specificity/convert.rs @@ -27,6 +27,8 @@ use std::cmp; +use crate::id::format::Format; + use super::segment::atom::{Character, Wildcard}; use super::segment::convert::ToSegments; use super::segment::{Atom, Segment, Segments}; @@ -37,32 +39,45 @@ use super::Specificity; // ---------------------------------------------------------------------------- /// Computes the [`Specificity`]. -pub trait IntoSpecificity { +pub trait ToSpecificity { /// Computes the specificity of the value. - fn into_specificity(self) -> Specificity; + fn to_specificity(&self) -> Specificity; } // ---------------------------------------------------------------------------- // Trait implementations // ---------------------------------------------------------------------------- -impl IntoSpecificity for Segments<'_> { +impl ToSpecificity for Format { + /// Computes the specificity of the formatted string. + #[inline] + fn to_specificity(&self) -> Specificity { + let iter = 0..N; + iter.map(|index| self.get(index).to_specificity()) + .reduce(Specificity::sum) + .unwrap_or_default() + } +} + +// ---------------------------------------------------------------------------- + +impl ToSpecificity for Segments<'_> { /// Computes the specificity of the segments set. #[inline] - fn into_specificity(self) -> Specificity { - self.into_iter() - .map(IntoSpecificity::into_specificity) + fn to_specificity(&self) -> Specificity { + self.iter() + .map(ToSpecificity::to_specificity) .reduce(Specificity::sum) .unwrap_or_default() } } -impl IntoSpecificity for Segment<'_> { +impl ToSpecificity for Segment<'_> { /// Computes the specificity of the segment. #[inline] - fn into_specificity(self) -> Specificity { - self.into_iter() - .map(IntoSpecificity::into_specificity) + fn to_specificity(&self) -> Specificity { + self.iter() + .map(ToSpecificity::to_specificity) .reduce(Specificity::min) .unwrap_or_default() } @@ -70,30 +85,30 @@ impl IntoSpecificity for Segment<'_> { // ---------------------------------------------------------------------------- -impl IntoSpecificity for Atom<'_> { +impl ToSpecificity for Atom<'_> { /// Computes the specificity of the atom. #[inline] - fn into_specificity(self) -> Specificity { + fn to_specificity(&self) -> Specificity { match self { Atom::Literal(literal) => { let len = u16::try_from(literal.len()).unwrap_or(u16::MAX); Specificity(1, 0, 0, len) } - Atom::Wildcard(wildcard) => wildcard.into_specificity(), - Atom::Character(character) => character.into_specificity(), + Atom::Wildcard(wildcard) => wildcard.to_specificity(), + Atom::Character(character) => character.to_specificity(), Atom::Group(data) => data - .into_iter() - .map(IntoSpecificity::into_specificity) + .iter() + .map(ToSpecificity::to_specificity) .reduce(cmp::min) .unwrap_or_default(), } } } -impl IntoSpecificity for Wildcard { +impl ToSpecificity for Wildcard { /// Computes the specificity of the wildcard. #[inline] - fn into_specificity(self) -> Specificity { + fn to_specificity(&self) -> Specificity { match self { Wildcard::Character => Specificity(0, 1, 0, 0), Wildcard::Sequence => Specificity(0, 1, 0, 0), @@ -102,10 +117,10 @@ impl IntoSpecificity for Wildcard { } } -impl IntoSpecificity for Character<'_> { +impl ToSpecificity for Character<'_> { /// Computes the specificity of the character. #[inline] - fn into_specificity(self) -> Specificity { + fn to_specificity(&self) -> Specificity { Specificity(0, 1, 0, 1) } } @@ -114,12 +129,12 @@ impl IntoSpecificity for Character<'_> { // Blanket implementations // ---------------------------------------------------------------------------- -impl IntoSpecificity for T +impl ToSpecificity for T where T: ToSegments, { #[inline] - fn into_specificity(self) -> Specificity { - self.to_segments().into_specificity() + fn to_specificity(&self) -> Specificity { + self.to_segments().to_specificity() } } diff --git a/crates/zrx-id/src/id/specificity/segment.rs b/crates/zrx-id/src/id/specificity/segment.rs index c3913a6..da852b6 100644 --- a/crates/zrx-id/src/id/specificity/segment.rs +++ b/crates/zrx-id/src/id/specificity/segment.rs @@ -26,7 +26,7 @@ //! Segment. use std::fmt::{self, Display}; -use std::vec::IntoIter; +use std::slice::Iter; pub mod atom; pub mod convert; @@ -46,6 +46,18 @@ pub struct Segment<'a> { atoms: Vec>, } +// ---------------------------------------------------------------------------- +// Implementations +// ---------------------------------------------------------------------------- + +impl Segment<'_> { + /// Creates an iterator over the atoms of the segment. + #[inline] + pub fn iter(&self) -> Iter<'_, Atom<'_>> { + self.atoms.iter() + } +} + // ---------------------------------------------------------------------------- // Trait implementations // ---------------------------------------------------------------------------- @@ -63,14 +75,14 @@ impl<'a> FromIterator> for Segment<'a> { } } -impl<'a> IntoIterator for Segment<'a> { - type Item = Atom<'a>; - type IntoIter = IntoIter; +impl<'a> IntoIterator for &'a Segment<'a> { + type Item = &'a Atom<'a>; + type IntoIter = Iter<'a, Atom<'a>>; - /// Creates a consuming iterator over the segment. + /// Creates an iterator over the atoms of the segment. #[inline] fn into_iter(self) -> Self::IntoIter { - self.atoms.into_iter() + 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 8e11fc7..b196cbc 100644 --- a/crates/zrx-id/src/id/specificity/segment/convert.rs +++ b/crates/zrx-id/src/id/specificity/segment/convert.rs @@ -51,6 +51,7 @@ impl ToSegments for T where T: AsTokens, { + /// Converts tokens to a segments set. #[inline] fn to_segments(&self) -> Segments<'_> { parse(&mut self.as_tokens().peekable(), false) @@ -140,7 +141,7 @@ fn parse_character<'a>(iter: &mut Iter<'a>) -> Character<'a> { fn parse_group<'a>(iter: &mut Iter<'a>) -> Vec> { let mut group = Vec::new(); loop { - group.push(Segments::from_iter(parse(iter, true))); + group.push(parse(iter, true)); match iter.peek() { Some(Token::Comma) => iter.next(), Some(Token::GroupEnd) => { diff --git a/crates/zrx-id/src/id/specificity/segment/segments.rs b/crates/zrx-id/src/id/specificity/segment/segments.rs index 793a1a9..db0fbfb 100644 --- a/crates/zrx-id/src/id/specificity/segment/segments.rs +++ b/crates/zrx-id/src/id/specificity/segment/segments.rs @@ -26,7 +26,7 @@ //! Segment set. use std::fmt::{self, Display, Write}; -use std::vec::IntoIter; +use std::slice::Iter; use super::Segment; @@ -46,6 +46,18 @@ pub struct Segments<'a> { inner: Vec>, } +// ---------------------------------------------------------------------------- +// Implementations +// ---------------------------------------------------------------------------- + +impl Segments<'_> { + /// Creates an iterator over the segment set. + #[inline] + pub fn iter(&self) -> Iter<'_, Segment<'_>> { + self.inner.iter() + } +} + // ---------------------------------------------------------------------------- // Trait implementations // ---------------------------------------------------------------------------- @@ -63,14 +75,14 @@ impl<'a> FromIterator> for Segments<'a> { } } -impl<'a> IntoIterator for Segments<'a> { - type Item = Segment<'a>; - type IntoIter = IntoIter; +impl<'a> IntoIterator for &'a Segments<'a> { + type Item = &'a Segment<'a>; + type IntoIter = Iter<'a, Segment<'a>>; - /// Creates a consuming iterator over the segment set. + /// Creates an iterator over the segment set. #[inline] fn into_iter(self) -> Self::IntoIter { - self.inner.into_iter() + self.iter() } } From 38b3ccd4d64aae71c7f92492c435cfc6acb56398 Mon Sep 17 00:00:00 2001 From: squidfunk Date: Mon, 23 Mar 2026 12:39:25 +0100 Subject: [PATCH 12/15] feature: add `AsRef` impl to `Id` and `Selector` to obtain `Format` Signed-off-by: squidfunk --- crates/zrx-id/src/id.rs | 17 +++++++++++++++++ crates/zrx-id/src/id/matcher/selector.rs | 17 +++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/crates/zrx-id/src/id.rs b/crates/zrx-id/src/id.rs index f7d6418..9d24b76 100644 --- a/crates/zrx-id/src/id.rs +++ b/crates/zrx-id/src/id.rs @@ -262,6 +262,23 @@ impl Id { // Trait implementations // ---------------------------------------------------------------------------- +impl AsRef> for Id { + /// Returns the formatted string. + /// + /// Note that it's normally not necessary to access the formatted string + /// directly, as all components can be accessed via the respective methods. + /// We need to access the underlying formatted string in our internal APIs, + /// e.g., to compute the [`Specificity`][] for the given [`Id`]. + /// + /// [`Specificity`]: crate::id::specificity::Specificity + #[inline] + fn as_ref(&self) -> &Format<7> { + self.format.as_ref() + } +} + +// ---------------------------------------------------------------------------- + impl FromStr for Id { type Err = Error; diff --git a/crates/zrx-id/src/id/matcher/selector.rs b/crates/zrx-id/src/id/matcher/selector.rs index a6dcfb6..840fc08 100644 --- a/crates/zrx-id/src/id/matcher/selector.rs +++ b/crates/zrx-id/src/id/matcher/selector.rs @@ -185,6 +185,23 @@ impl Selector { // Trait implementations // ---------------------------------------------------------------------------- +impl AsRef> for Selector { + /// Returns the formatted string. + /// + /// Note that it's normally not necessary to access the formatted string + /// directly, as all components can be accessed via the respective methods. + /// We need to access the underlying formatted string in our internal APIs, + /// e.g., to compute the [`Specificity`][] for the given [`Selector`]. + /// + /// [`Specificity`]: crate::id::specificity::Specificity + #[inline] + fn as_ref(&self) -> &Format<7> { + self.format.as_ref() + } +} + +// ---------------------------------------------------------------------------- + impl FromStr for Selector { type Err = Error; From a3ea5eb46bce88c0807c84c77fb19800d77f9eef Mon Sep 17 00:00:00 2001 From: squidfunk Date: Mon, 23 Mar 2026 12:39:54 +0100 Subject: [PATCH 13/15] feature: add `ToSpecificity` impl for `Id` and `Selector` Signed-off-by: squidfunk --- crates/zrx-id/src/id/specificity/convert.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/crates/zrx-id/src/id/specificity/convert.rs b/crates/zrx-id/src/id/specificity/convert.rs index 2ce47b3..7b36934 100644 --- a/crates/zrx-id/src/id/specificity/convert.rs +++ b/crates/zrx-id/src/id/specificity/convert.rs @@ -28,6 +28,8 @@ use std::cmp; use crate::id::format::Format; +use crate::id::matcher::selector::Selector; +use crate::id::Id; use super::segment::atom::{Character, Wildcard}; use super::segment::convert::ToSegments; @@ -48,6 +50,24 @@ pub trait ToSpecificity { // Trait implementations // ---------------------------------------------------------------------------- +impl ToSpecificity for Id { + /// Computes the specificity of the identifier. + #[inline] + fn to_specificity(&self) -> Specificity { + self.as_ref().to_specificity() + } +} + +impl ToSpecificity for Selector { + /// Computes the specificity of the selector. + #[inline] + fn to_specificity(&self) -> Specificity { + self.as_ref().to_specificity() + } +} + +// ---------------------------------------------------------------------------- + impl ToSpecificity for Format { /// Computes the specificity of the formatted string. #[inline] From b451d42a775d87ffd403f859430119de18eb236a Mon Sep 17 00:00:00 2001 From: squidfunk Date: Mon, 23 Mar 2026 13:20:28 +0100 Subject: [PATCH 14/15] feature: add `ToSpecificity` impl for `Term` and `Expression` Signed-off-by: squidfunk --- crates/zrx-id/src/id/specificity.rs | 20 ++++----- crates/zrx-id/src/id/specificity/convert.rs | 45 +++++++++++++++++++-- 2 files changed, 52 insertions(+), 13 deletions(-) diff --git a/crates/zrx-id/src/id/specificity.rs b/crates/zrx-id/src/id/specificity.rs index c821b73..9b89fff 100644 --- a/crates/zrx-id/src/id/specificity.rs +++ b/crates/zrx-id/src/id/specificity.rs @@ -46,9 +46,17 @@ pub struct Specificity(u16, u16, u16, u16); // ---------------------------------------------------------------------------- impl Specificity { - /// Creates the sum of both specificities. + /// Returns the minimum of both specificities. #[inline] - fn sum(self, other: Self) -> Self { + fn any(self, other: Self) -> Self { + let mut spec = cmp::min(self, other); + spec.3 = self.3.saturating_add(other.3); + spec + } + + /// Returns the sum of both specificities. + #[inline] + fn all(self, other: Self) -> Self { Self( self.0 + other.0, self.1 + other.1, @@ -56,14 +64,6 @@ impl Specificity { self.3 + other.3, ) } - - /// Creates a specificity by taking the minimum of both. - #[inline] - fn min(self, other: Self) -> Self { - let mut spec = cmp::min(self, other); - spec.3 = self.3.saturating_add(other.3); - spec - } } // ---------------------------------------------------------------------------- diff --git a/crates/zrx-id/src/id/specificity/convert.rs b/crates/zrx-id/src/id/specificity/convert.rs index 7b36934..697dc25 100644 --- a/crates/zrx-id/src/id/specificity/convert.rs +++ b/crates/zrx-id/src/id/specificity/convert.rs @@ -27,6 +27,8 @@ use std::cmp; +use crate::id::filter::expression::{Operand, Operator}; +use crate::id::filter::{Expression, Term}; use crate::id::format::Format; use crate::id::matcher::selector::Selector; use crate::id::Id; @@ -50,6 +52,43 @@ pub trait ToSpecificity { // Trait implementations // ---------------------------------------------------------------------------- +impl ToSpecificity for Expression { + /// Computes the specificity of the expression. + #[inline] + fn to_specificity(&self) -> Specificity { + let iter = self.operands().iter().map(ToSpecificity::to_specificity); + match self.operator() { + Operator::Any => iter.reduce(Specificity::any).unwrap_or_default(), + Operator::All => iter.reduce(Specificity::all).unwrap_or_default(), + Operator::Not => Specificity::default(), + } + } +} + +impl ToSpecificity for Term { + /// Computes the specificity of the term. + #[inline] + fn to_specificity(&self) -> Specificity { + match self { + Term::Id(id) => id.to_specificity(), + Term::Selector(selector) => selector.to_specificity(), + } + } +} + +impl ToSpecificity for Operand { + /// Computes the specificity of the operand. + #[inline] + fn to_specificity(&self) -> Specificity { + match self { + Operand::Expression(expr) => expr.to_specificity(), + Operand::Term(term) => term.to_specificity(), + } + } +} + +// ---------------------------------------------------------------------------- + impl ToSpecificity for Id { /// Computes the specificity of the identifier. #[inline] @@ -74,7 +113,7 @@ impl ToSpecificity for Format { fn to_specificity(&self) -> Specificity { let iter = 0..N; iter.map(|index| self.get(index).to_specificity()) - .reduce(Specificity::sum) + .reduce(Specificity::all) .unwrap_or_default() } } @@ -87,7 +126,7 @@ impl ToSpecificity for Segments<'_> { fn to_specificity(&self) -> Specificity { self.iter() .map(ToSpecificity::to_specificity) - .reduce(Specificity::sum) + .reduce(Specificity::all) .unwrap_or_default() } } @@ -98,7 +137,7 @@ impl ToSpecificity for Segment<'_> { fn to_specificity(&self) -> Specificity { self.iter() .map(ToSpecificity::to_specificity) - .reduce(Specificity::min) + .reduce(Specificity::any) .unwrap_or_default() } } From 9e6eaa2e23aeee6c4dc2cc0af90389b2c9b2eb69 Mon Sep 17 00:00:00 2001 From: squidfunk Date: Mon, 23 Mar 2026 13:36:49 +0100 Subject: [PATCH 15/15] fix: computed `Specificity` for `Expression::any` not aligned Signed-off-by: squidfunk --- crates/zrx-id/src/id/specificity.rs | 29 +++++++++++--------- crates/zrx-id/src/id/specificity/convert.rs | 30 ++++++++------------- 2 files changed, 27 insertions(+), 32 deletions(-) diff --git a/crates/zrx-id/src/id/specificity.rs b/crates/zrx-id/src/id/specificity.rs index 9b89fff..1fbf85c 100644 --- a/crates/zrx-id/src/id/specificity.rs +++ b/crates/zrx-id/src/id/specificity.rs @@ -46,23 +46,26 @@ pub struct Specificity(u16, u16, u16, u16); // ---------------------------------------------------------------------------- impl Specificity { - /// Returns the minimum of both specificities. + /// Computes the sum of both specificities. #[inline] - fn any(self, other: Self) -> Self { - let mut spec = cmp::min(self, other); - spec.3 = self.3.saturating_add(other.3); - spec + 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) } - /// Returns the sum of both specificities. + /// Computes the minimum of both specificities. #[inline] - fn all(self, other: Self) -> Self { - Self( - self.0 + other.0, - self.1 + other.1, - self.2 + other.2, - self.3 + other.3, - ) + fn min(self, other: Self) -> Self { + cmp::min(self, other) + } + + /// Computes the minimum of both specificities while summing their lengths. + #[inline] + fn min_sum_len(self, other: Self) -> Self { + let mut spec = cmp::min(self, other); + spec.3 = self.3.saturating_add(other.3); + spec } } diff --git a/crates/zrx-id/src/id/specificity/convert.rs b/crates/zrx-id/src/id/specificity/convert.rs index 697dc25..d7da07b 100644 --- a/crates/zrx-id/src/id/specificity/convert.rs +++ b/crates/zrx-id/src/id/specificity/convert.rs @@ -29,7 +29,6 @@ use std::cmp; use crate::id::filter::expression::{Operand, Operator}; use crate::id::filter::{Expression, Term}; -use crate::id::format::Format; use crate::id::matcher::selector::Selector; use crate::id::Id; @@ -58,8 +57,8 @@ impl ToSpecificity for Expression { fn to_specificity(&self) -> Specificity { let iter = self.operands().iter().map(ToSpecificity::to_specificity); match self.operator() { - Operator::Any => iter.reduce(Specificity::any).unwrap_or_default(), - Operator::All => iter.reduce(Specificity::all).unwrap_or_default(), + Operator::Any => iter.reduce(Specificity::min).unwrap_or_default(), + Operator::All => iter.reduce(Specificity::sum).unwrap_or_default(), Operator::Not => Specificity::default(), } } @@ -93,7 +92,10 @@ impl ToSpecificity for Id { /// Computes the specificity of the identifier. #[inline] fn to_specificity(&self) -> Specificity { - self.as_ref().to_specificity() + let iter = 1..7; + iter.map(|index| self.as_ref().get(index).to_specificity()) + .reduce(Specificity::sum) + .unwrap_or_default() } } @@ -101,19 +103,9 @@ impl ToSpecificity for Selector { /// Computes the specificity of the selector. #[inline] fn to_specificity(&self) -> Specificity { - self.as_ref().to_specificity() - } -} - -// ---------------------------------------------------------------------------- - -impl ToSpecificity for Format { - /// Computes the specificity of the formatted string. - #[inline] - fn to_specificity(&self) -> Specificity { - let iter = 0..N; - iter.map(|index| self.get(index).to_specificity()) - .reduce(Specificity::all) + let iter = 1..7; + iter.map(|index| self.as_ref().get(index).to_specificity()) + .reduce(Specificity::sum) .unwrap_or_default() } } @@ -126,7 +118,7 @@ impl ToSpecificity for Segments<'_> { fn to_specificity(&self) -> Specificity { self.iter() .map(ToSpecificity::to_specificity) - .reduce(Specificity::all) + .reduce(Specificity::sum) .unwrap_or_default() } } @@ -137,7 +129,7 @@ impl ToSpecificity for Segment<'_> { fn to_specificity(&self) -> Specificity { self.iter() .map(ToSpecificity::to_specificity) - .reduce(Specificity::any) + .reduce(Specificity::min_sum_len) .unwrap_or_default() } }