scim_server/schema/
registry.rs

1//! Schema registry for loading, managing, and accessing SCIM schemas.
2//!
3//! This module provides the SchemaRegistry which handles schema loading from files,
4//! schema management, and provides access to registered schemas for validation.
5
6use super::{embedded, types::{AttributeDefinition, AttributeType, Schema}};
7
8use chrono::{DateTime, FixedOffset};
9use serde_json::Value;
10use std::collections::HashMap;
11use std::fs;
12use std::path::Path;
13
14/// Registry for SCIM schemas with validation capabilities.
15///
16/// The schema registry manages all available schemas and provides validation
17/// services for resources. For the MVP, it contains only the hardcoded core User schema.
18#[derive(Debug, Clone)]
19pub struct SchemaRegistry {
20    core_user_schema: Schema,
21    core_group_schema: Schema,
22    schemas: HashMap<String, Schema>,
23}
24
25impl SchemaRegistry {
26    /// Create a new schema registry with embedded core schemas.
27    ///
28    /// This method uses the schemas embedded in the library and doesn't require
29    /// external schema files. For loading schemas from files, use `from_schema_dir()`.
30    pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
31        Self::with_embedded_schemas()
32    }
33
34    /// Create a new schema registry with embedded core schemas.
35    ///
36    /// This method uses the schemas embedded in the library and doesn't require
37    /// external schema files. This is the recommended method for schema discovery
38    /// functionality as it works without any file dependencies.
39    pub fn with_embedded_schemas() -> Result<Self, Box<dyn std::error::Error>> {
40        let core_user_schema = Self::load_schema_from_str(embedded::core_user_schema())?;
41        let core_group_schema = Self::load_schema_from_str(embedded::core_group_schema())?;
42
43        let mut schemas = HashMap::new();
44        schemas.insert(core_user_schema.id.clone(), core_user_schema.clone());
45        schemas.insert(core_group_schema.id.clone(), core_group_schema.clone());
46
47        Ok(Self {
48            core_user_schema,
49            core_group_schema,
50            schemas,
51        })
52    }
53
54    /// Create a schema registry by loading schemas from a directory.
55    pub fn from_schema_dir<P: AsRef<Path>>(
56        schema_dir: P,
57    ) -> Result<Self, Box<dyn std::error::Error>> {
58        let user_schema_path = schema_dir.as_ref().join("User.json");
59        let core_user_schema = Self::load_schema_from_file(&user_schema_path)?;
60
61        let group_schema_path = schema_dir.as_ref().join("Group.json");
62        let core_group_schema = Self::load_schema_from_file(&group_schema_path)?;
63
64        let mut schemas = HashMap::new();
65        schemas.insert(core_user_schema.id.clone(), core_user_schema.clone());
66        schemas.insert(core_group_schema.id.clone(), core_group_schema.clone());
67
68        Ok(Self {
69            core_user_schema,
70            core_group_schema,
71            schemas,
72        })
73    }
74
75    /// Load a schema from a JSON file.
76    fn load_schema_from_file<P: AsRef<Path>>(
77        path: P,
78    ) -> Result<Schema, Box<dyn std::error::Error>> {
79        let content = fs::read_to_string(&path)?;
80        Self::load_schema_from_str(&content)
81    }
82
83    /// Load a schema from a JSON string.
84    fn load_schema_from_str(content: &str) -> Result<Schema, Box<dyn std::error::Error>> {
85        let mut schema: Schema = serde_json::from_str(content)?;
86
87        // Convert JSON schema format to internal format
88        Self::convert_json_schema(&mut schema);
89
90        Ok(schema)
91    }
92
93    /// Convert JSON schema format to internal AttributeDefinition format.
94    fn convert_json_schema(schema: &mut Schema) {
95        for attr in &mut schema.attributes {
96            Self::convert_attribute_definition(attr);
97        }
98    }
99
100    /// Convert a single attribute definition from JSON format.
101    fn convert_attribute_definition(attr: &mut AttributeDefinition) {
102        // Convert data type from string to enum
103        // This is handled by serde deserialization
104
105        // Process sub-attributes recursively
106        for sub_attr in &mut attr.sub_attributes {
107            Self::convert_attribute_definition(sub_attr);
108        }
109    }
110
111    /// Get all available schemas.
112    pub fn get_schemas(&self) -> Vec<&Schema> {
113        self.schemas.values().collect()
114    }
115
116    /// Get a specific schema by ID.
117    pub fn get_schema(&self, id: &str) -> Option<&Schema> {
118        self.schemas.get(id)
119    }
120
121    /// Get the core User schema.
122    pub fn get_user_schema(&self) -> &Schema {
123        &self.core_user_schema
124    }
125
126    /// Get the core Group schema.
127    pub fn get_group_schema(&self) -> &Schema {
128        &self.core_group_schema
129    }
130
131    /// Add a schema to the registry.
132    pub fn add_schema(&mut self, schema: Schema) -> Result<(), Box<dyn std::error::Error>> {
133        self.schemas.insert(schema.id.clone(), schema);
134        Ok(())
135    }
136
137    /// Get a schema by ID.
138    pub fn get_schema_by_id(&self, schema_id: &str) -> Option<&Schema> {
139        self.schemas.get(schema_id)
140    }
141
142    /// Validate datetime format using chrono for full RFC3339 compliance
143    ///
144    /// This leverages chrono's well-tested RFC3339 parser, which provides:
145    /// - Full semantic validation (no invalid dates like Feb 30th)
146    /// - Proper timezone handling (+/-HH:MM, Z)
147    /// - Millisecond precision support
148    /// - Leap second awareness
149    ///
150    /// By using chrono, we avoid reimplementing complex datetime validation
151    /// and get specification-compliant parsing for free.
152    pub(super) fn is_valid_datetime_format(&self, value: &str) -> bool {
153        if value.is_empty() {
154            return false;
155        }
156
157        // Delegate to chrono's RFC3339 parser - it's well-tested and handles all edge cases
158        DateTime::<FixedOffset>::parse_from_rfc3339(value).is_ok()
159    }
160
161    /// Validate base64 encoding (basic character set validation)
162    ///
163    /// This performs basic character set validation for base64 data.
164    /// For production use, consider using a dedicated base64 crate like `base64`
165    /// for proper padding validation and decode verification.
166    pub(super) fn is_valid_base64(&self, value: &str) -> bool {
167        if value.is_empty() {
168            return false;
169        }
170
171        // Basic character set validation - sufficient for type checking
172        // Note: Doesn't validate padding rules or decode correctness
173        let base64_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
174        value.chars().all(|c| base64_chars.contains(c))
175    }
176
177    /// Validate URI format (basic scheme validation)
178    ///
179    /// This performs basic URI scheme validation sufficient for SCIM reference checking.
180    /// For comprehensive URI validation, consider using the `url` crate.
181    pub(super) fn is_valid_uri_format(&self, value: &str) -> bool {
182        if value.is_empty() {
183            return false;
184        }
185
186        // Basic scheme validation - sufficient for SCIM reference URIs
187        // Accepts HTTP(S) URLs and URN schemes commonly used in SCIM
188        value.contains("://") || value.starts_with("urn:")
189    }
190
191    /// Get the type name of a JSON value for error messages.
192    pub(super) fn get_value_type(value: &Value) -> &'static str {
193        match value {
194            Value::Null => "null",
195            Value::Bool(_) => "boolean",
196            Value::Number(n) if n.is_i64() => "integer",
197            Value::Number(_) => "decimal",
198            Value::String(_) => "string",
199            Value::Array(_) => "array",
200            Value::Object(_) => "object",
201        }
202    }
203
204    /// Get attribute definition for a complex attribute
205    pub(super) fn get_complex_attribute_definition(
206        &self,
207        attr_name: &str,
208    ) -> Option<&AttributeDefinition> {
209        // Look in core user schema for the attribute
210        self.core_user_schema
211            .attributes
212            .iter()
213            .find(|attr| attr.name == attr_name && matches!(attr.data_type, AttributeType::Complex))
214    }
215}
216
217impl Default for SchemaRegistry {
218    fn default() -> Self {
219        Self::new().expect("Failed to load default schemas")
220    }
221}