kittycad_modeling_cmds/
id.rs

1use schemars::JsonSchema;
2use serde::{de, de::Visitor, Deserialize, Deserializer, Serialize};
3use std::fmt;
4use std::str::FromStr;
5use uuid::Uuid;
6
7/// All commands have unique IDs. These should be randomly generated.
8#[derive(Debug, Clone, Copy, Hash, Ord, PartialOrd, Eq, PartialEq, JsonSchema, Serialize)]
9#[cfg_attr(test, derive(Default))]
10#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
11#[cfg_attr(feature = "ts-rs", ts(export_to = "ModelingCmd.ts"))]
12pub struct ModelingCmdId(pub Uuid);
13
14impl AsRef<Uuid> for ModelingCmdId {
15    fn as_ref(&self) -> &Uuid {
16        &self.0
17    }
18}
19
20// In order to force our own UUID requirements, we need to intercept /
21// implement our own serde deserializer for UUID essentially. We are
22// fortunate to have wrapped the UUID type already so we can do this.
23
24#[cfg(test)]
25mod tests {
26    use super::*;
27
28    #[test]
29    fn modeling_cmd_id_from_bson() {
30        #[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
31        struct Id {
32            id: ModelingCmdId,
33        }
34
35        // Serializing and deserializing an ID (via BSON) should not change it.
36        let id_before = Id {
37            id: ModelingCmdId("f09fc20f-40d4-4a73-92fa-05d53baaabac".parse().unwrap()),
38        };
39        let bytes = bson::to_vec(&id_before).unwrap();
40        let id_after = bson::from_reader(bytes.as_slice()).unwrap();
41        assert_eq!(id_before, id_after);
42    }
43}
44
45struct UuidVisitor;
46
47impl<'de> Visitor<'de> for UuidVisitor {
48    type Value = ModelingCmdId;
49
50    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
51        formatter.write_str("expected a string for uuid")
52    }
53
54    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
55    where
56        E: de::Error,
57    {
58        ModelingCmdId::from_str(value).map_err(|e| de::Error::custom(e.to_string()))
59    }
60
61    fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
62    where
63        E: de::Error,
64    {
65        Uuid::from_slice(v)
66            .map_err(|e| de::Error::custom(e.to_string()))
67            .map(ModelingCmdId)
68    }
69
70    fn visit_borrowed_bytes<E>(self, v: &'de [u8]) -> Result<Self::Value, E>
71    where
72        E: de::Error,
73    {
74        Uuid::from_slice(v)
75            .map_err(|e| de::Error::custom(e.to_string()))
76            .map(ModelingCmdId)
77    }
78
79    fn visit_byte_buf<E>(self, v: Vec<u8>) -> Result<Self::Value, E>
80    where
81        E: de::Error,
82    {
83        Uuid::from_slice(v.as_slice())
84            .map_err(|e| de::Error::custom(e.to_string()))
85            .map(ModelingCmdId)
86    }
87}
88
89impl<'de> Deserialize<'de> for ModelingCmdId {
90    fn deserialize<D>(deserializer: D) -> Result<ModelingCmdId, D::Error>
91    where
92        D: Deserializer<'de>,
93    {
94        deserializer.deserialize_any(UuidVisitor)
95    }
96}
97
98// causes uuid::Error(uuid::error::ErrorKind::GroupLength { group: 0, len: 0, index: 1 })
99const ERR_GROUP_LENGTH: &str = "----";
100
101impl std::str::FromStr for ModelingCmdId {
102    type Err = uuid::Error;
103
104    fn from_str(s: &str) -> Result<Self, Self::Err> {
105        // The following would be unnecessary if uuid::parser was public.
106        // The uuid crate does not require hyphens. Since we return the same
107        // UUID in various places, this leads to a different representation.
108        // For example, 01234567890123456789012345678901 is returned as
109        // 01234567-8901-2345-6789-012345678901. This is not great when
110        // developers expect their UUIDs to not change (even in representation).
111        // Forcing them to use hyphenated UUIDs resolves the issue.
112        // 8-4-4-4-12 is the grouping.
113        // uuid::error is a private module, so we have no access to ErrorKind.
114        // We must use another way to invoke a uuid::Error.
115        //
116        let s2 = if s.len() == 32 { ERR_GROUP_LENGTH } else { s };
117        Uuid::from_str(s2).map(Self)
118    }
119}
120
121impl From<Uuid> for ModelingCmdId {
122    fn from(uuid: Uuid) -> Self {
123        Self(uuid)
124    }
125}
126
127impl From<ModelingCmdId> for Uuid {
128    fn from(id: ModelingCmdId) -> Self {
129        id.0
130    }
131}
132
133impl std::fmt::Display for ModelingCmdId {
134    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135        self.0.fmt(f)
136    }
137}
138
139#[test]
140fn smoke_test() {
141    use std::str::FromStr;
142    assert_eq!(
143        ModelingCmdId::from_str("00000000-0000-0000-0000-000000000000"),
144        Ok(ModelingCmdId(
145            Uuid::from_str("00000000-0000-0000-0000-000000000000").unwrap()
146        ))
147    );
148}
149
150#[test]
151fn requires_hyphens() {
152    use std::str::FromStr;
153    assert_ne!(
154        ModelingCmdId::from_str("00000000000000000000000000000000"),
155        Ok(ModelingCmdId(
156            Uuid::from_str("00000000-0000-0000-0000-000000000000").unwrap()
157        ))
158    );
159}