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