scim_server/
schema_discovery.rs

1//! Schema discovery implementation with state machine design.
2//!
3//! This module implements a specialized SCIM component for schema discovery and service provider
4//! configuration using a type-parameterized state machine to ensure compile-time safety.
5//! This component is designed specifically for schema introspection, not for
6//! resource CRUD operations. For full SCIM resource management, use ScimServer.
7
8use crate::error::{BuildError, BuildResult, ScimResult};
9
10use crate::schema::{Schema, SchemaRegistry};
11use serde::{Deserialize, Serialize};
12
13use std::marker::PhantomData;
14
15/// State marker for uninitialized discovery component.
16///
17/// This state prevents any SCIM operations until the component is properly configured.
18#[derive(Debug)]
19pub struct Uninitialized;
20
21/// State marker for fully configured and ready discovery component.
22///
23/// Only components in this state can perform SCIM operations.
24#[derive(Debug)]
25pub struct Ready;
26
27/// Schema discovery component with state machine design.
28///
29/// The component uses phantom types to encode its configuration state at compile time,
30/// preventing invalid operations and ensuring proper initialization sequence.
31/// This component is specifically designed for schema discovery and service provider
32/// configuration, not for resource CRUD operations.
33///
34/// # Type Parameters
35/// * `State` - The current state of the component (Uninitialized or Ready)
36///
37/// # Example
38/// ```rust,no_run
39/// use scim_server::SchemaDiscovery;
40///
41/// #[tokio::main]
42/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
43///     // Create a schema discovery component
44///     let discovery = SchemaDiscovery::new()?;
45///
46///     // Get available schemas
47///     let schemas = discovery.get_schemas().await?;
48///     println!("Available schemas: {}", schemas.len());
49///
50///     // For resource CRUD operations, use ScimServer instead
51///     Ok(())
52/// }
53/// ```
54pub struct SchemaDiscovery<State = Ready> {
55    inner: Option<DiscoveryInner>,
56    _state: PhantomData<State>,
57}
58
59impl<State> std::fmt::Debug for SchemaDiscovery<State> {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        f.debug_struct("SchemaDiscovery")
62            .field("inner", &self.inner.is_some())
63            .field("state", &std::any::type_name::<State>())
64            .finish()
65    }
66}
67
68/// Internal discovery state shared across all component instances.
69struct DiscoveryInner {
70    schema_registry: SchemaRegistry,
71    service_config: ServiceProviderConfig,
72}
73
74impl std::fmt::Debug for DiscoveryInner {
75    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76        f.debug_struct("DiscoveryInner")
77            .field("schema_registry", &"SchemaRegistry")
78            .field("service_config", &self.service_config)
79            .finish()
80    }
81}
82
83impl SchemaDiscovery<Uninitialized> {
84    /// Create a new schema discovery component.
85    ///
86    /// This creates a component with default configuration and schema registry
87    /// for schema discovery and service provider configuration.
88    /// For resource CRUD operations, use ScimServer instead.
89    pub fn new() -> BuildResult<SchemaDiscovery<Ready>> {
90        let schema_registry = SchemaRegistry::new().map_err(|_e| BuildError::SchemaLoadError {
91            schema_id: "Core".to_string(),
92        })?;
93
94        let service_config = ServiceProviderConfig::default();
95
96        let inner = DiscoveryInner {
97            schema_registry,
98            service_config,
99        };
100
101        Ok(SchemaDiscovery {
102            inner: Some(inner),
103            _state: PhantomData,
104        })
105    }
106}
107
108impl SchemaDiscovery<Ready> {
109    // Discovery endpoints
110
111    /// Get all available schemas.
112    ///
113    /// Returns the complete list of schemas supported by this component instance.
114    /// For the MVP, this includes only the core User schema.
115    pub async fn get_schemas(&self) -> ScimResult<Vec<Schema>> {
116        let inner = self.inner.as_ref().expect("Server should be initialized");
117        Ok(inner
118            .schema_registry
119            .get_schemas()
120            .into_iter()
121            .cloned()
122            .collect())
123    }
124
125    /// Get a specific schema by ID.
126    ///
127    /// # Arguments
128    /// * `id` - The schema identifier (URI)
129    ///
130    /// # Returns
131    /// * `Some(Schema)` if the schema exists
132    /// * `None` if the schema is not found
133    pub async fn get_schema(&self, id: &str) -> ScimResult<Option<Schema>> {
134        let inner = self.inner.as_ref().expect("Server should be initialized");
135        Ok(inner.schema_registry.get_schema(id).cloned())
136    }
137
138    /// Get the service provider configuration.
139    ///
140    /// Returns the capabilities and configuration of this SCIM service provider
141    /// as defined in RFC 7644.
142    pub async fn get_service_provider_config(&self) -> ScimResult<ServiceProviderConfig> {
143        let inner = self.inner.as_ref().expect("Server should be initialized");
144        Ok(inner.service_config.clone())
145    }
146
147    /// Get the schema registry for advanced usage.
148    ///
149    /// This provides access to the underlying schema registry for custom validation
150    /// or schema introspection.
151    pub fn schema_registry(&self) -> &SchemaRegistry {
152        let inner = self
153            .inner
154            .as_ref()
155            .expect("Discovery component should be initialized");
156        &inner.schema_registry
157    }
158
159    /// Get the service provider configuration.
160    pub fn service_config(&self) -> &ServiceProviderConfig {
161        let inner = self
162            .inner
163            .as_ref()
164            .expect("Discovery component should be initialized");
165        &inner.service_config
166    }
167}
168
169/// Service provider configuration as defined in RFC 7644.
170///
171/// This structure describes the capabilities and configuration of the SCIM service provider,
172/// allowing clients to discover what features are supported.
173#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
174pub struct ServiceProviderConfig {
175    /// Whether PATCH operations are supported
176    #[serde(rename = "patch")]
177    pub patch_supported: bool,
178
179    /// Whether bulk operations are supported
180    #[serde(rename = "bulk")]
181    pub bulk_supported: bool,
182
183    /// Whether filtering is supported
184    #[serde(rename = "filter")]
185    pub filter_supported: bool,
186
187    /// Whether password change operations are supported
188    #[serde(rename = "changePassword")]
189    pub change_password_supported: bool,
190
191    /// Whether sorting is supported
192    #[serde(rename = "sort")]
193    pub sort_supported: bool,
194
195    /// Whether ETags are supported for versioning
196    #[serde(rename = "etag")]
197    pub etag_supported: bool,
198
199    /// Authentication schemes supported
200    #[serde(rename = "authenticationSchemes")]
201    pub authentication_schemes: Vec<AuthenticationScheme>,
202
203    /// Maximum number of operations in a bulk request
204    #[serde(rename = "bulk.maxOperations")]
205    pub bulk_max_operations: Option<u32>,
206
207    /// Maximum payload size for bulk operations
208    #[serde(rename = "bulk.maxPayloadSize")]
209    pub bulk_max_payload_size: Option<u64>,
210
211    /// Maximum number of resources returned in a query
212    #[serde(rename = "filter.maxResults")]
213    pub filter_max_results: Option<u32>,
214}
215
216impl Default for ServiceProviderConfig {
217    fn default() -> Self {
218        Self {
219            patch_supported: false,
220            bulk_supported: false,
221            filter_supported: false,
222            change_password_supported: false,
223            sort_supported: false,
224            etag_supported: false,
225            authentication_schemes: vec![],
226            bulk_max_operations: None,
227            bulk_max_payload_size: None,
228            filter_max_results: Some(200),
229        }
230    }
231}
232
233/// Authentication scheme definition for service provider config.
234#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
235pub struct AuthenticationScheme {
236    /// Authentication scheme name
237    pub name: String,
238    /// Human-readable description
239    pub description: String,
240    /// URI for more information
241    #[serde(rename = "specUri")]
242    pub spec_uri: Option<String>,
243    /// URI for documentation
244    #[serde(rename = "documentationUri")]
245    pub documentation_uri: Option<String>,
246    /// Authentication type (e.g., "oauth2", "httpbasic")
247    #[serde(rename = "type")]
248    pub auth_type: String,
249    /// Whether this scheme is the primary authentication method
250    pub primary: bool,
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256
257    #[tokio::test]
258    async fn test_discovery_creation() {
259        let discovery = SchemaDiscovery::new().expect("Failed to create discovery component");
260
261        // Test that the component can access schemas
262        let schemas = discovery
263            .get_schemas()
264            .await
265            .expect("Failed to get schemas");
266        assert!(!schemas.is_empty());
267    }
268
269    #[tokio::test]
270    async fn test_schema_access() {
271        let discovery = SchemaDiscovery::new().expect("Failed to create discovery component");
272
273        // Test schema retrieval
274        let user_schema = discovery
275            .get_schema("urn:ietf:params:scim:schemas:core:2.0:User")
276            .await
277            .expect("Failed to get schema");
278
279        assert!(user_schema.is_some());
280        if let Some(schema) = user_schema {
281            assert_eq!(schema.name, "User");
282        }
283    }
284
285    #[test]
286    fn test_service_provider_config() {
287        let config = ServiceProviderConfig::default();
288        assert!(!config.patch_supported);
289        assert!(!config.bulk_supported);
290        assert!(!config.filter_supported);
291    }
292}