scim_server/resource/
builder.rs

1//! Resource builder for type-safe SCIM resource construction.
2//!
3//! This module provides a fluent API for constructing SCIM resources with
4//! compile-time validation and type safety for all value objects.
5
6use crate::error::{ValidationError, ValidationResult};
7use crate::resource::resource::Resource;
8use crate::resource::value_objects::{
9    Address, EmailAddress, ExternalId, GroupMembers, Meta, MultiValuedAddresses, MultiValuedEmails,
10    MultiValuedPhoneNumbers, Name, PhoneNumber, ResourceId, SchemaUri, UserName,
11};
12use serde_json::{Map, Value};
13
14/// Enhanced Resource Builder for type-safe resource construction.
15///
16/// This builder provides a fluent API for constructing SCIM resources with
17/// compile-time validation and type safety for all value objects.
18///
19/// # Example
20/// ```rust
21/// use scim_server::Resource;
22/// use serde_json::json;
23///
24/// fn main() -> Result<(), Box<dyn std::error::Error>> {
25///     let user_data = json!({
26///         "id": "123",
27///         "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
28///         "userName": "jdoe",
29///         "name": {
30///             "givenName": "John",
31///             "familyName": "Doe"
32///         },
33///         "displayName": "John Doe"
34///     });
35///     let resource = Resource::from_json("User".to_string(), user_data)?;
36///
37///     Ok(())
38/// }
39/// ```
40#[derive(Debug, Clone)]
41pub struct ResourceBuilder {
42    resource_type: String,
43    id: Option<ResourceId>,
44    schemas: Vec<SchemaUri>,
45    external_id: Option<ExternalId>,
46    user_name: Option<UserName>,
47    meta: Option<Meta>,
48    name: Option<Name>,
49    addresses: Option<MultiValuedAddresses>,
50    phone_numbers: Option<MultiValuedPhoneNumbers>,
51    emails: Option<MultiValuedEmails>,
52    members: Option<GroupMembers>,
53    attributes: Map<String, Value>,
54}
55
56impl ResourceBuilder {
57    /// Create a new ResourceBuilder with the specified resource type.
58    pub fn new(resource_type: String) -> Self {
59        let mut schemas = Vec::new();
60
61        // Add default schema based on resource type
62        if resource_type == "User" {
63            if let Ok(schema) =
64                SchemaUri::new("urn:ietf:params:scim:schemas:core:2.0:User".to_string())
65            {
66                schemas.push(schema);
67            }
68        } else if resource_type == "Group" {
69            if let Ok(schema) =
70                SchemaUri::new("urn:ietf:params:scim:schemas:core:2.0:Group".to_string())
71            {
72                schemas.push(schema);
73            }
74        }
75
76        Self {
77            resource_type,
78            id: None,
79            schemas,
80            external_id: None,
81            user_name: None,
82            meta: None,
83            name: None,
84            addresses: None,
85            phone_numbers: None,
86            emails: None,
87            members: None,
88            attributes: Map::new(),
89        }
90    }
91
92    /// Set the resource ID.
93    pub fn with_id(mut self, id: ResourceId) -> Self {
94        self.id = Some(id);
95        self
96    }
97
98    /// Set the external ID.
99    pub fn with_external_id(mut self, external_id: ExternalId) -> Self {
100        self.external_id = Some(external_id);
101        self
102    }
103
104    /// Set the username (for User resources).
105    pub fn with_username(mut self, username: UserName) -> Self {
106        self.user_name = Some(username);
107        self
108    }
109
110    /// Set the meta attributes.
111    pub fn with_meta(mut self, meta: Meta) -> Self {
112        self.meta = Some(meta);
113        self
114    }
115
116    /// Set the name (for User resources).
117    pub fn with_name(mut self, name: Name) -> Self {
118        self.name = Some(name);
119        self
120    }
121
122    /// Set addresses for the resource.
123    pub fn with_addresses(mut self, addresses: MultiValuedAddresses) -> Self {
124        self.addresses = Some(addresses);
125        self
126    }
127
128    /// Set phone numbers for the resource.
129    pub fn with_phone_numbers(mut self, phone_numbers: MultiValuedPhoneNumbers) -> Self {
130        self.phone_numbers = Some(phone_numbers);
131        self
132    }
133
134    /// Set emails for the resource.
135    pub fn with_emails(mut self, emails: MultiValuedEmails) -> Self {
136        self.emails = Some(emails);
137        self
138    }
139
140    /// Set group members for the resource.
141    pub fn with_members(mut self, members: GroupMembers) -> Self {
142        self.members = Some(members);
143        self
144    }
145
146    /// Add a single address to the resource.
147    pub fn add_address(mut self, address: Address) -> Self {
148        match self.addresses {
149            Some(existing) => {
150                let new_addresses = existing.with_value(address);
151                self.addresses = Some(new_addresses);
152            }
153            None => {
154                let new_addresses = MultiValuedAddresses::single(address);
155                self.addresses = Some(new_addresses);
156            }
157        }
158        self
159    }
160
161    /// Add a single phone number to the resource.
162    pub fn add_phone_number(mut self, phone_number: PhoneNumber) -> Self {
163        match self.phone_numbers {
164            Some(existing) => {
165                let new_phones = existing.with_value(phone_number);
166                self.phone_numbers = Some(new_phones);
167            }
168            None => {
169                let new_phones = MultiValuedPhoneNumbers::single(phone_number);
170                self.phone_numbers = Some(new_phones);
171            }
172        }
173        self
174    }
175
176    /// Add a single email to the resource.
177    pub fn add_email(mut self, email: EmailAddress) -> Self {
178        match self.emails {
179            Some(existing) => {
180                let new_emails = existing.with_value(email);
181                self.emails = Some(new_emails);
182            }
183            None => {
184                let new_emails = MultiValuedEmails::single(email);
185                self.emails = Some(new_emails);
186            }
187        }
188        self
189    }
190
191    /// Add a schema URI.
192    pub fn add_schema(mut self, schema: SchemaUri) -> Self {
193        self.schemas.push(schema);
194        self
195    }
196
197    /// Set all schema URIs.
198    pub fn with_schemas(mut self, schemas: Vec<SchemaUri>) -> Self {
199        self.schemas = schemas;
200        self
201    }
202
203    /// Add an extended attribute.
204    pub fn with_attribute<S: Into<String>>(mut self, name: S, value: Value) -> Self {
205        self.attributes.insert(name.into(), value);
206        self
207    }
208
209    /// Add multiple extended attributes from a map.
210    pub fn with_attributes(mut self, attributes: Map<String, Value>) -> Self {
211        for (key, value) in attributes {
212            self.attributes.insert(key, value);
213        }
214        self
215    }
216
217    /// Build the Resource.
218    pub fn build(self) -> ValidationResult<Resource> {
219        // Validate that required fields are present
220        if self.schemas.is_empty() {
221            return Err(ValidationError::custom("At least one schema is required"));
222        }
223
224        Ok(Resource {
225            resource_type: self.resource_type,
226            id: self.id,
227            schemas: self.schemas,
228            external_id: self.external_id,
229            user_name: self.user_name,
230            meta: self.meta,
231            name: self.name,
232            addresses: self.addresses,
233            phone_numbers: self.phone_numbers,
234            emails: self.emails,
235            members: self.members,
236            attributes: self.attributes,
237        })
238    }
239
240    /// Build the Resource and create meta attributes for a new resource.
241    pub fn build_with_meta(mut self, base_url: &str) -> ValidationResult<Resource> {
242        // Create meta if not already set
243        if self.meta.is_none() {
244            let meta = Meta::new_for_creation(self.resource_type.clone())?;
245            let meta_with_location = if let Some(ref id) = self.id {
246                let location = Meta::generate_location(base_url, &self.resource_type, id.as_str());
247                meta.with_location(location)?
248            } else {
249                meta
250            };
251            self.meta = Some(meta_with_location);
252        }
253
254        self.build()
255    }
256}