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    pub fn get_group_schema(&self) -> &Schema {
127        &self.core_group_schema
128    }
129
130    /// Add a schema to the registry.
131    pub fn add_schema(&mut self, schema: Schema) -> Result<(), Box<dyn std::error::Error>> {
132        self.schemas.insert(schema.id.clone(), schema);
133        Ok(())
134    }
135
136    /// Get a schema by ID.
137    pub fn get_schema_by_id(&self, schema_id: &str) -> Option<&Schema> {
138        self.schemas.get(schema_id)
139    }
140
141    /// Validate datetime format using chrono for full RFC3339 compliance
142    ///
143    /// This leverages chrono's well-tested RFC3339 parser, which provides:
144    /// - Full semantic validation (no invalid dates like Feb 30th)
145    /// - Proper timezone handling (+/-HH:MM, Z)
146    /// - Millisecond precision support
147    /// - Leap second awareness
148    ///
149    /// By using chrono, we avoid reimplementing complex datetime validation
150    /// and get specification-compliant parsing for free.
151    pub(super) fn is_valid_datetime_format(&self, value: &str) -> bool {
152        if value.is_empty() {
153            return false;
154        }
155
156        // Delegate to chrono's RFC3339 parser - it's well-tested and handles all edge cases
157        DateTime::<FixedOffset>::parse_from_rfc3339(value).is_ok()
158    }
159
160    /// Validate base64 encoding (basic character set validation)
161    ///
162    /// This performs basic character set validation for base64 data.
163    /// For production use, consider using a dedicated base64 crate like `base64`
164    /// for proper padding validation and decode verification.
165    pub(super) fn is_valid_base64(&self, value: &str) -> bool {
166        if value.is_empty() {
167            return false;
168        }
169
170        // Basic character set validation - sufficient for type checking
171        // Note: Doesn't validate padding rules or decode correctness
172        let base64_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
173        value.chars().all(|c| base64_chars.contains(c))
174    }
175
176    /// Validate URI format (basic scheme validation)
177    ///
178    /// This performs basic URI scheme validation sufficient for SCIM reference checking.
179    /// For comprehensive URI validation, consider using the `url` crate.
180    pub(super) fn is_valid_uri_format(&self, value: &str) -> bool {
181        if value.is_empty() {
182            return false;
183        }
184
185        // Basic scheme validation - sufficient for SCIM reference URIs
186        // Accepts HTTP(S) URLs and URN schemes commonly used in SCIM
187        value.contains("://") || value.starts_with("urn:")
188    }
189
190    /// Get the type name of a JSON value for error messages.
191    pub(super) fn get_value_type(value: &Value) -> &'static str {
192        match value {
193            Value::Null => "null",
194            Value::Bool(_) => "boolean",
195            Value::Number(n) if n.is_i64() => "integer",
196            Value::Number(_) => "decimal",
197            Value::String(_) => "string",
198            Value::Array(_) => "array",
199            Value::Object(_) => "object",
200        }
201    }
202
203    /// Get attribute definition for a complex attribute
204    pub(super) fn get_complex_attribute_definition(
205        &self,
206        attr_name: &str,
207    ) -> Option<&AttributeDefinition> {
208        // Look in core user schema for the attribute
209        self.core_user_schema
210            .attributes
211            .iter()
212            .find(|attr| attr.name == attr_name && matches!(attr.data_type, AttributeType::Complex))
213    }
214}
215
216impl Default for SchemaRegistry {
217    fn default() -> Self {
218        Self::new().expect("Failed to load default schemas")
219    }
220}