newtype_uuid/
lib.rs

1//! A newtype wrapper around [`Uuid`].
2//!
3//! # Motivation
4//!
5//! Many large systems use UUIDs as unique identifiers for various entities. However, the [`Uuid`]
6//! type does not carry information about the kind of entity it identifies, which can lead to mixing
7//! up different types of UUIDs at runtime.
8//!
9//! This crate provides a wrapper type around [`Uuid`] that allows you to specify the kind of entity
10//! the UUID identifies.
11//!
12//! # Example
13//!
14//! ```
15//! use newtype_uuid::{GenericUuid, TypedUuid, TypedUuidKind, TypedUuidTag};
16//!
17//! // First, define a type that represents the kind of UUID this is.
18//! enum MyKind {}
19//!
20//! impl TypedUuidKind for MyKind {
21//!     fn tag() -> TypedUuidTag {
22//!         // Tags are required to be ASCII identifiers, with underscores
23//!         // and dashes also supported. The validity of a tag can be checked
24//!         // at compile time by assigning it to a const, like so:
25//!         const TAG: TypedUuidTag = TypedUuidTag::new("my_kind");
26//!         TAG
27//!     }
28//! }
29//!
30//! // Now, a UUID can be created with this kind.
31//! let uuid: TypedUuid<MyKind> = "dffc3068-1cd6-47d5-b2f3-636b41b07084".parse().unwrap();
32//!
33//! // The Display (and therefore ToString) impls still show the same value.
34//! assert_eq!(uuid.to_string(), "dffc3068-1cd6-47d5-b2f3-636b41b07084");
35//!
36//! // The Debug impl will show the tag as well.
37//! assert_eq!(
38//!     format!("{:?}", uuid),
39//!     "dffc3068-1cd6-47d5-b2f3-636b41b07084 (my_kind)"
40//! );
41//! ```
42//!
43//! If you have a large number of UUID kinds, consider using
44//! [`newtype-uuid-macros`] which comes with several convenience features.
45//!
46//! ```
47//! use newtype_uuid_macros::impl_typed_uuid_kinds;
48//!
49//! // Invoke this macro with:
50//! impl_typed_uuid_kinds! {
51//!     kinds = {
52//!         User = {},
53//!         Project = {},
54//!         // ...
55//!     },
56//! }
57//! ```
58//!
59//! See [`newtype-uuid-macros`] for more information.
60//!
61//! [`newtype-uuid-macros`]: https://docs.rs/newtype-uuid-macros
62//!
63//! For simpler cases, you can also write your own declarative macro. Use this
64//! template to get started:
65//!
66//! ```rust
67//! # use newtype_uuid::{TypedUuidKind, TypedUuidTag};
68//! macro_rules! impl_kinds {
69//!     ($($kind:ident => $tag:literal),* $(,)?) => {
70//!         $(
71//!             pub enum $kind {}
72//!
73//!             impl TypedUuidKind for $kind {
74//!                 #[inline]
75//!                 fn tag() -> TypedUuidTag {
76//!                     const TAG: TypedUuidTag = TypedUuidTag::new($tag);
77//!                     TAG
78//!                 }
79//!             }
80//!         )*
81//!     };
82//! }
83//!
84//! // Invoke this macro with:
85//! impl_kinds! {
86//!     UserKind => "user",
87//!     ProjectKind => "project",
88//! }
89//! ```
90//!
91//! # Implementations
92//!
93//! In general, [`TypedUuid`] uses the same wire and serialization formats as [`Uuid`]. This means
94//! that persistent representations of [`TypedUuid`] are the same as [`Uuid`]; [`TypedUuid`] is
95//! intended to be helpful within Rust code, not across serialization boundaries.
96//!
97//! - The `Display` and `FromStr` impls are forwarded to the underlying [`Uuid`].
98//! - If the `serde` feature is enabled, `TypedUuid` will serialize and deserialize using the same
99//!   format as [`Uuid`].
100//! - If the `schemars08` feature is enabled, [`TypedUuid`] will implement `JsonSchema` if the
101//!   corresponding [`TypedUuidKind`] implements `JsonSchema`.
102//!
103//! To abstract over typed and untyped UUIDs, the [`GenericUuid`] trait is provided. This trait also
104//! permits conversions between typed and untyped UUIDs.
105//!
106//! # Dependencies
107//!
108//! - The only required dependency is the [`uuid`] crate. Optional features may add further
109//!   dependencies.
110//!
111//! # Features
112//!
113//! - `default`: Enables default features in the newtype-uuid crate.
114//! - `std`: Enables the use of the standard library. *Enabled by default.*
115//! - `serde`: Enables serialization and deserialization support via Serde. *Not enabled by
116//!   default.*
117//! - `v4`: Enables the `new_v4` method for generating UUIDs. *Not enabled by default.*
118//! - `schemars08`: Enables support for generating JSON schemas via schemars 0.8. *Not enabled by
119//!   default.* Note that the format of the generated schema is **not currently part** of the stable
120//!   API, though we hope to stabilize it in the future.
121//! - `proptest1`: Enables support for generating `proptest::Arbitrary` instances of UUIDs. *Not enabled by default.*
122//!
123//! # Minimum supported Rust version (MSRV)
124//!
125//! The MSRV of this crate is **Rust 1.79.** In general, this crate will follow the MSRV of the
126//! underlying `uuid` crate or of dependencies, with an aim to be conservative.
127//!
128//! Within the 1.x series, MSRV updates will be accompanied by a minor version bump. The MSRVs for
129//! each minor version are:
130//!
131//! * Version **1.0.x**: Rust 1.60.
132//! * Version **1.1.x**: Rust 1.61. This permits `TypedUuid<T>` to have `const fn` methods.
133//! * Version **1.2.x**: Rust 1.67, required by some dependency updates.
134//! * Version **1.3.x**: Rust 1.79, required by some dependency updates.
135//!
136//! # Alternatives
137//!
138//! - [`typed-uuid`](https://crates.io/crates/typed-uuid): generally similar, but with a few design
139//!   decisions that are different.
140
141#![forbid(unsafe_code)]
142#![warn(missing_docs)]
143#![cfg_attr(not(feature = "std"), no_std)]
144#![cfg_attr(doc_cfg, feature(doc_cfg))]
145
146#[cfg(feature = "alloc")]
147extern crate alloc;
148
149/// Macro support for [`newtype-uuid-macros`].
150///
151/// This module re-exports types needed for [`newtype-uuid-macros`] to work.
152///
153/// [`newtype-uuid-macros`]: https://docs.rs/newtype-uuid-macros
154#[doc(hidden)]
155pub mod macro_support {
156    #[cfg(feature = "schemars08")]
157    pub use schemars as schemars08;
158    #[cfg(feature = "schemars08")]
159    pub use serde_json;
160}
161
162use core::{
163    cmp::Ordering,
164    fmt,
165    hash::{Hash, Hasher},
166    marker::PhantomData,
167    str::FromStr,
168};
169#[cfg(feature = "v7")]
170pub use uuid::Timestamp;
171use uuid::{Uuid, Version};
172
173/// A UUID with type-level information about what it's used for.
174///
175/// For more, see [the library documentation](crate).
176#[repr(transparent)]
177#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
178#[cfg_attr(feature = "serde", serde(transparent, bound = ""))]
179pub struct TypedUuid<T: TypedUuidKind> {
180    uuid: Uuid,
181    _phantom: PhantomData<T>,
182}
183
184impl<T: TypedUuidKind> TypedUuid<T> {
185    /// The 'nil UUID' (all zeros).
186    ///
187    /// The nil UUID is a special form of UUID that is specified to have all
188    /// 128 bits set to zero.
189    ///
190    /// # References
191    ///
192    /// * [Nil UUID in RFC4122](https://tools.ietf.org/html/rfc4122.html#section-4.1.7)
193    #[inline]
194    #[must_use]
195    pub const fn nil() -> Self {
196        Self {
197            uuid: Uuid::nil(),
198            _phantom: PhantomData,
199        }
200    }
201
202    /// The 'max UUID' (all ones).
203    ///
204    /// The max UUID is a special form of UUID that is specified to have all
205    /// 128 bits set to one.
206    ///
207    /// # References
208    ///
209    /// * [Max UUID in Draft RFC: New UUID Formats, Version 4](https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format-04#section-5.4)
210    #[inline]
211    #[must_use]
212    pub const fn max() -> Self {
213        Self {
214            uuid: Uuid::max(),
215            _phantom: PhantomData,
216        }
217    }
218
219    /// Creates a UUID from four field values.
220    #[inline]
221    #[must_use]
222    pub const fn from_fields(d1: u32, d2: u16, d3: u16, d4: [u8; 8]) -> Self {
223        Self {
224            uuid: Uuid::from_fields(d1, d2, d3, &d4),
225            _phantom: PhantomData,
226        }
227    }
228
229    /// Creates a UUID from four field values in little-endian order.
230    ///
231    /// The bytes in the `d1`, `d2` and `d3` fields will be flipped to convert into big-endian
232    /// order. This is based on the endianness of the UUID, rather than the target environment so
233    /// bytes will be flipped on both big and little endian machines.
234    #[inline]
235    #[must_use]
236    pub const fn from_fields_le(d1: u32, d2: u16, d3: u16, d4: [u8; 8]) -> Self {
237        Self {
238            uuid: Uuid::from_fields_le(d1, d2, d3, &d4),
239            _phantom: PhantomData,
240        }
241    }
242
243    /// Creates a UUID from a 128bit value.
244    #[inline]
245    #[must_use]
246    pub const fn from_u128(value: u128) -> Self {
247        Self {
248            uuid: Uuid::from_u128(value),
249            _phantom: PhantomData,
250        }
251    }
252
253    /// Creates a UUID from a 128bit value in little-endian order.
254    ///
255    /// The entire value will be flipped to convert into big-endian order. This is based on the
256    /// endianness of the UUID, rather than the target environment so bytes will be flipped on both
257    /// big and little endian machines.
258    #[inline]
259    #[must_use]
260    pub const fn from_u128_le(value: u128) -> Self {
261        Self {
262            uuid: Uuid::from_u128_le(value),
263            _phantom: PhantomData,
264        }
265    }
266
267    /// Creates a UUID from two 64bit values.
268    #[inline]
269    #[must_use]
270    pub const fn from_u64_pair(d1: u64, d2: u64) -> Self {
271        Self {
272            uuid: Uuid::from_u64_pair(d1, d2),
273            _phantom: PhantomData,
274        }
275    }
276
277    /// Creates a UUID using the supplied bytes.
278    #[inline]
279    #[must_use]
280    pub const fn from_bytes(bytes: uuid::Bytes) -> Self {
281        Self {
282            uuid: Uuid::from_bytes(bytes),
283            _phantom: PhantomData,
284        }
285    }
286
287    /// Creates a UUID using the supplied bytes in little-endian order.
288    ///
289    /// The individual fields encoded in the buffer will be flipped.
290    #[inline]
291    #[must_use]
292    pub const fn from_bytes_le(bytes: uuid::Bytes) -> Self {
293        Self {
294            uuid: Uuid::from_bytes_le(bytes),
295            _phantom: PhantomData,
296        }
297    }
298
299    /// Creates a new, random UUID v4 of this type.
300    #[inline]
301    #[cfg(feature = "v4")]
302    #[must_use]
303    pub fn new_v4() -> Self {
304        Self::from_untyped_uuid(Uuid::new_v4())
305    }
306
307    /// Creates a new, random UUID v7 of this type.
308    #[inline]
309    #[cfg(feature = "v7")]
310    #[must_use]
311    pub fn new_v7(ts: uuid::Timestamp) -> Self {
312        Self::from_untyped_uuid(Uuid::new_v7(ts))
313    }
314
315    /// Returns the version number of the UUID.
316    ///
317    /// This represents the algorithm used to generate the value.
318    /// This method is the future-proof alternative to [`Self::get_version`].
319    ///
320    /// # References
321    ///
322    /// * [Version Field in RFC 9562](https://www.ietf.org/rfc/rfc9562.html#section-4.2)
323    #[inline]
324    pub const fn get_version_num(&self) -> usize {
325        self.uuid.get_version_num()
326    }
327
328    /// Returns the version of the UUID.
329    ///
330    /// This represents the algorithm used to generate the value.
331    /// If the version field doesn't contain a recognized version then `None`
332    /// is returned. If you're trying to read the version for a future extension
333    /// you can also use [`Uuid::get_version_num`] to unconditionally return a
334    /// number. Future extensions may start to return `Some` once they're
335    /// standardized and supported.
336    ///
337    /// # References
338    ///
339    /// * [Version Field in RFC 9562](https://www.ietf.org/rfc/rfc9562.html#section-4.2)
340    #[inline]
341    pub fn get_version(&self) -> Option<Version> {
342        self.uuid.get_version()
343    }
344
345    /// Converts the UUID to one with looser semantics.
346    ///
347    /// By default, UUID kinds are considered independent, and conversions
348    /// between them must happen via the [`GenericUuid`] interface. But in some
349    /// cases, there may be a relationship between two different UUID kinds, and
350    /// you may wish to easily convert UUIDs from one kind to another.
351    ///
352    /// Typically, a conversion from `TypedUuid<T>` to `TypedUuid<U>` is most
353    /// useful when `T`'s semantics are a superset of `U`'s, or in other words,
354    /// when every `TypedUuid<T>` is logically also a `TypedUuid<U>`.
355    ///
356    /// For instance:
357    ///
358    /// * Imagine you have [`TypedUuidKind`]s for different types of
359    ///   database connections, where `DbConnKind` is the general type
360    ///   and `PgConnKind` is a specific kind for Postgres.
361    /// * Since every Postgres connection is also a database connection,
362    ///   a cast from `TypedUuid<PgConnKind>` to `TypedUuid<DbConnKind>`
363    ///   makes sense.
364    /// * The inverse cast would not make sense, as a database connection may not
365    ///   necessarily be a Postgres connection.
366    ///
367    /// This interface provides an alternative, safer way to perform this
368    /// conversion. Indicate your intention to allow a conversion between kinds
369    /// by implementing `From<T> for U`, as shown in the example below.
370    ///
371    /// # Examples
372    ///
373    /// ```
374    /// use newtype_uuid::{TypedUuid, TypedUuidKind, TypedUuidTag};
375    ///
376    /// // Let's say that these UUIDs represent repositories for different
377    /// // version control systems, such that you have a generic RepoKind:
378    /// pub enum RepoKind {}
379    /// impl TypedUuidKind for RepoKind {
380    ///     fn tag() -> TypedUuidTag {
381    ///         const TAG: TypedUuidTag = TypedUuidTag::new("repo");
382    ///         TAG
383    ///     }
384    /// }
385    ///
386    /// // You also have more specific kinds:
387    /// pub enum GitRepoKind {}
388    /// impl TypedUuidKind for GitRepoKind {
389    ///     fn tag() -> TypedUuidTag {
390    ///         const TAG: TypedUuidTag = TypedUuidTag::new("git_repo");
391    ///         TAG
392    ///     }
393    /// }
394    /// // (and HgRepoKind, JujutsuRepoKind, etc...)
395    ///
396    /// // First, define a `From` impl. This impl indicates your desire
397    /// // to convert from one kind to another.
398    /// impl From<GitRepoKind> for RepoKind {
399    ///     fn from(value: GitRepoKind) -> Self {
400    ///         match value {}
401    ///     }
402    /// }
403    ///
404    /// // Now you can convert between them:
405    /// let git_uuid: TypedUuid<GitRepoKind> =
406    ///     TypedUuid::from_u128(0xe9245204_34ea_4ca7_a1c6_2e94fa49df61);
407    /// let repo_uuid: TypedUuid<RepoKind> = git_uuid.upcast();
408    /// ```
409    #[inline]
410    #[must_use]
411    pub const fn upcast<U: TypedUuidKind>(self) -> TypedUuid<U>
412    where
413        T: Into<U>,
414    {
415        TypedUuid {
416            uuid: self.uuid,
417            _phantom: PhantomData,
418        }
419    }
420}
421
422// ---
423// Trait impls
424// ---
425
426impl<T: TypedUuidKind> PartialEq for TypedUuid<T> {
427    #[inline]
428    fn eq(&self, other: &Self) -> bool {
429        self.uuid.eq(&other.uuid)
430    }
431}
432
433impl<T: TypedUuidKind> Eq for TypedUuid<T> {}
434
435impl<T: TypedUuidKind> PartialOrd for TypedUuid<T> {
436    #[inline]
437    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
438        Some(self.cmp(other))
439    }
440}
441
442impl<T: TypedUuidKind> Ord for TypedUuid<T> {
443    #[inline]
444    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
445        self.uuid.cmp(&other.uuid)
446    }
447}
448
449impl<T: TypedUuidKind> Hash for TypedUuid<T> {
450    #[inline]
451    fn hash<H: Hasher>(&self, state: &mut H) {
452        self.uuid.hash(state);
453    }
454}
455
456impl<T: TypedUuidKind> fmt::Debug for TypedUuid<T> {
457    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
458        self.uuid.fmt(f)?;
459        write!(f, " ({})", T::tag())
460    }
461}
462
463impl<T: TypedUuidKind> fmt::Display for TypedUuid<T> {
464    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
465        self.uuid.fmt(f)
466    }
467}
468
469impl<T: TypedUuidKind> Clone for TypedUuid<T> {
470    #[inline]
471    fn clone(&self) -> Self {
472        *self
473    }
474}
475
476impl<T: TypedUuidKind> Copy for TypedUuid<T> {}
477
478impl<T: TypedUuidKind> FromStr for TypedUuid<T> {
479    type Err = ParseError;
480
481    fn from_str(s: &str) -> Result<Self, Self::Err> {
482        let uuid = Uuid::from_str(s).map_err(|error| ParseError {
483            error,
484            tag: T::tag(),
485        })?;
486        Ok(Self::from_untyped_uuid(uuid))
487    }
488}
489
490impl<T: TypedUuidKind> Default for TypedUuid<T> {
491    #[inline]
492    fn default() -> Self {
493        Self::from_untyped_uuid(Uuid::default())
494    }
495}
496
497impl<T: TypedUuidKind> AsRef<[u8]> for TypedUuid<T> {
498    #[inline]
499    fn as_ref(&self) -> &[u8] {
500        self.uuid.as_ref()
501    }
502}
503
504#[cfg(feature = "alloc")]
505impl<T: TypedUuidKind> From<TypedUuid<T>> for alloc::vec::Vec<u8> {
506    #[inline]
507    fn from(typed_uuid: TypedUuid<T>) -> Self {
508        typed_uuid.into_untyped_uuid().into_bytes().to_vec()
509    }
510}
511
512#[cfg(feature = "schemars08")]
513mod schemars08_imp {
514    use super::*;
515    use schemars::{
516        schema::{InstanceType, Schema, SchemaObject},
517        schema_for, JsonSchema, SchemaGenerator,
518    };
519
520    const CRATE_NAME: &str = "newtype-uuid";
521    const CRATE_VERSION: &str = "1";
522    const CRATE_PATH: &str = "newtype_uuid::TypedUuid";
523
524    /// Implements `JsonSchema` for `TypedUuid<T>`, if `T` implements `JsonSchema`.
525    ///
526    /// * `schema_name` is set to `"TypedUuidFor"`, concatenated by the schema name of `T`.
527    /// * `schema_id` is set to `format!("newtype_uuid::TypedUuid<{}>", T::schema_id())`.
528    /// * `json_schema` is the same as the one for `Uuid`, with the `x-rust-type` extension
529    ///   to allow automatic replacement in typify and progenitor.
530    impl<T> JsonSchema for TypedUuid<T>
531    where
532        T: TypedUuidKind + JsonSchema,
533    {
534        #[inline]
535        fn schema_name() -> String {
536            // Use the alias if available, otherwise generate our own schema name.
537            if let Some(alias) = T::alias() {
538                alias.to_owned()
539            } else {
540                format!("TypedUuidFor{}", T::schema_name())
541            }
542        }
543
544        #[inline]
545        fn schema_id() -> std::borrow::Cow<'static, str> {
546            std::borrow::Cow::Owned(format!("newtype_uuid::TypedUuid<{}>", T::schema_id()))
547        }
548
549        #[inline]
550        fn json_schema(generator: &mut SchemaGenerator) -> Schema {
551            // Look at the schema for `T`. If it has `x-rust-type`, *and* if an
552            // alias is available, we can lift up the `x-rust-type` into our own schema.
553            //
554            // We use a new schema generator for `T` to avoid T's schema being
555            // added to the list of schemas in `generator` in case the lifting
556            // is successful.
557            let t_schema = schema_for!(T);
558            if let Some(schema) = lift_json_schema(&t_schema.schema, T::alias()) {
559                return schema.into();
560            }
561
562            SchemaObject {
563                instance_type: Some(InstanceType::String.into()),
564                format: Some("uuid".to_string()),
565                extensions: [(
566                    "x-rust-type".to_string(),
567                    serde_json::json!({
568                        "crate": CRATE_NAME,
569                        "version": CRATE_VERSION,
570                        "path": CRATE_PATH,
571                        "parameters": [generator.subschema_for::<T>()]
572                    }),
573                )]
574                .into_iter()
575                .collect(),
576                ..Default::default()
577            }
578            .into()
579        }
580    }
581
582    // ? on Option is too easy to make mistakes with, so we use `let Some(..) =
583    // .. else` instead.
584    #[allow(clippy::question_mark)]
585    fn lift_json_schema(schema: &SchemaObject, alias: Option<&str>) -> Option<SchemaObject> {
586        let Some(alias) = alias else {
587            return None;
588        };
589
590        let Some(v) = schema.extensions.get("x-rust-type") else {
591            return None;
592        };
593
594        // The crate, version and path must all be present.
595        let Some(crate_) = v.get("crate") else {
596            return None;
597        };
598        let Some(version) = v.get("version") else {
599            return None;
600        };
601        let Some(path) = v.get("path").and_then(|p| p.as_str()) else {
602            return None;
603        };
604        let Some((module_path, _)) = path.rsplit_once("::") else {
605            return None;
606        };
607
608        // The preconditions are all met. We can lift the schema by appending
609        // the alias to the module path.
610        let alias_path = format!("{module_path}::{alias}");
611
612        Some(SchemaObject {
613            instance_type: Some(InstanceType::String.into()),
614            format: Some("uuid".to_string()),
615            extensions: [(
616                "x-rust-type".to_string(),
617                serde_json::json!({
618                    "crate": crate_,
619                    "version": version,
620                    "path": alias_path,
621                }),
622            )]
623            .into_iter()
624            .collect(),
625            ..Default::default()
626        })
627    }
628}
629
630#[cfg(feature = "proptest1")]
631mod proptest1_imp {
632    use super::*;
633    use proptest::{
634        arbitrary::{any, Arbitrary},
635        strategy::{BoxedStrategy, Strategy},
636    };
637
638    /// Parameters for use with `proptest` instances.
639    ///
640    /// This is currently not exported as a type because it has no options. But
641    /// it's left in as an extension point for the future.
642    #[derive(Clone, Debug, Default)]
643    pub struct TypedUuidParams(());
644
645    /// Generates random `TypedUuid<T>` instances.
646    ///
647    /// Currently, this always returns a version 4 UUID. Support for other kinds
648    /// of UUIDs might be added via [`Self::Parameters`] in the future.
649    impl<T> Arbitrary for TypedUuid<T>
650    where
651        T: TypedUuidKind,
652    {
653        type Parameters = TypedUuidParams;
654        type Strategy = BoxedStrategy<Self>;
655
656        fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
657            let bytes = any::<[u8; 16]>();
658            bytes
659                .prop_map(|b| {
660                    let uuid = uuid::Builder::from_random_bytes(b).into_uuid();
661                    TypedUuid::<T>::from_untyped_uuid(uuid)
662                })
663                .boxed()
664        }
665    }
666}
667
668/// Represents marker types that can be used as a type parameter for [`TypedUuid`].
669///
670/// Generally, an implementation of this will be a zero-sized type that can never be constructed. An
671/// empty struct or enum works well for this.
672///
673/// # Implementations
674///
675/// If the `schemars08` feature is enabled, and [`JsonSchema`] is implemented for a kind `T`, then
676/// [`TypedUuid`]`<T>` will also implement [`JsonSchema`].
677///
678/// If you have a large number of UUID kinds, consider using
679/// [`newtype-uuid-macros`] which comes with several convenience features.
680///
681/// ```
682/// use newtype_uuid_macros::impl_typed_uuid_kinds;
683///
684/// // Invoke this macro with:
685/// impl_typed_uuid_kinds! {
686///     kinds = {
687///         User = {},
688///         Project = {},
689///         // ...
690///     },
691/// }
692/// ```
693///
694/// See [`newtype-uuid-macros`] for more information.
695///
696/// [`newtype-uuid-macros`]: https://docs.rs/newtype-uuid-macros
697/// [`JsonSchema`]: schemars::JsonSchema
698pub trait TypedUuidKind: Send + Sync + 'static {
699    /// Returns the corresponding tag for this kind.
700    ///
701    /// The tag forms a runtime representation of this type.
702    ///
703    /// The tag is required to be a static string.
704    fn tag() -> TypedUuidTag;
705
706    /// Returns a string that corresponds to a type alias for `TypedUuid<Self>`,
707    /// if one is defined.
708    ///
709    /// The type alias must be defined in the same module as `Self`. This
710    /// function is used by the schemars integration to refer to embed a
711    /// reference to that alias in the schema, if available.
712    ///
713    /// This is usually defined by the [`newtype-uuid-macros`] crate.
714    ///
715    /// [`newtype-uuid-macros`]: https://docs.rs/newtype-uuid-macros
716    #[inline]
717    fn alias() -> Option<&'static str> {
718        None
719    }
720}
721
722/// Describes what kind of [`TypedUuid`] something is.
723///
724/// This is the runtime equivalent of [`TypedUuidKind`].
725#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
726pub struct TypedUuidTag(&'static str);
727
728impl TypedUuidTag {
729    /// Creates a new `TypedUuidTag` from a static string.
730    ///
731    /// The string must be non-empty, and consist of:
732    /// - ASCII letters
733    /// - digits (only after the first character)
734    /// - underscores
735    /// - hyphens (only after the first character)
736    ///
737    /// # Panics
738    ///
739    /// Panics if the above conditions aren't met. Use [`Self::try_new`] to handle errors instead.
740    #[must_use]
741    pub const fn new(tag: &'static str) -> Self {
742        match Self::try_new_impl(tag) {
743            Ok(tag) => tag,
744            Err(message) => panic!("{}", message),
745        }
746    }
747
748    /// Attempts to create a new `TypedUuidTag` from a static string.
749    ///
750    /// The string must be non-empty, and consist of:
751    /// - ASCII letters
752    /// - digits (only after the first character)
753    /// - underscores
754    /// - hyphens (only after the first character)
755    ///
756    /// # Errors
757    ///
758    /// Returns a [`TagError`] if the above conditions aren't met.
759    pub const fn try_new(tag: &'static str) -> Result<Self, TagError> {
760        match Self::try_new_impl(tag) {
761            Ok(tag) => Ok(tag),
762            Err(message) => Err(TagError {
763                input: tag,
764                message,
765            }),
766        }
767    }
768
769    const fn try_new_impl(tag: &'static str) -> Result<Self, &'static str> {
770        if tag.is_empty() {
771            return Err("tag must not be empty");
772        }
773
774        let bytes = tag.as_bytes();
775        if !(bytes[0].is_ascii_alphabetic() || bytes[0] == b'_') {
776            return Err("first character of tag must be an ASCII letter or underscore");
777        }
778
779        let mut bytes = match bytes {
780            [_, rest @ ..] => rest,
781            [] => panic!("already checked that it's non-empty"),
782        };
783        while let [rest @ .., last] = &bytes {
784            if !(last.is_ascii_alphanumeric() || *last == b'_' || *last == b'-') {
785                break;
786            }
787            bytes = rest;
788        }
789
790        if !bytes.is_empty() {
791            return Err("tag must only contain ASCII letters, digits, underscores, or hyphens");
792        }
793
794        Ok(Self(tag))
795    }
796
797    /// Returns the tag as a string.
798    pub const fn as_str(&self) -> &'static str {
799        self.0
800    }
801}
802
803impl fmt::Display for TypedUuidTag {
804    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
805        f.write_str(self.0)
806    }
807}
808
809impl AsRef<str> for TypedUuidTag {
810    fn as_ref(&self) -> &str {
811        self.0
812    }
813}
814
815/// An error that occurred while creating a [`TypedUuidTag`].
816#[derive(Clone, Debug)]
817#[non_exhaustive]
818pub struct TagError {
819    /// The input string.
820    pub input: &'static str,
821
822    /// The error message.
823    pub message: &'static str,
824}
825
826impl fmt::Display for TagError {
827    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
828        write!(
829            f,
830            "error creating tag from '{}': {}",
831            self.input, self.message
832        )
833    }
834}
835
836#[cfg(feature = "std")]
837impl std::error::Error for TagError {}
838
839/// An error that occurred while parsing a [`TypedUuid`].
840#[derive(Clone, Debug)]
841#[non_exhaustive]
842pub struct ParseError {
843    /// The underlying error.
844    pub error: uuid::Error,
845
846    /// The tag of the UUID that failed to parse.
847    pub tag: TypedUuidTag,
848}
849
850impl fmt::Display for ParseError {
851    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
852        write!(f, "error parsing UUID ({})", self.tag)
853    }
854}
855
856#[cfg(feature = "std")]
857impl std::error::Error for ParseError {
858    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
859        Some(&self.error)
860    }
861}
862
863/// A trait abstracting over typed and untyped UUIDs.
864///
865/// This can be used to write code that's generic over [`TypedUuid`], [`Uuid`], and other types that
866/// may wrap [`TypedUuid`] (due to e.g. orphan rules).
867///
868/// This trait is similar to `From`, but a bit harder to get wrong -- in general, the conversion
869/// from and to untyped UUIDs should be careful and explicit.
870pub trait GenericUuid {
871    /// Creates a new instance of `Self` from an untyped [`Uuid`].
872    #[must_use]
873    fn from_untyped_uuid(uuid: Uuid) -> Self
874    where
875        Self: Sized;
876
877    /// Converts `self` into an untyped [`Uuid`].
878    #[must_use]
879    fn into_untyped_uuid(self) -> Uuid
880    where
881        Self: Sized;
882
883    /// Returns the inner [`Uuid`].
884    ///
885    /// Generally, [`into_untyped_uuid`](Self::into_untyped_uuid) should be preferred. However,
886    /// in some cases it may be necessary to use this method to satisfy lifetime constraints.
887    fn as_untyped_uuid(&self) -> &Uuid;
888}
889
890impl GenericUuid for Uuid {
891    #[inline]
892    fn from_untyped_uuid(uuid: Uuid) -> Self {
893        uuid
894    }
895
896    #[inline]
897    fn into_untyped_uuid(self) -> Uuid {
898        self
899    }
900
901    #[inline]
902    fn as_untyped_uuid(&self) -> &Uuid {
903        self
904    }
905}
906
907impl<T: TypedUuidKind> GenericUuid for TypedUuid<T> {
908    #[inline]
909    fn from_untyped_uuid(uuid: Uuid) -> Self {
910        Self {
911            uuid,
912            _phantom: PhantomData,
913        }
914    }
915
916    #[inline]
917    fn into_untyped_uuid(self) -> Uuid {
918        self.uuid
919    }
920
921    #[inline]
922    fn as_untyped_uuid(&self) -> &Uuid {
923        &self.uuid
924    }
925}
926
927#[cfg(test)]
928mod tests {
929    use super::*;
930
931    #[test]
932    fn test_validate_tags() {
933        for &valid_tag in &[
934            "a", "a-", "a_", "a-b", "a_b", "a1", "a1-", "a1_", "a1-b", "a1_b", "_a",
935        ] {
936            TypedUuidTag::try_new(valid_tag).expect("tag is valid");
937            // Should not panic
938            _ = TypedUuidTag::new(valid_tag);
939        }
940
941        for invalid_tag in &["", "1", "-", "a1b!", "a1-b!", "a1_b:", "\u{1f4a9}"] {
942            TypedUuidTag::try_new(invalid_tag).unwrap_err();
943        }
944    }
945
946    // This test just ensures that `GenericUuid` is object-safe.
947    #[test]
948    #[cfg(all(feature = "v4", feature = "std"))]
949    fn test_generic_uuid_object_safe() {
950        let uuid = Uuid::new_v4();
951        let box_uuid = Box::new(uuid) as Box<dyn GenericUuid>;
952        assert_eq!(box_uuid.as_untyped_uuid(), &uuid);
953    }
954}