scim_server/resource/value_objects/
extension.rs

1//! Extension value objects for custom SCIM schema attributes.
2//!
3//! This module provides support for SCIM extension attributes that are defined
4//! by custom schemas. Extension attributes allow SCIM implementations to extend
5//! the core schema with additional attributes while maintaining type safety
6//! and validation.
7//!
8//! ## Design Principles
9//!
10//! - **Schema-Driven**: Extensions are defined by schema URIs and definitions
11//! - **Type-Safe**: Extension values are validated against their schema definitions
12//! - **Flexible**: Support for all SCIM attribute types in extensions
13//! - **Namespace-Aware**: Extensions are grouped by schema URI namespaces
14
15use super::value_object_trait::{ExtensionAttribute, ValueObject};
16use crate::error::{ValidationError, ValidationResult};
17use crate::resource::value_objects::SchemaUri;
18use crate::schema::types::{AttributeDefinition, AttributeType};
19use base64::Engine;
20use serde::{Deserialize, Serialize};
21use serde_json::Value;
22use std::any::Any;
23use std::collections::HashMap;
24
25/// A single extension attribute with its value and metadata.
26///
27/// Extension attributes are defined by custom schemas and can contain
28/// any valid SCIM attribute type. They maintain a reference to their
29/// schema definition for validation purposes.
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct ExtensionAttributeValue {
32    /// The schema URI that defines this extension
33    schema_uri: SchemaUri,
34    /// The name of the attribute within the extension schema
35    attribute_name: String,
36    /// The JSON value of the attribute
37    value: Value,
38    /// The schema definition for validation
39    #[serde(skip)]
40    definition: Option<AttributeDefinition>,
41}
42
43impl ExtensionAttributeValue {
44    /// Create a new extension attribute value.
45    pub fn new(
46        schema_uri: SchemaUri,
47        attribute_name: String,
48        value: Value,
49        definition: Option<AttributeDefinition>,
50    ) -> ValidationResult<Self> {
51        let ext_attr = Self {
52            schema_uri,
53            attribute_name,
54            value,
55            definition,
56        };
57
58        // Validate the value against the definition if available
59        if let Some(ref def) = ext_attr.definition {
60            ext_attr.validate_value_against_definition(def)?;
61        }
62
63        Ok(ext_attr)
64    }
65
66    /// Create an extension attribute without immediate validation.
67    ///
68    /// This is useful when the schema definition is not available at construction time
69    /// but will be provided later for validation.
70    pub fn new_unchecked(schema_uri: SchemaUri, attribute_name: String, value: Value) -> Self {
71        Self {
72            schema_uri,
73            attribute_name,
74            value,
75            definition: None,
76        }
77    }
78
79    /// Set the schema definition for this extension attribute.
80    pub fn with_definition(mut self, definition: AttributeDefinition) -> ValidationResult<Self> {
81        self.validate_value_against_definition(&definition)?;
82        self.definition = Some(definition);
83        Ok(self)
84    }
85
86    /// Get the schema URI for this extension.
87    pub fn schema_uri(&self) -> &SchemaUri {
88        &self.schema_uri
89    }
90
91    /// Get the attribute name.
92    pub fn attribute_name(&self) -> &str {
93        &self.attribute_name
94    }
95
96    /// Get the JSON value.
97    pub fn value(&self) -> &Value {
98        &self.value
99    }
100
101    /// Get the schema definition if available.
102    pub fn definition(&self) -> Option<&AttributeDefinition> {
103        self.definition.as_ref()
104    }
105
106    /// Extract the extension namespace from the schema URI.
107    pub fn extension_namespace(&self) -> String {
108        // Extract namespace from URN format like "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
109        if let Some(parts) = self.schema_uri.as_str().split(':').last() {
110            if parts.contains(':') {
111                // More complex namespace extraction if needed
112                parts.to_string()
113            } else {
114                parts.to_string()
115            }
116        } else {
117            // Fallback to using the entire URI as namespace
118            self.schema_uri.as_str().to_string()
119        }
120    }
121
122    /// Validate the value against the schema definition.
123    fn validate_value_against_definition(
124        &self,
125        definition: &AttributeDefinition,
126    ) -> ValidationResult<()> {
127        // Check attribute name matches
128        if definition.name != self.attribute_name {
129            return Err(ValidationError::InvalidAttributeName {
130                actual: self.attribute_name.clone(),
131                expected: definition.name.clone(),
132            });
133        }
134
135        // Validate value type against definition
136        self.validate_value_type(definition)?;
137
138        // Validate required constraints
139        if definition.required && matches!(self.value, Value::Null) {
140            return Err(ValidationError::RequiredAttributeMissing(
141                self.attribute_name.clone(),
142            ));
143        }
144
145        // Validate canonical values if specified
146        if !definition.canonical_values.is_empty() {
147            self.validate_canonical_values(definition)?;
148        }
149
150        Ok(())
151    }
152
153    /// Validate the value type against the attribute definition.
154    fn validate_value_type(&self, definition: &AttributeDefinition) -> ValidationResult<()> {
155        let matches_type = match (&definition.data_type, &self.value) {
156            (AttributeType::String, Value::String(_)) => true,
157            (AttributeType::Boolean, Value::Bool(_)) => true,
158            (AttributeType::Integer, Value::Number(n)) if n.is_i64() => true,
159            (AttributeType::Decimal, Value::Number(_)) => true,
160            (AttributeType::DateTime, Value::String(s)) => {
161                // Basic datetime format validation
162                chrono::DateTime::parse_from_rfc3339(s).is_ok()
163            }
164            (AttributeType::Binary, Value::String(s)) => {
165                // Basic base64 validation
166                base64::engine::general_purpose::STANDARD.decode(s).is_ok()
167            }
168            (AttributeType::Reference, Value::String(_)) => true, // URI validation could be added
169            (AttributeType::Complex, Value::Object(_)) => true,
170            (_, Value::Null) => !definition.required,
171            _ => false,
172        };
173
174        if !matches_type {
175            return Err(ValidationError::InvalidAttributeType {
176                attribute: self.attribute_name.clone(),
177                expected: format!("{:?}", definition.data_type),
178                actual: self.get_value_type_name().to_string(),
179            });
180        }
181
182        Ok(())
183    }
184
185    /// Validate canonical values constraint.
186    fn validate_canonical_values(&self, definition: &AttributeDefinition) -> ValidationResult<()> {
187        if let Value::String(value_str) = &self.value {
188            if !definition.canonical_values.contains(value_str) {
189                return Err(ValidationError::InvalidCanonicalValue {
190                    attribute: self.attribute_name.clone(),
191                    value: value_str.clone(),
192                    allowed: definition.canonical_values.clone(),
193                });
194            }
195        }
196        Ok(())
197    }
198
199    /// Get the type name of the JSON value for error reporting.
200    fn get_value_type_name(&self) -> &'static str {
201        match &self.value {
202            Value::Null => "null",
203            Value::Bool(_) => "boolean",
204            Value::Number(n) if n.is_i64() => "integer",
205            Value::Number(_) => "decimal",
206            Value::String(_) => "string",
207            Value::Array(_) => "array",
208            Value::Object(_) => "object",
209        }
210    }
211}
212
213impl ValueObject for ExtensionAttributeValue {
214    fn attribute_type(&self) -> AttributeType {
215        if let Some(ref def) = self.definition {
216            def.data_type.clone()
217        } else {
218            // Infer type from JSON value
219            match &self.value {
220                Value::String(_) => AttributeType::String,
221                Value::Bool(_) => AttributeType::Boolean,
222                Value::Number(n) if n.is_i64() => AttributeType::Integer,
223                Value::Number(_) => AttributeType::Decimal,
224                Value::Object(_) => AttributeType::Complex,
225                _ => AttributeType::String, // Default fallback
226            }
227        }
228    }
229
230    fn attribute_name(&self) -> &str {
231        &self.attribute_name
232    }
233
234    fn to_json(&self) -> ValidationResult<Value> {
235        Ok(self.value.clone())
236    }
237
238    fn validate_against_schema(&self, definition: &AttributeDefinition) -> ValidationResult<()> {
239        self.validate_value_against_definition(definition)
240    }
241
242    fn as_json_value(&self) -> Value {
243        self.value.clone()
244    }
245
246    fn supports_definition(&self, definition: &AttributeDefinition) -> bool {
247        definition.name == self.attribute_name
248    }
249
250    fn clone_boxed(&self) -> Box<dyn ValueObject> {
251        Box::new(self.clone())
252    }
253
254    fn as_any(&self) -> &dyn Any {
255        self
256    }
257}
258
259impl ExtensionAttribute for ExtensionAttributeValue {
260    fn schema_uri(&self) -> &str {
261        self.schema_uri.as_str()
262    }
263
264    fn extension_namespace(&self) -> &str {
265        // For now, we'll use the schema URI as the namespace
266        // In a more sophisticated implementation, this could be cached
267        // or computed more efficiently
268        self.schema_uri.as_str()
269    }
270
271    fn validate_extension_rules(&self) -> ValidationResult<()> {
272        // Extension-specific validation rules can be added here
273        // For now, we delegate to the standard schema validation
274        if let Some(ref def) = self.definition {
275            self.validate_against_schema(def)
276        } else {
277            Ok(())
278        }
279    }
280}
281
282/// Collection of extension attributes grouped by schema URI.
283///
284/// This provides an organized way to manage multiple extension attributes
285/// and ensures that attributes are properly namespaced by their schema URIs.
286#[derive(Debug, Clone, Serialize, Deserialize)]
287pub struct ExtensionCollection {
288    /// Map of schema URI to extension attributes
289    extensions: HashMap<String, Vec<ExtensionAttributeValue>>,
290}
291
292impl ExtensionCollection {
293    /// Create a new empty extension collection.
294    pub fn new() -> Self {
295        Self {
296            extensions: HashMap::new(),
297        }
298    }
299
300    /// Add an extension attribute to the collection.
301    pub fn add_attribute(&mut self, attribute: ExtensionAttributeValue) {
302        let schema_uri = attribute.schema_uri().as_str().to_string();
303        self.extensions
304            .entry(schema_uri)
305            .or_insert_with(Vec::new)
306            .push(attribute);
307    }
308
309    /// Get all extension attributes for a specific schema URI.
310    pub fn get_by_schema(&self, schema_uri: &str) -> Option<&Vec<ExtensionAttributeValue>> {
311        self.extensions.get(schema_uri)
312    }
313
314    /// Get a specific extension attribute by schema URI and attribute name.
315    pub fn get_attribute(
316        &self,
317        schema_uri: &str,
318        attribute_name: &str,
319    ) -> Option<&ExtensionAttributeValue> {
320        self.extensions
321            .get(schema_uri)?
322            .iter()
323            .find(|attr| attr.attribute_name() == attribute_name)
324    }
325
326    /// Get all schema URIs that have extensions in this collection.
327    pub fn schema_uris(&self) -> Vec<&str> {
328        self.extensions.keys().map(|s| s.as_str()).collect()
329    }
330
331    /// Get all extension attributes across all schemas.
332    pub fn all_attributes(&self) -> Vec<&ExtensionAttributeValue> {
333        self.extensions
334            .values()
335            .flat_map(|attrs| attrs.iter())
336            .collect()
337    }
338
339    /// Remove all extensions for a specific schema URI.
340    pub fn remove_schema(&mut self, schema_uri: &str) -> Option<Vec<ExtensionAttributeValue>> {
341        self.extensions.remove(schema_uri)
342    }
343
344    /// Check if the collection is empty.
345    pub fn is_empty(&self) -> bool {
346        self.extensions.is_empty()
347    }
348
349    /// Get the total number of extension attributes.
350    pub fn len(&self) -> usize {
351        self.extensions.values().map(|v| v.len()).sum()
352    }
353
354    /// Validate all extension attributes in the collection.
355    pub fn validate_all(&self) -> ValidationResult<()> {
356        for attributes in self.extensions.values() {
357            for attribute in attributes {
358                attribute.validate_extension_rules()?;
359            }
360        }
361        Ok(())
362    }
363
364    /// Convert the extension collection to a JSON object.
365    ///
366    /// The resulting JSON object has schema URIs as keys and
367    /// objects containing the extension attributes as values.
368    pub fn to_json(&self) -> ValidationResult<Value> {
369        let mut result = serde_json::Map::new();
370
371        for (schema_uri, attributes) in &self.extensions {
372            let mut schema_obj = serde_json::Map::new();
373
374            for attribute in attributes {
375                schema_obj.insert(attribute.attribute_name().to_string(), attribute.to_json()?);
376            }
377
378            result.insert(schema_uri.clone(), Value::Object(schema_obj));
379        }
380
381        Ok(Value::Object(result))
382    }
383
384    /// Create an extension collection from a JSON object.
385    ///
386    /// The JSON object should have schema URIs as keys and
387    /// objects containing extension attributes as values.
388    pub fn from_json(value: &Value) -> ValidationResult<Self> {
389        let mut collection = Self::new();
390
391        if let Value::Object(schema_map) = value {
392            for (schema_uri_str, schema_value) in schema_map {
393                let schema_uri = SchemaUri::new(schema_uri_str.clone())?;
394
395                if let Value::Object(attr_map) = schema_value {
396                    for (attr_name, attr_value) in attr_map {
397                        let ext_attr = ExtensionAttributeValue::new_unchecked(
398                            schema_uri.clone(),
399                            attr_name.clone(),
400                            attr_value.clone(),
401                        );
402                        collection.add_attribute(ext_attr);
403                    }
404                }
405            }
406        }
407
408        Ok(collection)
409    }
410}
411
412impl Default for ExtensionCollection {
413    fn default() -> Self {
414        Self::new()
415    }
416}
417
418#[cfg(test)]
419mod tests {
420    use super::*;
421    use crate::schema::types::{Mutability, Uniqueness};
422
423    fn create_test_schema_uri() -> SchemaUri {
424        SchemaUri::new("urn:ietf:params:scim:schemas:extension:enterprise:2.0:User".to_string())
425            .unwrap()
426    }
427
428    fn create_test_definition() -> AttributeDefinition {
429        AttributeDefinition {
430            name: "employeeNumber".to_string(),
431            data_type: AttributeType::String,
432            multi_valued: false,
433            required: false,
434            case_exact: false,
435            mutability: Mutability::ReadWrite,
436            uniqueness: Uniqueness::None,
437            canonical_values: vec![],
438            sub_attributes: vec![],
439            returned: None,
440        }
441    }
442
443    #[test]
444    fn test_extension_attribute_creation() {
445        let schema_uri = create_test_schema_uri();
446        let definition = create_test_definition();
447        let value = Value::String("12345".to_string());
448
449        let ext_attr = ExtensionAttributeValue::new(
450            schema_uri.clone(),
451            "employeeNumber".to_string(),
452            value.clone(),
453            Some(definition),
454        )
455        .unwrap();
456
457        assert_eq!(ext_attr.schema_uri(), &schema_uri);
458        assert_eq!(ext_attr.attribute_name(), "employeeNumber");
459        assert_eq!(ext_attr.value(), &value);
460    }
461
462    #[test]
463    fn test_extension_attribute_validation() {
464        let schema_uri = create_test_schema_uri();
465        let definition = create_test_definition();
466
467        // Valid value
468        let valid_value = Value::String("12345".to_string());
469        let result = ExtensionAttributeValue::new(
470            schema_uri.clone(),
471            "employeeNumber".to_string(),
472            valid_value,
473            Some(definition.clone()),
474        );
475        assert!(result.is_ok());
476
477        // Invalid type
478        let invalid_value = Value::Number(serde_json::Number::from(12345));
479        let result = ExtensionAttributeValue::new(
480            schema_uri.clone(),
481            "employeeNumber".to_string(),
482            invalid_value,
483            Some(definition),
484        );
485        assert!(result.is_err());
486    }
487
488    #[test]
489    fn test_extension_collection() {
490        let mut collection = ExtensionCollection::new();
491
492        let schema_uri = create_test_schema_uri();
493        let ext_attr = ExtensionAttributeValue::new_unchecked(
494            schema_uri.clone(),
495            "employeeNumber".to_string(),
496            Value::String("12345".to_string()),
497        );
498
499        collection.add_attribute(ext_attr);
500
501        assert_eq!(collection.len(), 1);
502        assert!(!collection.is_empty());
503        assert!(collection.get_by_schema(schema_uri.as_str()).is_some());
504        assert!(
505            collection
506                .get_attribute(schema_uri.as_str(), "employeeNumber")
507                .is_some()
508        );
509    }
510
511    #[test]
512    fn test_extension_collection_json_round_trip() {
513        let mut collection = ExtensionCollection::new();
514
515        let schema_uri = create_test_schema_uri();
516        let ext_attr = ExtensionAttributeValue::new_unchecked(
517            schema_uri.clone(),
518            "employeeNumber".to_string(),
519            Value::String("12345".to_string()),
520        );
521
522        collection.add_attribute(ext_attr);
523
524        // Convert to JSON and back
525        let json = collection.to_json().unwrap();
526        let restored_collection = ExtensionCollection::from_json(&json).unwrap();
527
528        assert_eq!(collection.len(), restored_collection.len());
529        assert!(
530            restored_collection
531                .get_attribute(schema_uri.as_str(), "employeeNumber")
532                .is_some()
533        );
534    }
535
536    #[test]
537    fn test_value_object_trait_implementation() {
538        let schema_uri = create_test_schema_uri();
539        let ext_attr = ExtensionAttributeValue::new_unchecked(
540            schema_uri,
541            "employeeNumber".to_string(),
542            Value::String("12345".to_string()),
543        );
544
545        assert_eq!(ext_attr.attribute_type(), AttributeType::String);
546        assert_eq!(ext_attr.attribute_name(), "employeeNumber");
547        assert_eq!(ext_attr.as_json_value(), Value::String("12345".to_string()));
548    }
549
550    #[test]
551    fn test_extension_attribute_trait_implementation() {
552        let schema_uri = create_test_schema_uri();
553        let ext_attr = ExtensionAttributeValue::new_unchecked(
554            schema_uri.clone(),
555            "employeeNumber".to_string(),
556            Value::String("12345".to_string()),
557        );
558
559        assert_eq!(ext_attr.schema_uri(), &schema_uri);
560        assert!(ext_attr.validate_extension_rules().is_ok());
561    }
562}