Skip to main content

siderite_validation/types/
uuid.rs

1//! UUID newtype wrapping [`uuid::Uuid`].
2
3use serde::{Deserialize, Serialize};
4
5use crate::context::ValidationContext;
6use crate::dump::Dump;
7use crate::error::FieldError;
8use crate::rules;
9use crate::schema::{Schema, SchemaObject, SchemaRegistry};
10use crate::validate::Validate;
11use serde_json::Value;
12
13/// An RFC 4122 UUID of any version.
14///
15/// Unlike Pydantic `UUID4`, this does not require version 4. Input is a
16/// hyphenated (`8-4-4-4-12`) or simple (32 hex digits) string; matching is
17/// case-insensitive. URN and braced forms are rejected.
18#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
19#[serde(try_from = "String", into = "String")]
20pub struct Uuid(::uuid::Uuid);
21
22impl Uuid {
23    /// Parse `value` as a hyphenated or simple UUID.
24    pub fn new(value: impl AsRef<str>) -> Result<Self, FieldError> {
25        rules::parse_uuid(value.as_ref()).map(Self)
26    }
27}
28
29crate::types::impl_wrapper!(Uuid => ::uuid::Uuid);
30
31impl TryFrom<String> for Uuid {
32    type Error = FieldError;
33
34    fn try_from(value: String) -> Result<Self, Self::Error> {
35        Self::new(value)
36    }
37}
38
39impl TryFrom<&str> for Uuid {
40    type Error = FieldError;
41
42    fn try_from(value: &str) -> Result<Self, Self::Error> {
43        Self::new(value)
44    }
45}
46
47impl From<Uuid> for String {
48    fn from(value: Uuid) -> Self {
49        value.0.to_string()
50    }
51}
52
53impl Validate for Uuid {
54    fn prepare(input: &mut Value, ctx: &mut ValidationContext) {
55        if super::prepare_as_string(input, ctx) {
56            ctx.check(Self::new(input.as_str().unwrap_or_default()).map(|_| ()));
57        }
58    }
59}
60
61impl Schema for Uuid {
62    fn schema(_: &mut SchemaRegistry) -> SchemaObject {
63        SchemaObject::of_type("string").with("format", "uuid")
64    }
65}
66
67impl Dump for Uuid {}
68
69#[cfg(test)]
70mod tests {
71    #![allow(clippy::unwrap_used)]
72
73    use super::*;
74    use crate::context::ValidationContext;
75    use crate::dump::{Dump, DumpOptions};
76    use crate::schema::schema_for;
77    use crate::types::prepare_codes;
78    use serde_json::json;
79
80    const HYPHENATED: &str = "550e8400-e29b-41d4-a716-446655440000";
81    const SIMPLE: &str = "550e8400e29b41d4a716446655440000";
82
83    #[test]
84    fn uuid_construction() {
85        let uuid = Uuid::new(HYPHENATED).unwrap();
86        assert_eq!(uuid.to_string(), HYPHENATED);
87        assert_eq!(Uuid::new(SIMPLE).unwrap().to_string(), HYPHENATED);
88        assert!(Uuid::new("550E8400-E29B-41D4-A716-446655440000").is_ok());
89        assert_eq!(Uuid::new("not-a-uuid").unwrap_err().code, "uuid_parsing");
90        assert_eq!(
91            Uuid::new("urn:uuid:550e8400-e29b-41d4-a716-446655440000")
92                .unwrap_err()
93                .code,
94            "uuid_parsing"
95        );
96    }
97
98    #[test]
99    fn uuid_serde_round_trip_and_reject() {
100        let uuid = Uuid::new(SIMPLE).unwrap();
101        let encoded = serde_json::to_string(&uuid).unwrap();
102        assert_eq!(encoded, format!("\"{HYPHENATED}\""));
103        let round: Uuid = serde_json::from_str(&encoded).unwrap();
104        assert_eq!(round, uuid);
105        assert!(serde_json::from_str::<Uuid>("\"nope\"").is_err());
106    }
107
108    #[test]
109    fn uuid_prepare_lax_and_strict() {
110        let (out, codes) = prepare_codes::<Uuid>(json!(HYPHENATED), ValidationContext::new());
111        assert!(codes.is_empty());
112        assert_eq!(out, json!(HYPHENATED));
113        assert_eq!(
114            prepare_codes::<Uuid>(json!(1), ValidationContext::new()).1,
115            ["string_type"]
116        );
117        assert_eq!(
118            prepare_codes::<Uuid>(json!(1), ValidationContext::new().with_strict(true)).1,
119            ["string_type"]
120        );
121        assert_eq!(
122            prepare_codes::<Uuid>(json!("nope"), ValidationContext::new()).1,
123            ["uuid_parsing"]
124        );
125    }
126
127    #[test]
128    fn uuid_schema_dump_and_parse_value() {
129        assert_eq!(
130            schema_for::<Uuid>().0.into_value(),
131            json!({"type": "string", "format": "uuid"})
132        );
133        let uuid = Uuid::new(HYPHENATED).unwrap();
134        assert_eq!(uuid.dump(&DumpOptions::new()).unwrap(), json!(HYPHENATED));
135        let parsed = crate::parse_value::<Uuid>(json!(SIMPLE), ValidationContext::new()).unwrap();
136        assert_eq!(parsed.to_string(), HYPHENATED);
137        let err = crate::parse_value::<Uuid>(json!("nope"), ValidationContext::new()).unwrap_err();
138        assert_eq!(err.errors[0].code, "uuid_parsing");
139    }
140}