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 =
91            SchemaRegistry::with_embedded_schemas().map_err(|_e| BuildError::SchemaLoadError {
92                schema_id: "Core".to_string(),
93            })?;
94
95        let service_config = ServiceProviderConfig::default();
96
97        let inner = DiscoveryInner {
98            schema_registry,
99            service_config,
100        };
101
102        Ok(SchemaDiscovery {
103            inner: Some(inner),
104            _state: PhantomData,
105        })
106    }
107}
108
109impl SchemaDiscovery<Ready> {
110    // Discovery endpoints
111
112    /// Get all available schemas.
113    ///
114    /// Returns the complete list of schemas supported by this component instance.
115    /// For the MVP, this includes only the core User schema.
116    pub async fn get_schemas(&self) -> ScimResult<Vec<Schema>> {
117        let inner = self.inner.as_ref().expect("Server should be initialized");
118        Ok(inner
119            .schema_registry
120            .get_schemas()
121            .into_iter()
122            .cloned()
123            .collect())
124    }
125
126    /// Get a specific schema by ID.
127    ///
128    /// # Arguments
129    /// * `id` - The schema identifier (URI)
130    ///
131    /// # Returns
132    /// * `Some(Schema)` if the schema exists
133    /// * `None` if the schema is not found
134    pub async fn get_schema(&self, id: &str) -> ScimResult<Option<Schema>> {
135        let inner = self.inner.as_ref().expect("Server should be initialized");
136        Ok(inner.schema_registry.get_schema(id).cloned())
137    }
138
139    /// Get the service provider configuration.
140    ///
141    /// Returns the capabilities and configuration of this SCIM service provider
142    /// as defined in RFC 7644.
143    pub async fn get_service_provider_config(&self) -> ScimResult<ServiceProviderConfig> {
144        let inner = self.inner.as_ref().expect("Server should be initialized");
145        Ok(inner.service_config.clone())
146    }
147
148    /// Get the schema registry for advanced usage.
149    ///
150    /// This provides access to the underlying schema registry for custom validation
151    /// or schema introspection.
152    pub fn schema_registry(&self) -> &SchemaRegistry {
153        let inner = self
154            .inner
155            .as_ref()
156            .expect("Discovery component should be initialized");
157        &inner.schema_registry
158    }
159
160    /// Get the service provider configuration.
161    pub fn service_config(&self) -> &ServiceProviderConfig {
162        let inner = self
163            .inner
164            .as_ref()
165            .expect("Discovery component should be initialized");
166        &inner.service_config
167    }
168}
169
170/// Service provider configuration as defined in RFC 7644.
171///
172/// This structure describes the capabilities and configuration of the SCIM service provider,
173/// allowing clients to discover what features are supported.
174#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
175pub struct ServiceProviderConfig {
176    /// Whether PATCH operations are supported
177    #[serde(rename = "patch")]
178    pub patch_supported: bool,
179
180    /// Whether bulk operations are supported
181    #[serde(rename = "bulk")]
182    pub bulk_supported: bool,
183
184    /// Whether filtering is supported
185    #[serde(rename = "filter")]
186    pub filter_supported: bool,
187
188    /// Whether password change operations are supported
189    #[serde(rename = "changePassword")]
190    pub change_password_supported: bool,
191
192    /// Whether sorting is supported
193    #[serde(rename = "sort")]
194    pub sort_supported: bool,
195
196    /// Whether ETags are supported for versioning
197    #[serde(rename = "etag")]
198    pub etag_supported: bool,
199
200    /// Authentication schemes supported
201    #[serde(rename = "authenticationSchemes")]
202    pub authentication_schemes: Vec<AuthenticationScheme>,
203
204    /// Maximum number of operations in a bulk request
205    #[serde(rename = "bulk.maxOperations")]
206    pub bulk_max_operations: Option<u32>,
207
208    /// Maximum payload size for bulk operations
209    #[serde(rename = "bulk.maxPayloadSize")]
210    pub bulk_max_payload_size: Option<u64>,
211
212    /// Maximum number of resources returned in a query
213    #[serde(rename = "filter.maxResults")]
214    pub filter_max_results: Option<u32>,
215}
216
217impl Default for ServiceProviderConfig {
218    fn default() -> Self {
219        Self {
220            patch_supported: false,
221            bulk_supported: false,
222            filter_supported: false,
223            change_password_supported: false,
224            sort_supported: false,
225            etag_supported: false,
226            authentication_schemes: vec![],
227            bulk_max_operations: None,
228            bulk_max_payload_size: None,
229            filter_max_results: Some(200),
230        }
231    }
232}
233
234/// Authentication scheme definition for service provider config.
235#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
236pub struct AuthenticationScheme {
237    /// Authentication scheme name
238    pub name: String,
239    /// Human-readable description
240    pub description: String,
241    /// URI for more information
242    #[serde(rename = "specUri")]
243    pub spec_uri: Option<String>,
244    /// URI for documentation
245    #[serde(rename = "documentationUri")]
246    pub documentation_uri: Option<String>,
247    /// Authentication type (e.g., "oauth2", "httpbasic")
248    #[serde(rename = "type")]
249    pub auth_type: String,
250    /// Whether this scheme is the primary authentication method
251    pub primary: bool,
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257
258    #[tokio::test]
259    async fn test_discovery_creation() {
260        let discovery = SchemaDiscovery::new().expect("Failed to create discovery component");
261
262        // Test that the component can access schemas
263        let schemas = discovery
264            .get_schemas()
265            .await
266            .expect("Failed to get schemas");
267        assert!(!schemas.is_empty());
268    }
269
270    #[tokio::test]
271    async fn test_schema_access() {
272        let discovery = SchemaDiscovery::new().expect("Failed to create discovery component");
273
274        // Test schema retrieval
275        let user_schema = discovery
276            .get_schema("urn:ietf:params:scim:schemas:core:2.0:User")
277            .await
278            .expect("Failed to get schema");
279
280        assert!(user_schema.is_some());
281        if let Some(schema) = user_schema {
282            assert_eq!(schema.name, "User");
283        }
284    }
285
286    #[test]
287    fn test_service_provider_config() {
288        let config = ServiceProviderConfig::default();
289        assert!(!config.patch_supported);
290        assert!(!config.bulk_supported);
291        assert!(!config.filter_supported);
292    }
293
294    #[tokio::test]
295    async fn test_tutorial_example_works() {
296        // This test verifies that the exact tutorial example from the documentation works
297        // and addresses the critical issue found in schema-discovery-test-2025-08-15.md
298
299        // The tutorial example should work without panicking
300        let discovery = SchemaDiscovery::new()
301            .expect("SchemaDiscovery::new() should work with embedded schemas");
302
303        // Get available schemas
304        let schemas = discovery
305            .get_schemas()
306            .await
307            .expect("get_schemas() should work");
308        assert!(
309            !schemas.is_empty(),
310            "Should have at least one schema available"
311        );
312        println!("Available schemas: {}", schemas.len());
313
314        // Get service provider configuration
315        let config = discovery
316            .get_service_provider_config()
317            .await
318            .expect("get_service_provider_config() should work");
319        println!("Bulk operations supported: {}", config.bulk_supported);
320
321        // Verify we can access specific schemas
322        let user_schema = discovery
323            .get_schema("urn:ietf:params:scim:schemas:core:2.0:User")
324            .await
325            .expect("Should be able to get User schema");
326        assert!(user_schema.is_some(), "User schema should be available");
327
328        // This test confirms the fix for the critical SchemaLoadError issue
329        // identified in the test results where SchemaDiscovery::new() was failing
330    }
331}