Skip to main content

printobserver_types/
ids.rs

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