printobserver_types/
ids.rs1use 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
22const HYPHENATED_LEN: usize = 36;
24
25const REQUIRED_VERSION: usize = 7;
27
28#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct IdentifierError {
31 type_name: &'static str,
33 detail: String,
35}
36
37impl IdentifierError {
38 #[must_use]
40 pub const fn type_name(&self) -> &'static str {
41 self.type_name
42 }
43
44 #[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
63fn 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
89macro_rules! identifier {
91 ($name:ident, $what:literal) => {
92 #[doc = concat!("PrintObserver's own identifier for ", $what, ".")]
93 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
98 pub struct $name(Uuid);
99
100 impl $name {
101 #[must_use]
103 pub fn new() -> Self {
104 Self(Uuid::now_v7())
105 }
106
107 #[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");