diff --git a/crates/zrx-graph/src/graph.rs b/crates/zrx-graph/src/graph.rs index 499a9a52..f8116336 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/operator.rs b/crates/zrx-graph/src/graph/operator.rs index c4dfcfc8..e4d9bf09 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 0d35eec7..5557d963 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], diff --git a/crates/zrx-graph/src/graph/traversal.rs b/crates/zrx-graph/src/graph/traversal.rs index 6996218f..9238027e 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 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. + /// + /// - 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 { @@ -368,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 { @@ -503,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 ); } @@ -537,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 ); } diff --git a/crates/zrx-id/src/id.rs b/crates/zrx-id/src/id.rs index 3ff647aa..9d24b76c 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; @@ -261,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; @@ -367,7 +385,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 } } @@ -417,13 +437,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) } } diff --git a/crates/zrx-id/src/id/filter/builder.rs b/crates/zrx-id/src/id/filter/builder.rs index bd7ef1ad..4a3c468c 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. diff --git a/crates/zrx-id/src/id/matcher/selector.rs b/crates/zrx-id/src/id/matcher/selector.rs index a5aad64b..840fc086 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>, @@ -184,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; @@ -343,7 +361,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 } } @@ -351,6 +371,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 { diff --git a/crates/zrx-id/src/id/specificity.rs b/crates/zrx-id/src/id/specificity.rs new file mode 100644 index 00000000..1fbf85c1 --- /dev/null +++ b/crates/zrx-id/src/id/specificity.rs @@ -0,0 +1,108 @@ +// 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::ToSpecificity; + +// ---------------------------------------------------------------------------- +// Structs +// ---------------------------------------------------------------------------- + +/// Specificity. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct Specificity(u16, u16, u16, u16); + +// ---------------------------------------------------------------------------- +// Implementations +// ---------------------------------------------------------------------------- + +impl Specificity { + /// Computes the sum of both specificities. + #[inline] + 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) + } + + /// Computes the minimum of both specificities. + #[inline] + 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 + } +} + +// ---------------------------------------------------------------------------- +// Trait implementations +// ---------------------------------------------------------------------------- + +impl From for Specificity +where + T: ToSpecificity, +{ + /// Creates a specificity from a value. + #[inline] + fn from(value: T) -> Self { + value.to_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 00000000..d7da07ba --- /dev/null +++ b/crates/zrx-id/src/id/specificity/convert.rs @@ -0,0 +1,191 @@ +// 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 crate::id::filter::expression::{Operand, Operator}; +use crate::id::filter::{Expression, Term}; +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::Specificity; + +// ---------------------------------------------------------------------------- +// Traits +// ---------------------------------------------------------------------------- + +/// Computes the [`Specificity`]. +pub trait ToSpecificity { + /// Computes the specificity of the value. + fn to_specificity(&self) -> Specificity; +} + +// ---------------------------------------------------------------------------- +// 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::min).unwrap_or_default(), + Operator::All => iter.reduce(Specificity::sum).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] + fn to_specificity(&self) -> Specificity { + let iter = 1..7; + iter.map(|index| self.as_ref().get(index).to_specificity()) + .reduce(Specificity::sum) + .unwrap_or_default() + } +} + +impl ToSpecificity for Selector { + /// Computes the specificity of the selector. + #[inline] + fn to_specificity(&self) -> Specificity { + let iter = 1..7; + iter.map(|index| self.as_ref().get(index).to_specificity()) + .reduce(Specificity::sum) + .unwrap_or_default() + } +} + +// ---------------------------------------------------------------------------- + +impl ToSpecificity for Segments<'_> { + /// Computes the specificity of the segments set. + #[inline] + fn to_specificity(&self) -> Specificity { + self.iter() + .map(ToSpecificity::to_specificity) + .reduce(Specificity::sum) + .unwrap_or_default() + } +} + +impl ToSpecificity for Segment<'_> { + /// Computes the specificity of the segment. + #[inline] + fn to_specificity(&self) -> Specificity { + self.iter() + .map(ToSpecificity::to_specificity) + .reduce(Specificity::min_sum_len) + .unwrap_or_default() + } +} + +// ---------------------------------------------------------------------------- + +impl ToSpecificity for Atom<'_> { + /// Computes the specificity of the atom. + #[inline] + 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.to_specificity(), + Atom::Character(character) => character.to_specificity(), + Atom::Group(data) => data + .iter() + .map(ToSpecificity::to_specificity) + .reduce(cmp::min) + .unwrap_or_default(), + } + } +} + +impl ToSpecificity for Wildcard { + /// Computes the specificity of the wildcard. + #[inline] + fn to_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 ToSpecificity for Character<'_> { + /// Computes the specificity of the character. + #[inline] + fn to_specificity(&self) -> Specificity { + Specificity(0, 1, 0, 1) + } +} + +// ---------------------------------------------------------------------------- +// Blanket implementations +// ---------------------------------------------------------------------------- + +impl ToSpecificity for T +where + T: ToSegments, +{ + #[inline] + 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 new file mode 100644 index 00000000..da852b6a --- /dev/null +++ b/crates/zrx-id/src/id/specificity/segment.rs @@ -0,0 +1,101 @@ +// 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::slice::Iter; + +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>, +} + +// ---------------------------------------------------------------------------- +// Implementations +// ---------------------------------------------------------------------------- + +impl Segment<'_> { + /// Creates an iterator over the atoms of the segment. + #[inline] + pub fn iter(&self) -> Iter<'_, Atom<'_>> { + self.atoms.iter() + } +} + +// ---------------------------------------------------------------------------- +// 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 &'a Segment<'a> { + type Item = &'a Atom<'a>; + type IntoIter = Iter<'a, Atom<'a>>; + + /// Creates an iterator over the atoms of the segment. + #[inline] + fn into_iter(self) -> Self::IntoIter { + self.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 00000000..c37c1852 --- /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 00000000..43d40047 --- /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 00000000..2fe189bb --- /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 00000000..b196cbc8 --- /dev/null +++ b/crates/zrx-id/src/id/specificity/segment/convert.rs @@ -0,0 +1,157 @@ +// 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, +{ + /// Converts tokens to a segments set. + #[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(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 00000000..db0fbfb8 --- /dev/null +++ b/crates/zrx-id/src/id/specificity/segment/segments.rs @@ -0,0 +1,106 @@ +// 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::slice::Iter; + +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>, +} + +// ---------------------------------------------------------------------------- +// Implementations +// ---------------------------------------------------------------------------- + +impl Segments<'_> { + /// Creates an iterator over the segment set. + #[inline] + pub fn iter(&self) -> Iter<'_, Segment<'_>> { + self.inner.iter() + } +} + +// ---------------------------------------------------------------------------- +// 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 &'a Segments<'a> { + type Item = &'a Segment<'a>; + type IntoIter = Iter<'a, Segment<'a>>; + + /// Creates an iterator over the segment set. + #[inline] + fn into_iter(self) -> Self::IntoIter { + self.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 00000000..0c744a3c --- /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 00000000..f47aed14 --- /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 1d9fba75..53b394c8 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};