Skip to main content

zrx_id/
id.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//! Identifier.
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::path::PathBuf;
34use std::str::FromStr;
35
36use zrx_path::PathExt;
37use zrx_scheduler::Value;
38
39mod builder;
40mod convert;
41mod error;
42pub mod expression;
43pub mod filter;
44pub mod format;
45mod macros;
46pub mod matcher;
47pub mod selector;
48pub mod specificity;
49pub mod uri;
50
51pub use builder::Builder;
52pub use convert::TryToId;
53pub use error::{Error, Result};
54use format::Format;
55use uri::Uri;
56
57// ----------------------------------------------------------------------------
58// Structs
59// ----------------------------------------------------------------------------
60
61/// Identifier.
62///
63/// Identifiers are structured string-based representations which are used to
64/// uniquely identify artifacts as they move through streams and stores. They
65/// use a compact, yet human-readable format that is easy to generate and
66/// parse, and consists of the following six components:
67///
68/// - `provider`, e.g., file or git.
69/// - `resource`, e.g., volume, branch or tag.
70/// - `variant`, e.g., language, version or format.
71/// - `context`, e.g., source or output directory.
72/// - `location`, e.g., file or folder.
73/// - `fragment`, e.g., line number or anchor.
74///
75/// Identifiers implement [`Eq`], [`PartialEq`] and [`Hash`], as well as [`Ord`]
76/// and [`PartialOrd`], so they can be stored in ordered and unordered storages,
77/// as well as efficiently compared with each other. The structured string-based
78/// representation is defined as follows:
79///
80/// ``` text
81/// zri:<provider>:<resource>:<variant>:<context>:<location>:<fragment>
82/// ```
83///
84/// This ensures blazing fast cloning and editing. Additionally, identifiers are
85/// guaranteed to not contain backslashes or path traversals in components. An
86/// empty component, for those that are allowed to remain empty, is equal to the
87/// default in the context set by the given provider.
88///
89/// # Examples
90///
91/// Create an identifier:
92///
93/// ```
94/// # use std::error::Error;
95/// # fn main() -> Result<(), Box<dyn Error>> {
96/// use zrx_id::Id;
97///
98/// // Create identifier builder
99/// let builder = Id::builder()
100///     .provider("file")
101///     .context("docs")
102///     .location("index.md");
103///
104/// // Create identifier from builder
105/// let id = builder.build()?;
106/// assert_eq!(id.as_str(), "zri:file:::docs:index.md:");
107/// # Ok(())
108/// # }
109/// ```
110///
111/// Create an identifier from a string:
112///
113/// ```
114/// # use std::error::Error;
115/// # fn main() -> Result<(), Box<dyn Error>> {
116/// use zrx_id::Id;
117///
118/// // Create identifier from string
119/// let id: Id = "zri:file:::docs:index.md:".parse()?;
120/// # Ok(())
121/// # }
122/// ```
123#[derive(Clone)]
124pub struct Id {
125    /// Formatted string.
126    format: Format<7>,
127    /// Precomputed hash.
128    hash: u64,
129}
130
131// ----------------------------------------------------------------------------
132// Implementations
133// ----------------------------------------------------------------------------
134
135impl Id {
136    /// Converts the identifier to a relative file system path.
137    ///
138    /// This method creates a relative [`PathBuf`] from both, the `context` and
139    /// `location` components of the identifier, using platform-dependent path
140    /// separators. The resulting path is always relative, and never absolute,
141    /// since both, `context` and `location`, are always relative.
142    ///
143    /// In order to resolve the path, the [`Id::resource`] needs to be taken
144    /// into account, which is of course provider-specific. Note that for use
145    /// of paths in URLs, [`Id::as_uri`] must be used, which guarantees that
146    /// all path separators are forward slashes.
147    ///
148    /// # Examples
149    ///
150    /// ```
151    /// # use std::error::Error;
152    /// # fn main() -> Result<(), Box<dyn Error>> {
153    /// use std::path::Path;
154    /// use zrx_id::Id;
155    ///
156    /// // Create identifier from string
157    /// let id: Id = "zri:file:::docs:index.md:".parse()?;
158    ///
159    /// // Create path from identifier
160    /// let path = id.to_path();
161    /// assert_eq!(path, Path::new("docs/index.md"));
162    /// # Ok(())
163    /// # }
164    /// ```
165    #[inline]
166    #[must_use]
167    pub fn to_path(&self) -> PathBuf {
168        let mut path = PathBuf::from(self.context().as_ref());
169        path.push(self.location().as_ref());
170        path.relative_to(".")
171    }
172
173    /// Returns the string representation.
174    ///
175    /// # Examples
176    ///
177    /// ```
178    /// # use std::error::Error;
179    /// # fn main() -> Result<(), Box<dyn Error>> {
180    /// use zrx_id::Id;
181    ///
182    /// // Create identifier from string
183    /// let id: Id = "zri:file:::docs:index.md:".parse()?;
184    ///
185    /// // Obtain string representation
186    /// assert_eq!(id.as_str(), "zri:file:::docs:index.md:");
187    /// # Ok(())
188    /// # }
189    /// ```
190    #[inline]
191    #[must_use]
192    pub fn as_str(&self) -> &str {
193        self.format.as_str()
194    }
195
196    /// Returns the URI representation.
197    ///
198    /// This method creates a URI from [`Id::location`], which is necessary for
199    /// using the identifier in URLs, e.g., to construct relative links.
200    ///
201    /// # Examples
202    ///
203    /// ```
204    /// # use std::error::Error;
205    /// # fn main() -> Result<(), Box<dyn Error>> {
206    /// use zrx_id::uri::Uri;
207    /// use zrx_id::Id;
208    ///
209    /// // Create identifier from string
210    /// let id: Id = "zri:file:::docs:index.md:".parse()?;
211    ///
212    /// // Obtain URI representation
213    /// assert_eq!(id.as_uri(), Uri::from("index.md"));
214    /// # Ok(())
215    /// # }
216    /// ```
217    #[inline]
218    #[must_use]
219    pub fn as_uri(&self) -> Uri<'_> {
220        Uri::from(self.location())
221    }
222}
223
224#[allow(clippy::must_use_candidate)]
225impl Id {
226    /// Returns the `provider` component.
227    #[inline]
228    pub fn provider(&self) -> Cow<'_, str> {
229        self.format.get(1)
230    }
231
232    /// Returns the `resource` component, if any.
233    #[inline]
234    pub fn resource(&self) -> Option<Cow<'_, str>> {
235        Some(self.format.get(2)).filter(|value| !value.is_empty())
236    }
237
238    /// Returns the `variant` component, if any.
239    #[inline]
240    pub fn variant(&self) -> Option<Cow<'_, str>> {
241        Some(self.format.get(3)).filter(|value| !value.is_empty())
242    }
243
244    /// Returns the `context` component.
245    #[inline]
246    pub fn context(&self) -> Cow<'_, str> {
247        self.format.get(4)
248    }
249
250    /// Returns the `location` component.
251    #[inline]
252    pub fn location(&self) -> Cow<'_, str> {
253        self.format.get(5)
254    }
255
256    /// Returns the `fragment` component, if any.
257    #[inline]
258    pub fn fragment(&self) -> Option<Cow<'_, str>> {
259        Some(self.format.get(6)).filter(|value| !value.is_empty())
260    }
261}
262
263// ----------------------------------------------------------------------------
264// Trait implementations
265// ----------------------------------------------------------------------------
266
267impl Value for Id {}
268
269// ----------------------------------------------------------------------------
270
271impl AsRef<Format<7>> for Id {
272    /// Returns the formatted string.
273    ///
274    /// Note that it's normally not necessary to access the formatted string
275    /// directly, as all components can be accessed via the respective methods.
276    /// We need to access the underlying formatted string in our internal APIs,
277    /// e.g., to compute the [`Specificity`][] for the given [`Id`].
278    ///
279    /// [`Specificity`]: crate::id::specificity::Specificity
280    #[inline]
281    fn as_ref(&self) -> &Format<7> {
282        &self.format
283    }
284}
285
286// ----------------------------------------------------------------------------
287
288impl FromStr for Id {
289    type Err = Error;
290
291    /// Attempts to create an identifier from a string.
292    ///
293    /// The string must adhere to the following format and include exactly six
294    /// `:` separators, even in case some components are omitted. The optional
295    /// components are `resource`, `variant` and `fragment`, and can be left
296    /// empty, which is represented as empty strings internally.
297    ///
298    /// ``` text
299    /// zri:<provider>:<resource>:<variant>:<context>:<location>:<fragment>
300    /// ```
301    ///
302    /// # Errors
303    ///
304    /// Returns [`Error::Component`] if any of the `provider`, `context` or
305    /// `location` components are not set, and [`Error::Prefix`] if the prefix
306    /// isn't `zri`. On low-level format errors, [`Error::Format`] is returned.
307    ///
308    /// # Examples
309    ///
310    /// ```
311    /// # use std::error::Error;
312    /// # fn main() -> Result<(), Box<dyn Error>> {
313    /// use zrx_id::Id;
314    ///
315    /// // Create identifier from string
316    /// let id: Id = "zri:file:::docs:index.md:".parse()?;
317    /// # Ok(())
318    /// # }
319    /// ```
320    fn from_str(value: &str) -> Result<Self> {
321        let format = Format::from_str(value)?;
322
323        // Ensure prefix is set
324        if format.get(0) != "zri" {
325            Err(Error::Prefix)?;
326        }
327
328        // Ensure provider is set
329        if format.get(1).is_empty() {
330            Err(Error::Component("provider"))?;
331        }
332
333        // Ensure context is set
334        if format.get(4).is_empty() {
335            Err(Error::Component("context"))?;
336        }
337
338        // Ensure location is set
339        if format.get(5).is_empty() {
340            Err(Error::Component("location"))?;
341        }
342
343        // Precompute hash for fast hashing
344        let hash = {
345            let mut hasher = AHasher::default();
346            format.hash(&mut hasher);
347            hasher.finish()
348        };
349
350        // No errors occurred
351        Ok(Self { format, hash })
352    }
353}
354
355// ----------------------------------------------------------------------------
356
357impl Hash for Id {
358    /// Hashes the identifier.
359    ///
360    /// Since identifiers are immutable, we can use a precomputed hash for fast
361    /// hashing. This is especially useful when identifiers are used as keys in
362    /// hash maps or hash sets, where hashing is a frequent operation, as the
363    /// performance gains are significant with constant time.
364    #[inline]
365    fn hash<H>(&self, state: &mut H)
366    where
367        H: Hasher,
368    {
369        state.write_u64(self.hash);
370    }
371}
372
373// ----------------------------------------------------------------------------
374
375impl PartialEq for Id {
376    /// Compares two identifiers for equality.
377    ///
378    /// # Examples
379    ///
380    /// ```
381    /// # use std::error::Error;
382    /// # fn main() -> Result<(), Box<dyn Error>> {
383    /// use zrx_id::Id;
384    ///
385    /// // Create and compare identifiers
386    /// let a: Id = "zri:file:::docs:index.md:".parse()?;
387    /// let b: Id = "zri:file:::docs:index.md:".parse()?;
388    /// assert_eq!(a, b);
389    /// # Ok(())
390    /// # }
391    /// ```
392    #[inline]
393    fn eq(&self, other: &Self) -> bool {
394        // We first compare the precomputed hashes, which is extremly fast, as
395        // it saves us the comparison when the identifiers are different
396        self.hash == other.hash && self.format == other.format
397    }
398}
399
400impl Eq for Id {}
401
402// ----------------------------------------------------------------------------
403
404impl PartialOrd for Id {
405    /// Orders two identifiers.
406    ///
407    /// # Examples
408    ///
409    /// ```
410    /// # use std::error::Error;
411    /// # fn main() -> Result<(), Box<dyn Error>> {
412    /// use zrx_id::Id;
413    ///
414    /// // Create and compare identifiers
415    /// let a: Id = "zri:file:::docs:index.md:".parse()?;
416    /// let b: Id = "zri:file:::docs:about.md:".parse()?;
417    /// assert!(a > b);
418    /// # Ok(())
419    /// # }
420    /// ```
421    #[inline]
422    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
423        Some(self.cmp(other))
424    }
425}
426
427impl Ord for Id {
428    /// Orders two identifiers.
429    ///
430    /// # Examples
431    ///
432    /// ```
433    /// # use std::error::Error;
434    /// # fn main() -> Result<(), Box<dyn Error>> {
435    /// use zrx_id::Id;
436    ///
437    /// // Create and compare identifiers
438    /// let a: Id = "zri:file:::docs:index.md:".parse()?;
439    /// let b: Id = "zri:file:::docs:about.md:".parse()?;
440    /// assert!(a > b);
441    /// # Ok(())
442    /// # }
443    /// ```
444    #[inline]
445    fn cmp(&self, other: &Self) -> Ordering {
446        self.format.cmp(&other.format)
447    }
448}
449
450// ----------------------------------------------------------------------------
451
452impl Display for Id {
453    /// Formats the identifier for display.
454    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
455        Display::fmt(&self.format, f)
456    }
457}
458
459impl Debug for Id {
460    /// Formats the identifier for debugging.
461    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
462        f.debug_struct("Id")
463            .field("provider", &self.provider())
464            .field("resource", &self.resource())
465            .field("variant", &self.variant())
466            .field("context", &self.context())
467            .field("location", &self.location())
468            .field("fragment", &self.fragment())
469            .finish()
470    }
471}