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}