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