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