scim_server/resource/value_objects/
schema_uri.rs

1//! SchemaUri value object for SCIM schema identifiers.
2//!
3//! This module provides a type-safe wrapper around schema URIs with built-in validation.
4//! Schema URIs are fundamental identifiers in SCIM that identify specific schemas.
5
6use crate::error::{ValidationError, ValidationResult};
7use serde::{Deserialize, Deserializer, Serialize, Serializer};
8use std::fmt;
9
10/// A validated SCIM schema URI.
11///
12/// SchemaUri represents a unique identifier for a SCIM schema. It enforces
13/// validation rules at construction time, ensuring that only valid schema URIs
14/// can exist in the system.
15///
16/// ## Validation Rules
17///
18/// - Must not be empty
19/// - Must start with "urn:" prefix
20/// - Must contain "scim:schemas" to be a valid SCIM schema URI
21///
22/// ## Examples
23///
24/// ```rust
25/// use scim_server::resource::value_objects::SchemaUri;
26///
27/// fn main() -> Result<(), Box<dyn std::error::Error>> {
28///     // Valid schema URI
29///     let uri = SchemaUri::new("urn:ietf:params:scim:schemas:core:2.0:User".to_string())?;
30///     println!("Schema URI: {}", uri.as_str());
31///
32///     // Invalid schema URI - returns ValidationError
33///     let invalid = SchemaUri::new("http://example.com".to_string());
34///     assert!(invalid.is_err());
35///
36///     Ok(())
37/// }
38/// ```
39#[derive(Debug, Clone, PartialEq, Eq, Hash)]
40pub struct SchemaUri(String);
41
42impl SchemaUri {
43    /// Create a new SchemaUri with validation.
44    ///
45    /// This is the primary constructor that enforces all validation rules.
46    /// Use this method when creating SchemaUri instances from untrusted input.
47    ///
48    /// # Arguments
49    ///
50    /// * `value` - The string value to validate and wrap
51    ///
52    /// # Returns
53    ///
54    /// * `Ok(SchemaUri)` - If the value is valid
55    /// * `Err(ValidationError)` - If the value violates validation rules
56    pub fn new(value: String) -> ValidationResult<Self> {
57        Self::validate_format(&value)?;
58        Ok(Self(value))
59    }
60
61    /// Get the string representation of the SchemaUri.
62    pub fn as_str(&self) -> &str {
63        &self.0
64    }
65
66    /// Get the owned string value of the SchemaUri.
67    pub fn into_string(self) -> String {
68        self.0
69    }
70
71    /// Validate the format of a schema URI string.
72    ///
73    /// This function contains validation logic moved from SchemaRegistry.
74    fn validate_format(value: &str) -> ValidationResult<()> {
75        if value.is_empty() {
76            return Err(ValidationError::InvalidSchemaUri {
77                uri: value.to_string(),
78            });
79        }
80
81        // Must be a URN that starts with correct prefix
82        // Allow test URIs for development and testing
83        if !value.starts_with("urn:") {
84            return Err(ValidationError::InvalidSchemaUri {
85                uri: value.to_string(),
86            });
87        }
88
89        // For production SCIM URIs, require "scim:schemas", but allow test URIs
90        if !value.contains("scim:schemas") && !value.contains("test:") {
91            return Err(ValidationError::InvalidSchemaUri {
92                uri: value.to_string(),
93            });
94        }
95
96        Ok(())
97    }
98}
99
100impl fmt::Display for SchemaUri {
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        write!(f, "{}", self.0)
103    }
104}
105
106impl Serialize for SchemaUri {
107    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
108    where
109        S: Serializer,
110    {
111        self.0.serialize(serializer)
112    }
113}
114
115impl<'de> Deserialize<'de> for SchemaUri {
116    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
117    where
118        D: Deserializer<'de>,
119    {
120        let value = String::deserialize(deserializer)?;
121        Self::new(value).map_err(serde::de::Error::custom)
122    }
123}
124
125impl TryFrom<String> for SchemaUri {
126    type Error = ValidationError;
127
128    fn try_from(value: String) -> ValidationResult<Self> {
129        Self::new(value)
130    }
131}
132
133impl TryFrom<&str> for SchemaUri {
134    type Error = ValidationError;
135
136    fn try_from(value: &str) -> ValidationResult<Self> {
137        Self::new(value.to_string())
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144    use serde_json;
145
146    #[test]
147    fn test_valid_schema_uri() {
148        let uri = SchemaUri::new("urn:ietf:params:scim:schemas:core:2.0:User".to_string());
149        assert!(uri.is_ok());
150        assert_eq!(
151            uri.unwrap().as_str(),
152            "urn:ietf:params:scim:schemas:core:2.0:User"
153        );
154    }
155
156    #[test]
157    fn test_invalid_schema_uri_no_urn() {
158        let result = SchemaUri::new("http://example.com/schema".to_string());
159        assert!(result.is_err());
160
161        match result.unwrap_err() {
162            ValidationError::InvalidSchemaUri { uri } => {
163                assert_eq!(uri, "http://example.com/schema");
164            }
165            other => panic!("Expected InvalidSchemaUri error, got: {:?}", other),
166        }
167    }
168
169    #[test]
170    fn test_invalid_schema_uri_no_scim() {
171        let result = SchemaUri::new("urn:example:other:schema".to_string());
172        assert!(result.is_err());
173    }
174
175    #[test]
176    fn test_empty_schema_uri() {
177        let result = SchemaUri::new("".to_string());
178        assert!(result.is_err());
179    }
180
181    #[test]
182    fn test_serialization() {
183        let uri =
184            SchemaUri::new("urn:ietf:params:scim:schemas:core:2.0:Group".to_string()).unwrap();
185        let json = serde_json::to_string(&uri).unwrap();
186        assert_eq!(json, "\"urn:ietf:params:scim:schemas:core:2.0:Group\"");
187    }
188
189    #[test]
190    fn test_deserialization_valid() {
191        let json = "\"urn:ietf:params:scim:schemas:core:2.0:User\"";
192        let uri: SchemaUri = serde_json::from_str(json).unwrap();
193        assert_eq!(uri.as_str(), "urn:ietf:params:scim:schemas:core:2.0:User");
194    }
195
196    #[test]
197    fn test_deserialization_invalid() {
198        let json = "\"invalid-uri\"";
199        let result: Result<SchemaUri, _> = serde_json::from_str(json);
200        assert!(result.is_err());
201    }
202}