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