scim_server/resource/value_objects/
resource_id.rs

1//! ResourceId value object for SCIM resource identifiers.
2//!
3//! This module provides a type-safe wrapper around resource IDs with built-in validation.
4//! Resource IDs are fundamental identifiers in SCIM and must follow specific format rules.
5
6use crate::error::{ValidationError, ValidationResult};
7use crate::resource::value_objects::value_object_trait::{SchemaConstructible, ValueObject};
8use crate::schema::types::{AttributeDefinition, AttributeType};
9use serde::{Deserialize, Deserializer, Serialize, Serializer};
10use serde_json::Value;
11use std::any::Any;
12use std::fmt;
13
14/// A validated SCIM resource identifier.
15///
16/// ResourceId represents a unique identifier for a SCIM resource. It enforces
17/// validation rules at construction time, ensuring that only valid resource IDs
18/// can exist in the system.
19///
20/// ## Validation Rules
21///
22/// - Must not be empty
23/// - Must be a valid string
24/// - Additional format rules may be added in the future
25///
26/// ## Examples
27///
28/// ```rust
29/// use scim_server::resource::value_objects::ResourceId;
30///
31/// fn main() -> Result<(), Box<dyn std::error::Error>> {
32///     // Valid resource ID
33///     let id = ResourceId::new("2819c223-7f76-453a-919d-413861904646".to_string())?;
34///     println!("Resource ID: {}", id.as_str());
35///
36///     // Invalid resource ID - returns ValidationError
37///     let invalid = ResourceId::new("".to_string());
38///     assert!(invalid.is_err());
39///
40///     Ok(())
41/// }
42/// ```
43#[derive(Debug, Clone, PartialEq, Eq, Hash)]
44pub struct ResourceId(String);
45
46impl ResourceId {
47    /// Create a new ResourceId with validation.
48    ///
49    /// This is the primary constructor that enforces all validation rules.
50    /// Use this method when creating ResourceId instances from untrusted input.
51    ///
52    /// # Arguments
53    ///
54    /// * `value` - The string value to validate and wrap
55    ///
56    /// # Returns
57    ///
58    /// * `Ok(ResourceId)` - If the value is valid
59    /// * `Err(ValidationError)` - If the value violates validation rules
60    ///
61    /// # Examples
62    ///
63    /// ```rust
64    /// use scim_server::resource::value_objects::ResourceId;
65    ///
66    /// fn main() -> Result<(), Box<dyn std::error::Error>> {
67    ///     let id = ResourceId::new("user-123".to_string())?;
68    ///     let empty_id = ResourceId::new("".to_string()); // Error
69    ///     assert!(empty_id.is_err());
70    ///     Ok(())
71    /// }
72    /// ```
73    pub fn new(value: String) -> ValidationResult<Self> {
74        Self::validate_format(&value)?;
75        Ok(Self(value))
76    }
77
78    /// Get the string representation of the ResourceId.
79    ///
80    /// Returns a reference to the underlying string value. This is safe
81    /// because the value is guaranteed to be valid by construction.
82    ///
83    /// # Examples
84    ///
85    /// ```rust
86    /// use scim_server::resource::value_objects::ResourceId;
87    ///
88    /// fn main() -> Result<(), Box<dyn std::error::Error>> {
89    ///     let id = ResourceId::new("test-id".to_string())?;
90    ///     assert_eq!(id.as_str(), "test-id");
91    ///     Ok(())
92    /// }
93    /// ```
94    pub fn as_str(&self) -> &str {
95        &self.0
96    }
97
98    /// Get the owned string value of the ResourceId.
99    ///
100    /// Consumes the ResourceId and returns the underlying string.
101    /// Use this when you need to transfer ownership of the string value.
102    ///
103    /// # Examples
104    ///
105    /// ```rust
106    /// use scim_server::resource::value_objects::ResourceId;
107    ///
108    /// fn main() -> Result<(), Box<dyn std::error::Error>> {
109    ///     let id = ResourceId::new("test-id".to_string())?;
110    ///     let owned_string = id.into_string();
111    ///     assert_eq!(owned_string, "test-id");
112    ///     Ok(())
113    /// }
114    /// ```
115    pub fn into_string(self) -> String {
116        self.0
117    }
118
119    /// Validate the format of a resource ID string.
120    ///
121    /// This function contains the core validation logic moved from SchemaRegistry.
122    /// It enforces all the rules that define a valid resource ID.
123    ///
124    /// # Arguments
125    ///
126    /// * `value` - The string to validate
127    ///
128    /// # Returns
129    ///
130    /// * `Ok(())` - If the value is valid
131    /// * `Err(ValidationError)` - If the value violates any rules
132    fn validate_format(value: &str) -> ValidationResult<()> {
133        // Check if id is empty
134        if value.is_empty() {
135            return Err(ValidationError::EmptyId);
136        }
137
138        // TODO: Add more sophisticated ID format validation if needed
139        // For now, we accept any non-empty string as a valid ID
140        // Future enhancements might include:
141        // - UUID format validation
142        // - Character set restrictions
143        // - Length limits
144
145        Ok(())
146    }
147}
148
149impl fmt::Display for ResourceId {
150    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151        write!(f, "{}", self.0)
152    }
153}
154
155impl Serialize for ResourceId {
156    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
157    where
158        S: Serializer,
159    {
160        self.0.serialize(serializer)
161    }
162}
163
164impl<'de> Deserialize<'de> for ResourceId {
165    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
166    where
167        D: Deserializer<'de>,
168    {
169        let value = String::deserialize(deserializer)?;
170        Self::new(value).map_err(serde::de::Error::custom)
171    }
172}
173
174/// Convert from String to ResourceId with validation.
175impl TryFrom<String> for ResourceId {
176    type Error = ValidationError;
177
178    fn try_from(value: String) -> ValidationResult<Self> {
179        Self::new(value)
180    }
181}
182
183/// Convert from &str to ResourceId with validation.
184impl TryFrom<&str> for ResourceId {
185    type Error = ValidationError;
186
187    fn try_from(value: &str) -> ValidationResult<Self> {
188        Self::new(value.to_string())
189    }
190}
191
192impl ValueObject for ResourceId {
193    fn attribute_type(&self) -> AttributeType {
194        AttributeType::String
195    }
196
197    fn attribute_name(&self) -> &str {
198        "id"
199    }
200
201    fn to_json(&self) -> ValidationResult<Value> {
202        Ok(Value::String(self.0.clone()))
203    }
204
205    fn validate_against_schema(&self, definition: &AttributeDefinition) -> ValidationResult<()> {
206        if definition.data_type != AttributeType::String {
207            return Err(ValidationError::InvalidAttributeType {
208                attribute: definition.name.clone(),
209                expected: "string".to_string(),
210                actual: format!("{:?}", definition.data_type),
211            });
212        }
213
214        if definition.name != "id" {
215            return Err(ValidationError::InvalidAttributeName {
216                actual: definition.name.clone(),
217                expected: "id".to_string(),
218            });
219        }
220
221        Ok(())
222    }
223
224    fn as_json_value(&self) -> Value {
225        Value::String(self.0.clone())
226    }
227
228    fn supports_definition(&self, definition: &AttributeDefinition) -> bool {
229        definition.data_type == AttributeType::String && definition.name == "id"
230    }
231
232    fn clone_boxed(&self) -> Box<dyn ValueObject> {
233        Box::new(self.clone())
234    }
235
236    fn as_any(&self) -> &dyn Any {
237        self
238    }
239}
240
241impl SchemaConstructible for ResourceId {
242    fn from_schema_and_value(
243        definition: &AttributeDefinition,
244        value: &Value,
245    ) -> ValidationResult<Self> {
246        if definition.name != "id" || definition.data_type != AttributeType::String {
247            return Err(ValidationError::UnsupportedAttributeType {
248                attribute: definition.name.clone(),
249                type_name: format!("{:?}", definition.data_type),
250            });
251        }
252
253        if let Some(id_str) = value.as_str() {
254            Self::new(id_str.to_string())
255        } else {
256            Err(ValidationError::InvalidAttributeType {
257                attribute: definition.name.clone(),
258                expected: "string".to_string(),
259                actual: "non-string".to_string(),
260            })
261        }
262    }
263
264    fn can_construct_from(definition: &AttributeDefinition) -> bool {
265        definition.name == "id" && definition.data_type == AttributeType::String
266    }
267
268    fn constructor_priority() -> u8 {
269        100 // High priority for exact name match
270    }
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276    use crate::schema::types::{Mutability, Uniqueness};
277    use serde_json;
278
279    #[test]
280    fn test_valid_resource_id() {
281        let id = ResourceId::new("2819c223-7f76-453a-919d-413861904646".to_string());
282        assert!(id.is_ok());
283
284        let id = id.unwrap();
285        assert_eq!(id.as_str(), "2819c223-7f76-453a-919d-413861904646");
286    }
287
288    #[test]
289    fn test_empty_resource_id() {
290        let result = ResourceId::new("".to_string());
291        assert!(result.is_err());
292
293        match result.unwrap_err() {
294            ValidationError::EmptyId => {} // Expected
295            other => panic!("Expected EmptyId error, got: {:?}", other),
296        }
297    }
298
299    #[test]
300    fn test_simple_string_id() {
301        let id = ResourceId::new("user-123".to_string());
302        assert!(id.is_ok());
303
304        let id = id.unwrap();
305        assert_eq!(id.as_str(), "user-123");
306    }
307
308    #[test]
309    fn test_into_string() {
310        let id = ResourceId::new("test-id".to_string()).unwrap();
311        let string_value = id.into_string();
312        assert_eq!(string_value, "test-id");
313    }
314
315    #[test]
316    fn test_display() {
317        let id = ResourceId::new("display-test".to_string()).unwrap();
318        assert_eq!(format!("{}", id), "display-test");
319    }
320
321    #[test]
322    fn test_serialization() {
323        let id = ResourceId::new("serialize-test".to_string()).unwrap();
324        let json = serde_json::to_string(&id).unwrap();
325        assert_eq!(json, "\"serialize-test\"");
326    }
327
328    #[test]
329    fn test_deserialization_valid() {
330        let json = "\"deserialize-test\"";
331        let id: ResourceId = serde_json::from_str(json).unwrap();
332        assert_eq!(id.as_str(), "deserialize-test");
333    }
334
335    #[test]
336    fn test_deserialization_invalid() {
337        let json = "\"\""; // Empty string
338        let result: Result<ResourceId, _> = serde_json::from_str(json);
339        assert!(result.is_err());
340    }
341
342    #[test]
343    fn test_try_from_string() {
344        let result = ResourceId::try_from("try-from-test".to_string());
345        assert!(result.is_ok());
346        assert_eq!(result.unwrap().as_str(), "try-from-test");
347
348        let empty_result = ResourceId::try_from("".to_string());
349        assert!(empty_result.is_err());
350    }
351
352    #[test]
353    fn test_try_from_str() {
354        let result = ResourceId::try_from("try-from-str-test");
355        assert!(result.is_ok());
356        assert_eq!(result.unwrap().as_str(), "try-from-str-test");
357
358        let empty_result = ResourceId::try_from("");
359        assert!(empty_result.is_err());
360    }
361
362    #[test]
363    fn test_equality() {
364        let id1 = ResourceId::new("same-id".to_string()).unwrap();
365        let id2 = ResourceId::new("same-id".to_string()).unwrap();
366        let id3 = ResourceId::new("different-id".to_string()).unwrap();
367
368        assert_eq!(id1, id2);
369        assert_ne!(id1, id3);
370    }
371
372    #[test]
373    fn test_hash() {
374        use std::collections::HashMap;
375
376        let id1 = ResourceId::new("hash-test-1".to_string()).unwrap();
377        let id2 = ResourceId::new("hash-test-2".to_string()).unwrap();
378
379        let mut map = HashMap::new();
380        map.insert(id1.clone(), "value1");
381        map.insert(id2.clone(), "value2");
382
383        assert_eq!(map.get(&id1), Some(&"value1"));
384        assert_eq!(map.get(&id2), Some(&"value2"));
385    }
386
387    #[test]
388    fn test_clone() {
389        let id = ResourceId::new("clone-test".to_string()).unwrap();
390        let cloned = id.clone();
391
392        assert_eq!(id, cloned);
393        assert_eq!(id.as_str(), cloned.as_str());
394    }
395
396    #[test]
397    fn test_value_object_trait() {
398        let id = ResourceId::new("test-id".to_string()).unwrap();
399
400        assert_eq!(id.attribute_type(), AttributeType::String);
401        assert_eq!(id.attribute_name(), "id");
402        assert_eq!(id.as_json_value(), Value::String("test-id".to_string()));
403
404        let json_result = id.to_json().unwrap();
405        assert_eq!(json_result, Value::String("test-id".to_string()));
406    }
407
408    #[test]
409    fn test_schema_constructible_trait() {
410        let definition = AttributeDefinition {
411            name: "id".to_string(),
412            data_type: AttributeType::String,
413            multi_valued: false,
414            required: true,
415            case_exact: true,
416            mutability: Mutability::ReadOnly,
417            uniqueness: Uniqueness::Server,
418            canonical_values: vec![],
419            sub_attributes: vec![],
420            returned: None,
421        };
422
423        let value = Value::String("test-id".to_string());
424        let result = ResourceId::from_schema_and_value(&definition, &value);
425        assert!(result.is_ok());
426        assert_eq!(result.unwrap().as_str(), "test-id");
427
428        // Test can_construct_from
429        assert!(ResourceId::can_construct_from(&definition));
430
431        // Test with wrong attribute name
432        let mut wrong_def = definition.clone();
433        wrong_def.name = "userName".to_string();
434        assert!(!ResourceId::can_construct_from(&wrong_def));
435    }
436
437    #[test]
438    fn test_validate_against_schema() {
439        let id = ResourceId::new("test-id".to_string()).unwrap();
440
441        let valid_definition = AttributeDefinition {
442            name: "id".to_string(),
443            data_type: AttributeType::String,
444            multi_valued: false,
445            required: true,
446            case_exact: true,
447            mutability: Mutability::ReadOnly,
448            uniqueness: Uniqueness::Server,
449            canonical_values: vec![],
450            sub_attributes: vec![],
451            returned: None,
452        };
453
454        assert!(id.validate_against_schema(&valid_definition).is_ok());
455
456        // Test with wrong type
457        let mut invalid_def = valid_definition.clone();
458        invalid_def.data_type = AttributeType::Integer;
459        assert!(id.validate_against_schema(&invalid_def).is_err());
460
461        // Test with wrong name
462        invalid_def.name = "userName".to_string();
463        invalid_def.data_type = AttributeType::String;
464        assert!(id.validate_against_schema(&invalid_def).is_err());
465    }
466}