Skip to main content

zrx_id/id/
specificity.rs

1// Copyright (c) 2025-2026 Zensical and contributors
2
3// SPDX-License-Identifier: MIT
4// All contributions are certified under the DCO
5
6// Permission is hereby granted, free of charge, to any person obtaining a copy
7// of this software and associated documentation files (the `Software`), to
8// deal in the Software without restriction, including without limitation the
9// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
10// sell copies of the Software, and to permit persons to whom the Software is
11// furnished to do so, subject to the following conditions:
12
13// The above copyright notice and this permission notice shall be included in
14// all copies or substantial portions of the Software.
15
16// THE SOFTWARE IS PROVIDED `AS IS`, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
19// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
22// IN THE SOFTWARE.
23
24// ----------------------------------------------------------------------------
25
26//! Specificity.
27
28use std::cmp::{self, Ordering};
29
30mod convert;
31pub mod segment;
32mod specified;
33mod tokens;
34
35pub use convert::ToSpecificity;
36pub use specified::Specified;
37
38// ----------------------------------------------------------------------------
39// Structs
40// ----------------------------------------------------------------------------
41
42/// Specificity.
43///
44/// Specificity is an ordering and tie-breaking concept borrowed from CSS, where
45/// more specific selectors take precedence over less specific ones. Specificity
46/// is computable for the likes of [`Expression`][], [`Term`][], [`Operand`][],
47/// [`Id`][], [`Selector`][], and [`Glob`][].
48///
49/// # Representation
50///
51/// Specificity is represented as a 4-tuple `(a, b, c, l)`, which is compared
52/// in lexicographic order, meaning that components are compared in sequence:
53///
54/// - `a` – number of segments with literals only, e.g. `src`, `main.rs`.
55/// - `b` – number of segments with single-wildcards, e.g. `*`, `?`, `[abc]`.
56/// - `c` – number of segments with double-wildcards, compared in reverse.
57/// - `l` – number of literals across all segments.
58///
59/// # Atoms
60///
61/// In a [`Segment`][], atoms are combined with [`Specificity::min_sum_len`] -
62/// the structural component `a`, `b`, or `c` is assigned by taking the minimum
63/// across all atoms in the segment, whereas the length component `l` receives
64/// the sum across all atoms in the segment.
65///
66/// # Ids and selectors
67///
68/// The specificity of an [`Id`][] or [`Selector`][] is computed by summing the
69/// specificities of its components, where the specificity of each component is
70/// computed individually and then combined with [`Specificity::sum`][]. Empty
71/// components receive the [`Specificity::default`], which is `(0, 0, 0, 0)`.
72///
73/// ``` sh
74/// zrs:{git,file}:::{docs}:index.md: # (3, 0, 0, 15)
75/// zrs::::docs:{index,about}.md:     # (2, 0, 0, 12)
76/// zrs:::::index.{md,rst}:           # (1, 0, 0, 8)
77/// zrs:::::{*}:                      # (0, 1, 0, 0)
78/// ```
79///
80/// # Expressions
81///
82/// An [`Expression`][] is a combination of multiple [`Id`][] and [`Selector`][]
83/// terms, with its specificity computed according to its [`Operator`][]:
84///
85/// - [`Expression::any`][]: takes the minimum. The expression is as specific
86///   as its least specific operand, since any operand can match.
87///
88/// - [`Expression::all`][]: sums specificities. The expression is as specific
89///   as the combination of all its operands, since all operands must match.
90///
91/// - [`Expression::not`][]: contributes nothing, i.e., `(0, 0, 0, 0)`, since
92///   a negation is a guard that filters matches but does not select them.
93///
94/// Alternate groups, e.g. `{jpg,png}`, are equivalent to [`Expression::any`][]
95/// at the [`Atom`][] level and follow the same rules.
96///
97/// [`Atom`]: crate::id::specificity::segment::Atom
98/// [`Expression`]: crate::id::expression::Expression
99/// [`Expression::all`]: crate::id::expression::Expression::all
100/// [`Expression::any`]: crate::id::expression::Expression::any
101/// [`Expression::not`]: crate::id::expression::Expression::not
102/// [`Glob`]: globset::Glob
103/// [`Id`]: crate::id::Id
104/// [`Operand`]: crate::id::expression::Operand
105/// [`Operator`]: crate::id::expression::Operator
106/// [`Segment`]: crate::id::specificity::segment::Segment
107/// [`Selector`]: crate::id::selector::Selector
108/// [`Term`]: crate::id::expression::Term
109///
110/// # Examples
111///
112/// ```
113/// # use std::error::Error;
114/// # fn main() -> Result<(), Box<dyn Error>> {
115/// use zrx_id::specificity::ToSpecificity;
116/// use zrx_id::{selector, Expression};
117///
118/// // Create expression and compute specificity
119/// let expr = Expression::any(|expr| {
120///     expr.with(selector!(location = "**/*.jpg")?)?
121///         .with(selector!(location = "**/*.png")?)
122/// })?;
123/// assert_eq!(expr.to_specificity(), (0, 1, 1, 4).into());
124/// # Ok(())
125/// # }
126/// ```
127#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
128pub struct Specificity(u16, u16, u16, u16);
129
130// ----------------------------------------------------------------------------
131// Implementations
132// ----------------------------------------------------------------------------
133
134impl Specificity {
135    /// Computes the sum of both specificities.
136    #[inline]
137    fn sum(self, other: Self) -> Self {
138        let Specificity(a1, b1, c1, l1) = self;
139        let Specificity(a2, b2, c2, l2) = other;
140        Self(
141            a1.saturating_add(a2),
142            b1.saturating_add(b2),
143            c1.saturating_add(c2),
144            l1.saturating_add(l2),
145        )
146    }
147
148    /// Computes the minimum of both specificities.
149    #[inline]
150    fn min(self, other: Self) -> Self {
151        cmp::min(self, other)
152    }
153
154    /// Computes the minimum of both specificities, summing their lengths.
155    #[inline]
156    fn min_sum_len(self, other: Self) -> Self {
157        let mut spec = cmp::min(self, other);
158        spec.3 = self.3.saturating_add(other.3);
159        spec
160    }
161}
162
163// ----------------------------------------------------------------------------
164// Trait implementations
165// ----------------------------------------------------------------------------
166
167impl<T> From<T> for Specificity
168where
169    T: ToSpecificity,
170{
171    /// Creates a specificity from a value.
172    #[inline]
173    fn from(value: T) -> Self {
174        value.to_specificity()
175    }
176}
177
178impl From<(u16, u16, u16, u16)> for Specificity {
179    /// Creates a specificity from a tuple.
180    #[inline]
181    fn from((a, b, c, l): (u16, u16, u16, u16)) -> Self {
182        Self(a, b, c, l)
183    }
184}
185
186// ----------------------------------------------------------------------------
187
188impl PartialOrd for Specificity {
189    /// Orders two specificities.
190    ///
191    /// # Examples
192    ///
193    /// ```
194    /// # use std::error::Error;
195    /// # fn main() -> Result<(), Box<dyn Error>> {
196    /// use zrx_id::selector;
197    /// use zrx_id::specificity::ToSpecificity;
198    ///
199    /// // Create and compare selectors by specificity
200    /// let a = selector!(location = "**/*.md")?;
201    /// let b = selector!(location = "*.md")?;
202    /// assert!(a.to_specificity() < b.to_specificity());
203    /// # Ok(())
204    /// # }
205    /// ```
206    #[inline]
207    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
208        Some(self.cmp(other))
209    }
210}
211
212impl Ord for Specificity {
213    /// Orders two specificities.
214    ///
215    /// # Examples
216    ///
217    /// ```
218    /// # use std::error::Error;
219    /// # fn main() -> Result<(), Box<dyn Error>> {
220    /// use zrx_id::selector;
221    /// use zrx_id::specificity::ToSpecificity;
222    ///
223    /// // Create and compare selectors by specificity
224    /// let a = selector!(location = "**/*.md")?;
225    /// let b = selector!(location = "*.md")?;
226    /// assert!(a.to_specificity() < b.to_specificity());
227    /// # Ok(())
228    /// # }
229    /// ```
230    #[inline]
231    fn cmp(&self, other: &Self) -> Ordering {
232        let Specificity(a1, b1, c1, l1) = self;
233        let Specificity(a2, b2, c2, l2) = other;
234
235        // An all-zero specificity is the least specific and must always be the
236        // first in order, so check if this applies to any of the specificities
237        if a1 | b1 | c1 | l1 == 0 {
238            return Ordering::Less;
239        }
240        if a2 | b2 | c2 | l2 == 0 {
241            return Ordering::Greater;
242        }
243
244        // Otherwise, compare each component, where `c` is reversed since fewer
245        // double-wildcards is more specific than more double-wildcards
246        a1.cmp(a2)
247            .then_with(|| b1.cmp(b2))
248            .then_with(|| c2.cmp(c1))
249            .then_with(|| l1.cmp(l2))
250    }
251}