Skip to main content

printobserver_types/
ids.rs

1//! The identity rule, and the three identifiers the event envelope reaches.
2//!
3//! Every identifier this system mints is a UUID version 7, minted by the
4//! store, and serialized as its lowercase hyphenated string. Parsing is strict
5//! in both directions a permissive UUID reader is loose in: a spelling other
6//! than the lowercase hyphenated one is refused rather than normalized, and a
7//! version field other than 7 is refused rather than carried, so an identifier
8//! this system did not mint cannot enter it by parse.
9//!
10//! The rule is the [`identifier!`](crate::identifier) macro, and it is
11//! exported: a domain that mints an identifier of its own declares it through
12//! the macro, in its own crate, and gets the same minting, display, parsing
13//! and schema every other identifier has. Declared here are only the three the
14//! envelope itself reaches — a print, an event, an image — because those are
15//! what every domain and every client must agree on. An identifier one domain
16//! keys its own records by is that domain's to declare.
17//!
18//! An identifier belonging to an external system — a provider's own print id,
19//! a firmware's tool number — is held in that system's own representation and
20//! named for it, and is never one of these.
21
22use core::fmt;
23
24pub use uuid::Uuid;
25
26/// The length of a hyphenated UUID: the one spelling these identifiers take.
27const HYPHENATED_LEN: usize = 36;
28
29/// The UUID version every identifier this system mints carries.
30const REQUIRED_VERSION: usize = 7;
31
32/// Why a string is not one of this system's identifiers.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct IdentifierError {
35    /// The type name the string was being read as.
36    type_name: &'static str,
37    /// What is wrong with the string.
38    detail: String,
39}
40
41impl IdentifierError {
42    /// The type name the string was being read as.
43    #[must_use]
44    pub const fn type_name(&self) -> &'static str {
45        self.type_name
46    }
47
48    /// What is wrong with the string.
49    #[must_use]
50    pub fn detail(&self) -> &str {
51        &self.detail
52    }
53}
54
55impl fmt::Display for IdentifierError {
56    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
57        write!(
58            formatter,
59            "{} is not valid: {}",
60            self.type_name, self.detail
61        )
62    }
63}
64
65impl core::error::Error for IdentifierError {}
66
67/// Read the one spelling this system accepts, refusing every other.
68///
69/// This is the parse half of the rule every identifier the [`identifier!`]
70/// macro declares applies; it is public so that the macro can reach it from
71/// another crate, and is nothing a caller needs by hand.
72///
73/// # Errors
74///
75/// Returns [`IdentifierError`] naming the type and what is wrong with the
76/// string: a spelling other than the lowercase hyphenated one, text that is
77/// not a UUID at all, or a UUID at a version other than 7.
78pub fn parse_identifier(type_name: &'static str, text: &str) -> Result<Uuid, IdentifierError> {
79    let refuse = |detail: String| IdentifierError { type_name, detail };
80    if text.len() != HYPHENATED_LEN {
81        return Err(refuse(format!(
82            "expected the lowercase hyphenated spelling of {HYPHENATED_LEN} characters, \
83             found {} characters in {text:?}",
84            text.len()
85        )));
86    }
87    if text.chars().any(|character| character.is_ascii_uppercase()) {
88        return Err(refuse(format!(
89            "expected the lowercase spelling, found an uppercase character in {text:?}"
90        )));
91    }
92    let uuid = Uuid::parse_str(text)
93        .map_err(|error| refuse(format!("{text:?} is not a UUID: {error}")))?;
94    if uuid.get_version_num() != REQUIRED_VERSION {
95        return Err(refuse(format!(
96            "expected a version {REQUIRED_VERSION} UUID, found version {} in {text:?}",
97            uuid.get_version_num()
98        )));
99    }
100    Ok(uuid)
101}
102
103/// Declare one identifier newtype: minting, display, strict parsing and schema.
104///
105/// `identifier!(Name, "what it identifies")` declares `Name` as a UUID version
106/// 7 newtype, minted by [`new`](Self::new), displayed and serialized as its
107/// lowercase hyphenated string, refused on parse in any other spelling or at
108/// any other version, and emitting the schema every identifier of this system
109/// emits. The type crate declares the three the envelope reaches through it;
110/// a domain declares its own the same way, in its own crate.
111#[macro_export]
112macro_rules! identifier {
113    ($name:ident, $what:literal) => {
114        #[doc = concat!("PrintObserver's own identifier for ", $what, ".")]
115        ///
116        /// A UUID version 7, minted by the store, serialized as its lowercase
117        /// hyphenated string, and refused on parse in any other spelling or at
118        /// any other version.
119        #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
120        pub struct $name($crate::ids::Uuid);
121
122        impl $name {
123            /// Mint a fresh identifier. This is the store's to call.
124            #[must_use]
125            pub fn new() -> Self {
126                Self($crate::ids::Uuid::now_v7())
127            }
128
129            /// The identifier as the UUID it is.
130            #[must_use]
131            pub const fn as_uuid(&self) -> &$crate::ids::Uuid {
132                &self.0
133            }
134        }
135
136        impl ::core::default::Default for $name {
137            fn default() -> Self {
138                Self::new()
139            }
140        }
141
142        impl ::core::fmt::Display for $name {
143            fn fmt(&self, formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
144                ::core::write!(formatter, "{}", self.0.as_hyphenated())
145            }
146        }
147
148        impl ::core::str::FromStr for $name {
149            type Err = $crate::IdentifierError;
150
151            fn from_str(text: &str) -> ::core::result::Result<Self, Self::Err> {
152                $crate::ids::parse_identifier(stringify!($name), text).map(Self)
153            }
154        }
155
156        impl $crate::serde::Serialize for $name {
157            fn serialize<S: $crate::serde::Serializer>(
158                &self,
159                serializer: S,
160            ) -> ::core::result::Result<S::Ok, S::Error> {
161                serializer.collect_str(&self.0.as_hyphenated())
162            }
163        }
164
165        impl<'de> $crate::serde::Deserialize<'de> for $name {
166            fn deserialize<D: $crate::serde::Deserializer<'de>>(
167                deserializer: D,
168            ) -> ::core::result::Result<Self, D::Error> {
169                let text = <::std::string::String as $crate::serde::Deserialize>::deserialize(
170                    deserializer,
171                )?;
172                text.parse()
173                    .map_err(<D::Error as $crate::serde::de::Error>::custom)
174            }
175        }
176
177        impl $crate::schemars::JsonSchema for $name {
178            fn schema_name() -> ::std::borrow::Cow<'static, str> {
179                ::std::borrow::Cow::Borrowed(stringify!($name))
180            }
181
182            fn json_schema(
183                _generator: &mut $crate::schemars::SchemaGenerator,
184            ) -> $crate::schemars::Schema {
185                $crate::schemars::json_schema!({
186                    "type": "string",
187                    "format": "uuid",
188                    "title": stringify!($name),
189                    "description": concat!(
190                        "A lowercase hyphenated version 7 UUID identifying ", $what, "."
191                    ),
192                    "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$"
193                })
194            }
195        }
196    };
197}
198
199identifier!(PrintId, "one print");
200identifier!(EventId, "one event");
201identifier!(ImageId, "one image");