Skip to main content

zrx_id/id/
selector.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//! Selector.
27
28use ahash::AHasher;
29use std::borrow::Cow;
30use std::cmp::Ordering;
31use std::fmt::{self, Debug, Display};
32use std::hash::{Hash, Hasher};
33use std::str::FromStr;
34
35use super::expression::Term;
36use super::format::Format;
37use super::{Error, Id, Result};
38
39mod builder;
40mod convert;
41mod macros;
42
43pub use builder::Builder;
44pub use convert::TryToSelector;
45
46// ----------------------------------------------------------------------------
47// Structs
48// ----------------------------------------------------------------------------
49
50/// Selector.
51///
52/// Selectors are used to match identifiers. Like identifiers, they consist of
53/// six components, which can be set to specific values or left empty to act as
54/// wildcards. Each component can be set to a glob as supported by [`globset`],
55/// which allows for powerful matching capabilities.
56///
57/// Selectors are no means to an end, but rather a building block to associate
58/// data or functions to identifiers via the construction of a [`Matcher`][],
59/// which uses an efficient algorithm to match an arbitrary set of selectors in
60/// linear time. While it's recommended to use [`Selector::builder`] together
61/// with the associated methods to create a new selector, selectors can also be
62/// created from a structured string representation with [`Selector::from_str`],
63/// which is used internally for serializing them to persistent storage:
64///
65/// ``` text
66/// zrs:<provider>:<resource>:<variant>:<context>:<location>:<fragment>
67/// ```
68///
69/// This ensures blazing fast cloning and editing. Additionally, selectors are
70/// guaranteed to not contain backslashes or path traversals in components. An
71/// empty component is interpreted as a wildcard, and thus matches all values
72/// in that component for any given selector.
73///
74/// [`Matcher`]: crate::id::matcher::Matcher
75///
76/// # Examples
77///
78/// Create a selector:
79///
80/// ```
81/// # use std::error::Error;
82/// # fn main() -> Result<(), Box<dyn Error>> {
83/// use zrx_id::Selector;
84///
85/// // Create selector builder
86/// let builder = Selector::builder().location("**/*.md");
87///
88/// // Create selector from builder
89/// let selector = builder.build()?;
90/// assert_eq!(selector.as_str(), "zrs:::::**/*.md:");
91/// # Ok(())
92/// # }
93/// ```
94///
95/// Create a selector from a string:
96///
97/// ```
98/// # use std::error::Error;
99/// # fn main() -> Result<(), Box<dyn Error>> {
100/// use zrx_id::Selector;
101///
102/// // Create selector from string
103/// let selector: Selector = "zrs:::::**/*.md:".parse()?;
104/// # Ok(())
105/// # }
106/// ```
107#[derive(Clone)]
108pub struct Selector {
109    /// Formatted string.
110    format: Format<7>,
111    /// Precomputed hash.
112    hash: u64,
113}
114
115// ----------------------------------------------------------------------------
116// Implementations
117// ----------------------------------------------------------------------------
118
119impl Selector {
120    /// Returns the string representation.
121    ///
122    /// # Examples
123    ///
124    /// ```
125    /// # use std::error::Error;
126    /// # fn main() -> Result<(), Box<dyn Error>> {
127    /// use zrx_id::Selector;
128    ///
129    /// // Create selector from string
130    /// let selector: Selector = "zrs:::::**/*.md:".parse()?;
131    ///
132    /// // Obtain string representation
133    /// assert_eq!(selector.as_str(), "zrs:::::**/*.md:");
134    /// # Ok(())
135    /// # }
136    /// ```
137    #[inline]
138    #[must_use]
139    pub fn as_str(&self) -> &str {
140        self.format.as_str()
141    }
142}
143
144#[allow(clippy::must_use_candidate)]
145impl Selector {
146    /// Returns the `provider` component, if any.
147    #[inline]
148    pub fn provider(&self) -> Option<Cow<'_, str>> {
149        Some(self.format.get(1)).filter(|value| !value.is_empty())
150    }
151
152    /// Returns the `resource` component, if any.
153    #[inline]
154    pub fn resource(&self) -> Option<Cow<'_, str>> {
155        Some(self.format.get(2)).filter(|value| !value.is_empty())
156    }
157
158    /// Returns the `variant` component, if any.
159    #[inline]
160    pub fn variant(&self) -> Option<Cow<'_, str>> {
161        Some(self.format.get(3)).filter(|value| !value.is_empty())
162    }
163
164    /// Returns the `context` component, if any.
165    #[inline]
166    pub fn context(&self) -> Option<Cow<'_, str>> {
167        Some(self.format.get(4)).filter(|value| !value.is_empty())
168    }
169
170    /// Returns the `location` component, if any.
171    #[inline]
172    pub fn location(&self) -> Option<Cow<'_, str>> {
173        Some(self.format.get(5)).filter(|value| !value.is_empty())
174    }
175
176    /// Returns the `fragment` component, if any.
177    #[inline]
178    pub fn fragment(&self) -> Option<Cow<'_, str>> {
179        Some(self.format.get(6)).filter(|value| !value.is_empty())
180    }
181}
182
183// ----------------------------------------------------------------------------
184// Trait implementations
185// ----------------------------------------------------------------------------
186
187impl AsRef<Format<7>> for Selector {
188    /// Returns the formatted string.
189    ///
190    /// Note that it's normally not necessary to access the formatted string
191    /// directly, as all components can be accessed via the respective methods.
192    /// We need to access the underlying formatted string in our internal APIs,
193    /// e.g., to compute the [`Specificity`][] for the given [`Selector`].
194    ///
195    /// [`Specificity`]: crate::id::specificity::Specificity
196    #[inline]
197    fn as_ref(&self) -> &Format<7> {
198        &self.format
199    }
200}
201
202// ----------------------------------------------------------------------------
203
204impl FromStr for Selector {
205    type Err = Error;
206
207    /// Attempts to create a selector from a string.
208    ///
209    /// The string must adhere to the following format and include exactly six
210    /// `:` separators, even if some components are empty. All components are
211    /// optional, which means they can be left empty, which is equivalent to
212    /// setting them to a `**` wildcard.
213    ///
214    /// ``` text
215    /// zrs:<provider>:<resource>:<variant>:<context>:<location>:<fragment>
216    /// ```
217    ///
218    /// # Errors
219    ///
220    /// Returns [`Error::Prefix`] if the prefix isn't `zrs`. Low-level format
221    /// errors are returned as part of [`Error::Format`].
222    ///
223    /// # Examples
224    ///
225    /// ```
226    /// # use std::error::Error;
227    /// # fn main() -> Result<(), Box<dyn Error>> {
228    /// use zrx_id::Selector;
229    ///
230    /// // Create selector from string
231    /// let selector: Selector = "zrs:::::**/*.md:".parse()?;
232    /// # Ok(())
233    /// # }
234    /// ```
235    fn from_str(value: &str) -> Result<Self> {
236        let format = Format::from_str(value)?;
237
238        // Ensure prefix is set
239        if format.get(0) != "zrs" {
240            Err(Error::Prefix)?;
241        }
242
243        // Precompute hash for fast hashing
244        let hash = {
245            let mut hasher = AHasher::default();
246            format.hash(&mut hasher);
247            hasher.finish()
248        };
249
250        // No errors occurred
251        Ok(Self { format, hash })
252    }
253}
254
255// ----------------------------------------------------------------------------
256
257impl TryFrom<Id> for Selector {
258    type Error = Error;
259
260    /// Attempts to create a selector from an identifier.
261    ///
262    /// An [`Id`] can be converted into a [`Selector`] because all identifiers
263    /// are also valid selectors, as they represent exact matches. However, the
264    /// reverse is not true, as selectors can contain wildcards, as well as
265    /// optional components, which identifiers cannot.
266    ///
267    /// # Examples
268    ///
269    /// ```
270    /// # use std::error::Error;
271    /// # fn main() -> Result<(), Box<dyn Error>> {
272    /// use zrx_id::{Id, Selector};
273    ///
274    /// // Create selector from identifier
275    /// let id: Id = "zri:file:::docs:index.md:".parse()?;
276    /// let selector: Selector = id.try_into()?;
277    /// # Ok(())
278    /// # }
279    /// ```
280    #[inline]
281    fn try_from(id: Id) -> Result<Self> {
282        let format = id.format.to_builder().with(0, "zrs").build()?;
283
284        // Precompute hash for fast hashing
285        let hash = {
286            let mut hasher = AHasher::default();
287            format.hash(&mut hasher);
288            hasher.finish()
289        };
290
291        // No errors occurred
292        Ok(Self { format, hash })
293    }
294}
295
296impl TryFrom<Term> for Selector {
297    type Error = Error;
298
299    /// Attempts to create a selector from a term.
300    ///
301    /// # Examples
302    ///
303    /// ```
304    /// # use std::error::Error;
305    /// # fn main() -> Result<(), Box<dyn Error>> {
306    /// use zrx_id::expression::Term;
307    /// use zrx_id::{Id, Selector};
308    ///
309    /// // Create selector from identifier
310    /// let id: Id = "zri:file:::docs:index.md:".parse()?;
311    /// let selector: Selector = Term::from(id).try_into()?;
312    /// # Ok(())
313    /// # }
314    /// ```
315    #[inline]
316    fn try_from(term: Term) -> Result<Self> {
317        match term {
318            Term::Id(id) => id.try_into(),
319            Term::Selector(selector) => Ok(selector),
320        }
321    }
322}
323
324// ----------------------------------------------------------------------------
325
326impl Hash for Selector {
327    /// Hashes the selector.
328    ///
329    /// Since selectors are immutable, we can use a precomputed hash for fast
330    /// hashing. This is especially useful when selectors are used as keys in
331    /// hash maps or hash sets, where hashing is a frequent operation, as the
332    /// performance gains are significant.
333    #[inline]
334    fn hash<H>(&self, state: &mut H)
335    where
336        H: Hasher,
337    {
338        state.write_u64(self.hash);
339    }
340}
341
342// ----------------------------------------------------------------------------
343
344impl PartialEq for Selector {
345    /// Compares two selectors for equality.
346    ///
347    /// # Examples
348    ///
349    /// ```
350    /// # use std::error::Error;
351    /// # fn main() -> Result<(), Box<dyn Error>> {
352    /// use zrx_id::Selector;
353    ///
354    /// // Create and compare selectors
355    /// let a: Selector = "zrs:::::**/*.md:".parse()?;
356    /// let b: Selector = "zrs:::::**/*.md:".parse()?;
357    /// assert_eq!(a, b);
358    /// # Ok(())
359    /// # }
360    /// ```
361    #[inline]
362    fn eq(&self, other: &Self) -> bool {
363        // We first compare the precomputed hashes, which is extremely fast, as
364        // it saves us the comparison when the identifiers are different
365        self.hash == other.hash && self.format == other.format
366    }
367}
368
369impl Eq for Selector {}
370
371// ----------------------------------------------------------------------------
372
373impl PartialOrd for Selector {
374    /// Orders two selectors.
375    ///
376    /// # Examples
377    ///
378    /// ```
379    /// # use std::error::Error;
380    /// # fn main() -> Result<(), Box<dyn Error>> {
381    /// use zrx_id::Selector;
382    ///
383    /// // Create and compare selectors
384    /// let a: Selector = "zrs:::::**/*.md:".parse()?;
385    /// let b: Selector = "zrs:::::**/*.rs:".parse()?;
386    /// assert!(a < b);
387    /// # Ok(())
388    /// # }
389    /// ```
390    #[inline]
391    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
392        Some(self.cmp(other))
393    }
394}
395
396impl Ord for Selector {
397    /// Orders two selectors.
398    ///
399    /// # Examples
400    ///
401    /// ```
402    /// # use std::error::Error;
403    /// # fn main() -> Result<(), Box<dyn Error>> {
404    /// use zrx_id::Selector;
405    ///
406    /// // Create and compare selectors
407    /// let a: Selector = "zrs:::::**/*.md:".parse()?;
408    /// let b: Selector = "zrs:::::**/*.rs:".parse()?;
409    /// assert!(a < b);
410    /// # Ok(())
411    /// # }
412    /// ```
413    #[inline]
414    fn cmp(&self, other: &Self) -> Ordering {
415        self.format.cmp(&other.format)
416    }
417}
418
419// ----------------------------------------------------------------------------
420
421impl Display for Selector {
422    /// Formats the selector for display.
423    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
424        Display::fmt(&self.format, f)
425    }
426}
427
428impl Debug for Selector {
429    /// Formats the selector for debugging.
430    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
431        f.debug_struct("Selector")
432            .field("provider", &self.provider())
433            .field("resource", &self.resource())
434            .field("variant", &self.variant())
435            .field("context", &self.context())
436            .field("location", &self.location())
437            .field("fragment", &self.fragment())
438            .finish()
439    }
440}