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}