scim_server/resource/
resource.rs

1//! Core SCIM resource representation and validation.
2//!
3//! This module contains the main Resource struct and its associated methods
4//! for creating, validating, and manipulating SCIM resources with type safety
5//! for core attributes while maintaining JSON flexibility for extensions.
6
7use crate::error::{ValidationError, ValidationResult};
8use crate::resource::value_objects::{
9    Address, EmailAddress, ExternalId, GroupMembers, Meta, MultiValuedAddresses, MultiValuedEmails,
10    MultiValuedPhoneNumbers, Name, PhoneNumber, ResourceId, SchemaUri, UserName,
11};
12
13use serde_json::{Map, Value};
14
15/// Generic SCIM resource representation with type-safe core attributes.
16///
17/// This hybrid design uses value objects for core validated primitives while
18/// maintaining JSON flexibility for extensible attributes. The design ensures
19/// compile-time safety for critical fields while preserving SCIM's extensibility.
20#[derive(Debug, Clone)]
21pub struct Resource {
22    /// The type of this resource (e.g., "User", "Group")
23    pub resource_type: String,
24    /// Validated resource identifier (required for most operations)
25    pub id: Option<ResourceId>,
26    /// Validated schema URIs
27    pub schemas: Vec<SchemaUri>,
28    /// Validated external identifier (optional)
29    pub external_id: Option<ExternalId>,
30    /// Validated username (for User resources)
31    pub user_name: Option<UserName>,
32    /// Validated meta attributes (optional)
33    pub meta: Option<Meta>,
34    /// Validated name attributes (for User resources)
35    pub name: Option<Name>,
36    /// Validated addresses (multi-valued with primary support)
37    pub addresses: Option<MultiValuedAddresses>,
38    /// Validated phone numbers (multi-valued with primary support)
39    pub phone_numbers: Option<MultiValuedPhoneNumbers>,
40    /// Validated email addresses (multi-valued with primary support)
41    pub emails: Option<MultiValuedEmails>,
42    /// Group members (for Group resources)
43    pub members: Option<GroupMembers>,
44    /// Extended attributes and complex data as JSON
45    pub attributes: Map<String, Value>,
46}
47
48impl Resource {
49    /// Create a new resource from validated JSON data.
50    ///
51    /// This method extracts and validates core primitives while preserving
52    /// other attributes in the flexible JSON structure.
53    ///
54    /// # Arguments
55    /// * `resource_type` - The SCIM resource type identifier
56    /// * `data` - The resource data as a JSON value
57    ///
58    /// # Example
59    /// ```rust
60    /// use scim_server::Resource;
61    /// use serde_json::json;
62    ///
63    /// fn main() -> Result<(), Box<dyn std::error::Error>> {
64    ///     let user_data = json!({
65    ///         "id": "12345",
66    ///         "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
67    ///         "userName": "jdoe",
68    ///         "displayName": "John Doe"
69    ///     });
70    ///     let resource = Resource::from_json("User".to_string(), user_data)?;
71    ///
72    ///     Ok(())
73    /// }
74    /// ```
75    pub fn from_json(resource_type: String, data: Value) -> ValidationResult<Self> {
76        let obj = data
77            .as_object()
78            .ok_or_else(|| ValidationError::custom("Resource must be a JSON object"))?;
79
80        // Extract and validate core primitives
81        let id = Self::extract_resource_id(obj)?;
82        let schemas = Self::extract_schemas(obj, &resource_type)?;
83        let external_id = Self::extract_external_id(obj)?;
84        let user_name = Self::extract_user_name(obj)?;
85        let meta = Self::extract_meta(&data)?;
86        let name = Self::extract_name(obj)?;
87        let addresses = Self::extract_addresses(obj)?;
88        let phone_numbers = Self::extract_phone_numbers(obj)?;
89        let emails = Self::extract_emails(obj)?;
90        let members = Self::extract_members(obj)?;
91
92        // Collect remaining attributes (excluding core primitives)
93        let mut attributes = obj.clone();
94        attributes.remove("id");
95        attributes.remove("schemas");
96        attributes.remove("externalId");
97        attributes.remove("userName");
98        attributes.remove("meta");
99        attributes.remove("name");
100        attributes.remove("addresses");
101        attributes.remove("phoneNumbers");
102        attributes.remove("emails");
103        attributes.remove("members");
104
105        Ok(Self {
106            resource_type,
107            id,
108            schemas,
109            external_id,
110            user_name,
111            meta,
112            name,
113            addresses,
114            phone_numbers,
115            emails,
116            members,
117            attributes,
118        })
119    }
120
121    /// Create a new resource with validated core fields.
122    ///
123    /// This is the preferred constructor for new resources where core fields
124    /// are already validated.
125    pub fn new(
126        resource_type: String,
127        id: Option<ResourceId>,
128        schemas: Vec<SchemaUri>,
129        external_id: Option<ExternalId>,
130        user_name: Option<UserName>,
131        attributes: Map<String, Value>,
132    ) -> Self {
133        Self {
134            resource_type,
135            id,
136            schemas,
137            external_id,
138            user_name,
139            meta: None,
140            name: None,
141            addresses: None,
142            phone_numbers: None,
143            emails: None,
144            members: None,
145            attributes,
146        }
147    }
148
149    /// Create a new resource with validated core fields including meta.
150    ///
151    /// Extended constructor that includes meta attributes.
152    pub fn new_with_meta(
153        resource_type: String,
154        id: Option<ResourceId>,
155        schemas: Vec<SchemaUri>,
156        external_id: Option<ExternalId>,
157        user_name: Option<UserName>,
158        meta: Option<Meta>,
159        attributes: Map<String, Value>,
160    ) -> Self {
161        Self {
162            resource_type,
163            id,
164            schemas,
165            external_id,
166            user_name,
167            meta,
168            name: None,
169            addresses: None,
170            phone_numbers: None,
171            emails: None,
172            members: None,
173            attributes,
174        }
175    }
176
177    /// Extract and validate resource ID from JSON
178    fn extract_resource_id(obj: &Map<String, Value>) -> ValidationResult<Option<ResourceId>> {
179        if let Some(id_value) = obj.get("id") {
180            if let Some(id_str) = id_value.as_str() {
181                return Ok(Some(ResourceId::new(id_str.to_string())?));
182            } else {
183                return Err(ValidationError::InvalidIdFormat {
184                    id: id_value.to_string(),
185                });
186            }
187        }
188        Ok(None)
189    }
190
191    /// Extract and validate schemas from JSON
192    fn extract_schemas(
193        obj: &Map<String, Value>,
194        resource_type: &str,
195    ) -> ValidationResult<Vec<SchemaUri>> {
196        if let Some(schemas_value) = obj.get("schemas") {
197            if let Some(schemas_array) = schemas_value.as_array() {
198                if schemas_array.is_empty() {
199                    return Err(ValidationError::EmptySchemas);
200                }
201
202                let mut schemas = Vec::new();
203                for schema_value in schemas_array {
204                    if let Some(uri_str) = schema_value.as_str() {
205                        schemas.push(SchemaUri::new(uri_str.to_string())?);
206                    }
207                }
208                if !schemas.is_empty() {
209                    return Ok(schemas);
210                }
211            }
212        }
213
214        // Default schema based on resource type
215        let default_uri = match resource_type {
216            "User" => "urn:ietf:params:scim:schemas:core:2.0:User",
217            "Group" => "urn:ietf:params:scim:schemas:core:2.0:Group",
218            _ => return Err(ValidationError::custom("Unknown resource type")),
219        };
220
221        Ok(vec![SchemaUri::new(default_uri.to_string())?])
222    }
223
224    /// Extract and validate external ID from JSON
225    fn extract_external_id(obj: &Map<String, Value>) -> ValidationResult<Option<ExternalId>> {
226        if let Some(ext_id_value) = obj.get("externalId") {
227            if let Some(ext_id_str) = ext_id_value.as_str() {
228                return Ok(Some(ExternalId::new(ext_id_str.to_string())?));
229            } else {
230                return Err(ValidationError::InvalidExternalId);
231            }
232        }
233        Ok(None)
234    }
235
236    /// Extract and validate username from JSON
237    fn extract_user_name(obj: &Map<String, Value>) -> ValidationResult<Option<UserName>> {
238        if let Some(username_value) = obj.get("userName") {
239            if let Some(username_str) = username_value.as_str() {
240                return Ok(Some(UserName::new(username_str.to_string())?));
241            } else {
242                return Err(ValidationError::custom(
243                    "userName must be a string".to_string(),
244                ));
245            }
246        }
247        Ok(None)
248    }
249
250    /// Extract and validate name from JSON
251    fn extract_name(obj: &Map<String, Value>) -> ValidationResult<Option<Name>> {
252        if let Some(name_value) = obj.get("name") {
253            if let Some(_) = name_value.as_object() {
254                // Deserialize using serde
255                let name: Name = serde_json::from_value(name_value.clone())
256                    .map_err(|e| ValidationError::custom(format!("Invalid name format: {}", e)))?;
257                return Ok(Some(name));
258            } else {
259                return Err(ValidationError::custom(
260                    "name must be an object".to_string(),
261                ));
262            }
263        }
264        Ok(None)
265    }
266
267    /// Extract and validate addresses from JSON
268    fn extract_addresses(
269        obj: &Map<String, Value>,
270    ) -> ValidationResult<Option<MultiValuedAddresses>> {
271        if let Some(addresses_value) = obj.get("addresses") {
272            if let Some(_) = addresses_value.as_array() {
273                // Deserialize using serde
274                let addresses: Vec<Address> = serde_json::from_value(addresses_value.clone())
275                    .map_err(|e| {
276                        ValidationError::custom(format!("Invalid addresses format: {}", e))
277                    })?;
278                if !addresses.is_empty() {
279                    let multi_addresses = MultiValuedAddresses::new(addresses)?;
280                    return Ok(Some(multi_addresses));
281                }
282            } else {
283                return Err(ValidationError::custom(
284                    "addresses must be an array".to_string(),
285                ));
286            }
287        }
288        Ok(None)
289    }
290
291    /// Extract and validate phone numbers from JSON
292    fn extract_phone_numbers(
293        obj: &Map<String, Value>,
294    ) -> ValidationResult<Option<MultiValuedPhoneNumbers>> {
295        if let Some(phones_value) = obj.get("phoneNumbers") {
296            if let Some(_) = phones_value.as_array() {
297                // Deserialize using serde
298                let phone_numbers: Vec<PhoneNumber> = serde_json::from_value(phones_value.clone())
299                    .map_err(|e| {
300                        ValidationError::custom(format!("Invalid phoneNumbers format: {}", e))
301                    })?;
302                if !phone_numbers.is_empty() {
303                    let multi_phones = MultiValuedPhoneNumbers::new(phone_numbers)?;
304                    return Ok(Some(multi_phones));
305                }
306            } else {
307                return Err(ValidationError::custom(
308                    "phoneNumbers must be an array".to_string(),
309                ));
310            }
311        }
312        Ok(None)
313    }
314
315    /// Extract and validate email addresses from JSON
316    fn extract_emails(obj: &Map<String, Value>) -> ValidationResult<Option<MultiValuedEmails>> {
317        if let Some(emails_value) = obj.get("emails") {
318            if let Some(_) = emails_value.as_array() {
319                // Deserialize using serde
320                let emails: Vec<EmailAddress> = serde_json::from_value(emails_value.clone())
321                    .map_err(|e| {
322                        ValidationError::custom(format!("Invalid emails format: {}", e))
323                    })?;
324                if !emails.is_empty() {
325                    let multi_emails = MultiValuedEmails::new(emails)?;
326                    return Ok(Some(multi_emails));
327                }
328            } else {
329                return Err(ValidationError::custom(
330                    "emails must be an array".to_string(),
331                ));
332            }
333        }
334        Ok(None)
335    }
336
337    /// Extract and validate group members from JSON
338    fn extract_members(obj: &Map<String, Value>) -> ValidationResult<Option<GroupMembers>> {
339        if let Some(members_value) = obj.get("members") {
340            if let Some(_) = members_value.as_array() {
341                // Deserialize using serde to get the raw member data
342                let members_data: Vec<serde_json::Value> =
343                    serde_json::from_value(members_value.clone()).map_err(|e| {
344                        ValidationError::custom(format!("Invalid members format: {}", e))
345                    })?;
346
347                let mut members = Vec::new();
348                for member_data in members_data {
349                    if let Some(obj) = member_data.as_object() {
350                        if let Some(value_str) = obj.get("value").and_then(|v| v.as_str()) {
351                            let resource_id = ResourceId::new(value_str.to_string())?;
352                            let display = obj
353                                .get("display")
354                                .and_then(|v| v.as_str())
355                                .map(|s| s.to_string());
356                            let member_type = obj
357                                .get("type")
358                                .and_then(|v| v.as_str())
359                                .map(|s| s.to_string());
360
361                            let member = crate::resource::value_objects::GroupMember::new(
362                                resource_id,
363                                display,
364                                member_type,
365                            )?;
366                            members.push(member);
367                        }
368                    }
369                }
370
371                if !members.is_empty() {
372                    let group_members = GroupMembers::new(members)?;
373                    return Ok(Some(group_members));
374                }
375            } else {
376                return Err(ValidationError::custom(
377                    "members must be an array".to_string(),
378                ));
379            }
380        }
381        Ok(None)
382    }
383
384    /// Get the unique identifier of this resource.
385    pub fn get_id(&self) -> Option<&str> {
386        self.id.as_ref().map(|id| id.as_str())
387    }
388
389    /// Set the unique identifier of this resource.
390    pub fn set_id(&mut self, id: &str) -> ValidationResult<()> {
391        self.id = Some(ResourceId::new(id.to_string())?);
392        Ok(())
393    }
394
395    /// Get an attribute value from the resource.
396    pub fn get(&self, key: &str) -> Option<&Value> {
397        self.attributes.get(key)
398    }
399
400    /// Get the userName field for User resources.
401    pub fn get_username(&self) -> Option<&str> {
402        self.user_name.as_ref().map(|name| name.as_str())
403    }
404
405    /// Get the name field for User resources.
406    pub fn get_name(&self) -> Option<&Name> {
407        self.name.as_ref()
408    }
409
410    /// Get all addresses for the resource.
411    pub fn get_addresses(&self) -> Option<&MultiValuedAddresses> {
412        self.addresses.as_ref()
413    }
414
415    /// Get all phone numbers for the resource.
416    pub fn get_phone_numbers(&self) -> Option<&MultiValuedPhoneNumbers> {
417        self.phone_numbers.as_ref()
418    }
419
420    /// Get all emails for the resource.
421    pub fn get_emails(&self) -> Option<&MultiValuedEmails> {
422        self.emails.as_ref()
423    }
424
425    /// Get all group members for the resource.
426    pub fn get_members(&self) -> Option<&GroupMembers> {
427        self.members.as_ref()
428    }
429
430    /// Set the name for the resource.
431    pub fn set_name(&mut self, name: Name) {
432        self.name = Some(name);
433    }
434
435    /// Set addresses for the resource.
436    pub fn set_addresses(&mut self, addresses: MultiValuedAddresses) {
437        self.addresses = Some(addresses);
438    }
439
440    /// Set phone numbers for the resource.
441    pub fn set_phone_numbers(&mut self, phone_numbers: MultiValuedPhoneNumbers) {
442        self.phone_numbers = Some(phone_numbers);
443    }
444
445    /// Set emails for the resource.
446    pub fn set_emails(&mut self, emails: MultiValuedEmails) {
447        self.emails = Some(emails);
448    }
449
450    /// Set group members for the resource.
451    pub fn set_members(&mut self, members: GroupMembers) {
452        self.members = Some(members);
453    }
454
455    /// Add an address to the resource.
456    pub fn add_address(&mut self, address: Address) -> ValidationResult<()> {
457        match &self.addresses {
458            Some(existing) => {
459                let new_addresses = existing.clone().with_value(address);
460                self.addresses = Some(new_addresses);
461            }
462            None => {
463                let new_addresses = MultiValuedAddresses::single(address);
464                self.addresses = Some(new_addresses);
465            }
466        }
467        Ok(())
468    }
469
470    /// Add a phone number to the resource.
471    pub fn add_phone_number(&mut self, phone_number: PhoneNumber) -> ValidationResult<()> {
472        match &self.phone_numbers {
473            Some(existing) => {
474                let new_phones = existing.clone().with_value(phone_number);
475                self.phone_numbers = Some(new_phones);
476            }
477            None => {
478                let new_phones = MultiValuedPhoneNumbers::single(phone_number);
479                self.phone_numbers = Some(new_phones);
480            }
481        }
482        Ok(())
483    }
484
485    /// Add an email to the resource.
486    pub fn add_email(&mut self, email: EmailAddress) -> ValidationResult<()> {
487        match &self.emails {
488            Some(existing) => {
489                let new_emails = existing.clone().with_value(email);
490                self.emails = Some(new_emails);
491            }
492            None => {
493                let new_emails = MultiValuedEmails::single(email);
494                self.emails = Some(new_emails);
495            }
496        }
497        Ok(())
498    }
499
500    /// Get a specific attribute value from the extended attributes.
501    ///
502    /// # Arguments
503    /// * `attribute_name` - The name of the attribute to retrieve
504    pub fn get_attribute(&self, attribute_name: &str) -> Option<&Value> {
505        self.attributes.get(attribute_name)
506    }
507
508    /// Set a specific attribute value in the extended attributes.
509    ///
510    /// # Arguments
511    /// * `attribute_name` - The name of the attribute to set
512    /// * `value` - The value to set
513    pub fn set_attribute(&mut self, attribute_name: String, value: Value) {
514        self.attributes.insert(attribute_name, value);
515    }
516
517    /// Get the schemas associated with this resource.
518    pub fn get_schemas(&self) -> Vec<String> {
519        self.schemas
520            .iter()
521            .map(|s| s.as_str().to_string())
522            .collect()
523    }
524
525    /// Get the validated schema URIs.
526    pub fn get_schema_uris(&self) -> &[SchemaUri] {
527        &self.schemas
528    }
529
530    /// Add metadata to the resource.
531    ///
532    /// This method sets common SCIM metadata fields like resourceType,
533    /// created, lastModified, and location using the new Meta value object.
534    ///
535    /// # Deprecated
536    /// This method is deprecated in favor of `create_meta()` which uses type-safe Meta value objects.
537    pub fn add_metadata(&mut self, base_url: &str, created: &str, last_modified: &str) {
538        // Parse timestamps
539        let created_dt = chrono::DateTime::parse_from_rfc3339(created)
540            .map(|dt| dt.with_timezone(&chrono::Utc))
541            .unwrap_or_else(|_| chrono::Utc::now());
542
543        let last_modified_dt = chrono::DateTime::parse_from_rfc3339(last_modified)
544            .map(|dt| dt.with_timezone(&chrono::Utc))
545            .unwrap_or_else(|_| chrono::Utc::now());
546
547        let location = if let Some(id) = &self.id {
548            Some(Meta::generate_location(
549                base_url,
550                &self.resource_type,
551                id.as_str(),
552            ))
553        } else {
554            None
555        };
556
557        let version = if let Some(id) = &self.id {
558            Some(Meta::generate_version(id.as_str(), last_modified_dt))
559        } else {
560            None
561        };
562
563        // Create Meta value object (ignore validation errors for backward compatibility)
564        if let Ok(meta) = Meta::new(
565            self.resource_type.clone(),
566            created_dt,
567            last_modified_dt,
568            location,
569            version,
570        ) {
571            self.set_meta(meta);
572        }
573    }
574
575    /// Check if this resource is active.
576    ///
577    /// Returns the value of the "active" field, defaulting to true if not present.
578    pub fn is_active(&self) -> bool {
579        self.attributes
580            .get("active")
581            .and_then(|v| v.as_bool())
582            .unwrap_or(true)
583    }
584
585    /// Convert the resource to JSON format for serialization.
586    ///
587    /// This combines the type-safe core fields with the extended attributes
588    /// into a single JSON object.
589    pub fn to_json(&self) -> ValidationResult<Value> {
590        let mut result = self.attributes.clone();
591
592        // Add core fields
593        if let Some(ref id) = self.id {
594            result.insert("id".to_string(), Value::String(id.as_str().to_string()));
595        }
596
597        let schemas: Vec<Value> = self
598            .schemas
599            .iter()
600            .map(|s| Value::String(s.as_str().to_string()))
601            .collect();
602        result.insert("schemas".to_string(), Value::Array(schemas));
603
604        if let Some(ref external_id) = self.external_id {
605            result.insert(
606                "externalId".to_string(),
607                Value::String(external_id.as_str().to_string()),
608            );
609        }
610
611        if let Some(ref user_name) = self.user_name {
612            result.insert(
613                "userName".to_string(),
614                Value::String(user_name.as_str().to_string()),
615            );
616        }
617
618        // Add meta field if present (prioritize value object over JSON attributes)
619        if let Some(ref meta) = self.meta {
620            if let Ok(meta_json) = serde_json::to_value(meta) {
621                result.insert("meta".to_string(), meta_json);
622            }
623        }
624
625        // Add name field if present
626        if let Some(ref name) = self.name {
627            if let Ok(name_json) = serde_json::to_value(name) {
628                result.insert("name".to_string(), name_json);
629            }
630        }
631
632        // Add addresses if present
633        if let Some(ref addresses) = self.addresses {
634            let addresses_json = serde_json::to_value(addresses.values())
635                .map_err(|e| ValidationError::custom(format!("Serialization error: {}", e)))?;
636            result.insert("addresses".to_string(), addresses_json);
637        }
638
639        if let Some(ref phone_numbers) = self.phone_numbers {
640            let phones_json = serde_json::to_value(phone_numbers.values())
641                .map_err(|e| ValidationError::custom(format!("Serialization error: {}", e)))?;
642            result.insert("phoneNumbers".to_string(), phones_json);
643        }
644
645        if let Some(ref emails) = self.emails {
646            let emails_json = serde_json::to_value(emails.values())
647                .map_err(|e| ValidationError::custom(format!("Serialization error: {}", e)))?;
648            result.insert("emails".to_string(), emails_json);
649        }
650
651        if let Some(ref members) = self.members {
652            let members_json = serde_json::to_value(members.values())
653                .map_err(|e| ValidationError::custom(format!("Serialization error: {}", e)))?;
654            result.insert("members".to_string(), members_json);
655        }
656
657        Ok(Value::Object(result))
658    }
659
660    /// Get the external id if present.
661    pub fn get_external_id(&self) -> Option<&str> {
662        self.external_id.as_ref().map(|id| id.as_str())
663    }
664
665    /// Extract meta attributes from JSON data.
666    fn extract_meta(data: &Value) -> ValidationResult<Option<Meta>> {
667        if let Some(meta_value) = data.get("meta") {
668            if let Some(meta_obj) = meta_value.as_object() {
669                // Check if we have minimal required fields
670                let resource_type = if let Some(rt_value) = meta_obj.get("resourceType") {
671                    if let Some(rt_str) = rt_value.as_str() {
672                        Some(rt_str.to_string())
673                    } else {
674                        // resourceType exists but is not a string
675                        return Err(ValidationError::InvalidMetaStructure);
676                    }
677                } else {
678                    None
679                };
680
681                let created = meta_obj.get("created").and_then(|v| v.as_str());
682                let last_modified = meta_obj.get("lastModified").and_then(|v| v.as_str());
683
684                // If meta has any non-string types for datetime fields, fail immediately
685                if meta_obj.get("created").is_some() && created.is_none() {
686                    return Err(ValidationError::InvalidCreatedDateTime);
687                }
688                if meta_obj.get("lastModified").is_some() && last_modified.is_none() {
689                    return Err(ValidationError::InvalidModifiedDateTime);
690                }
691
692                // Validate location field data type if present
693                if let Some(location_value) = meta_obj.get("location") {
694                    if !location_value.is_string() {
695                        return Err(ValidationError::InvalidLocationUri);
696                    }
697                }
698
699                // Validate version field data type if present
700                if let Some(version_value) = meta_obj.get("version") {
701                    if !version_value.is_string() {
702                        return Err(ValidationError::InvalidVersionFormat);
703                    }
704                }
705
706                // Only proceed if we have both resourceType and timestamps
707                if let (Some(resource_type), Some(created), Some(last_modified)) =
708                    (resource_type, created, last_modified)
709                {
710                    let location = meta_obj
711                        .get("location")
712                        .and_then(|v| v.as_str())
713                        .map(|s| s.to_string());
714
715                    let version = meta_obj
716                        .get("version")
717                        .and_then(|v| v.as_str())
718                        .map(|s| s.to_string());
719
720                    // Parse DateTime strings with strict validation
721                    let created_dt = chrono::DateTime::parse_from_rfc3339(created)
722                        .map_err(|_| ValidationError::InvalidCreatedDateTime)?
723                        .with_timezone(&chrono::Utc);
724
725                    let last_modified_dt = chrono::DateTime::parse_from_rfc3339(last_modified)
726                        .map_err(|_| ValidationError::InvalidModifiedDateTime)?
727                        .with_timezone(&chrono::Utc);
728
729                    let meta = Meta::new(
730                        resource_type,
731                        created_dt,
732                        last_modified_dt,
733                        location,
734                        version,
735                    )?;
736                    Ok(Some(meta))
737                } else {
738                    // Meta exists but is incomplete - ignore it for backward compatibility
739                    Ok(None)
740                }
741            } else {
742                Err(ValidationError::InvalidMetaStructure)
743            }
744        } else {
745            Ok(None)
746        }
747    }
748
749    /// Get the meta attributes if present.
750    pub fn get_meta(&self) -> Option<&Meta> {
751        self.meta.as_ref()
752    }
753
754    /// Set meta attributes for the resource.
755    pub fn set_meta(&mut self, meta: Meta) {
756        // Update the JSON representation before moving
757        let meta_json = serde_json::to_value(&meta).unwrap_or(Value::Null);
758        self.set_attribute("meta".to_string(), meta_json);
759        self.meta = Some(meta);
760    }
761
762    /// Create meta attributes for a new resource.
763    pub fn create_meta(&mut self, base_url: &str) -> ValidationResult<()> {
764        let meta = Meta::new_for_creation(self.resource_type.clone())?;
765        let meta_with_location = if let Some(id) = &self.id {
766            let location = Meta::generate_location(base_url, &self.resource_type, id.as_str());
767            meta.with_location(location)?
768        } else {
769            meta
770        };
771        self.set_meta(meta_with_location);
772        Ok(())
773    }
774
775    /// Update meta attributes with current timestamp.
776    pub fn update_meta(&mut self) {
777        if let Some(meta) = &self.meta {
778            let updated_meta = meta.with_updated_timestamp();
779            self.set_meta(updated_meta);
780        }
781    }
782}