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}