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