1use crate::error::{ValidationError, ValidationResult};
8use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10use std::fmt;
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48pub struct Meta {
49 #[serde(rename = "resourceType")]
50 pub resource_type: String,
51 pub created: DateTime<Utc>,
52 #[serde(rename = "lastModified")]
53 pub last_modified: DateTime<Utc>,
54 pub location: Option<String>,
55 pub version: Option<String>,
56}
57
58impl Meta {
59 pub fn new(
77 resource_type: String,
78 created: DateTime<Utc>,
79 last_modified: DateTime<Utc>,
80 location: Option<String>,
81 version: Option<String>,
82 ) -> ValidationResult<Self> {
83 Self::validate_resource_type(&resource_type)?;
84 Self::validate_timestamps(created, last_modified)?;
85 if let Some(ref location_val) = location {
86 Self::validate_location(location_val)?;
87 }
88 if let Some(ref version_val) = version {
89 Self::validate_version(version_val)?;
90 }
91
92 Ok(Self {
93 resource_type,
94 created,
95 last_modified,
96 location,
97 version,
98 })
99 }
100
101 pub fn new_simple(
116 resource_type: String,
117 created: DateTime<Utc>,
118 last_modified: DateTime<Utc>,
119 ) -> ValidationResult<Self> {
120 Self::new(resource_type, created, last_modified, None, None)
121 }
122
123 pub fn new_for_creation(resource_type: String) -> ValidationResult<Self> {
137 let now = Utc::now();
138 Self::new_simple(resource_type, now, now)
139 }
140
141 pub fn resource_type(&self) -> &str {
143 &self.resource_type
144 }
145
146 pub fn created(&self) -> DateTime<Utc> {
148 self.created
149 }
150
151 pub fn last_modified(&self) -> DateTime<Utc> {
153 self.last_modified
154 }
155
156 pub fn location(&self) -> Option<&str> {
158 self.location.as_deref()
159 }
160
161 pub fn version(&self) -> Option<&str> {
163 self.version.as_deref()
164 }
165
166 pub fn with_updated_timestamp(&self) -> Self {
171 Self {
172 resource_type: self.resource_type.clone(),
173 created: self.created,
174 last_modified: Utc::now(),
175 location: self.location.clone(),
176 version: self.version.clone(),
177 }
178 }
179
180 pub fn with_location(mut self, location: String) -> ValidationResult<Self> {
185 Self::validate_location(&location)?;
186 self.location = Some(location);
187 Ok(self)
188 }
189
190 pub fn with_version(mut self, version: String) -> ValidationResult<Self> {
195 Self::validate_version(&version)?;
196 self.version = Some(version);
197 Ok(self)
198 }
199
200 pub fn generate_location(base_url: &str, resource_type: &str, resource_id: &str) -> String {
205 format!(
206 "{}/{}s/{}",
207 base_url.trim_end_matches('/'),
208 resource_type,
209 resource_id
210 )
211 }
212
213 fn validate_resource_type(resource_type: &str) -> ValidationResult<()> {
215 if resource_type.is_empty() {
216 return Err(ValidationError::MissingResourceType);
217 }
218
219 if !resource_type
221 .chars()
222 .all(|c| c.is_alphanumeric() || c == '_')
223 {
224 return Err(ValidationError::InvalidResourceType {
225 resource_type: resource_type.to_string(),
226 });
227 }
228
229 Ok(())
230 }
231
232 fn validate_timestamps(
234 created: DateTime<Utc>,
235 last_modified: DateTime<Utc>,
236 ) -> ValidationResult<()> {
237 if last_modified < created {
238 return Err(ValidationError::Custom {
239 message: "Last modified timestamp cannot be before created timestamp".to_string(),
240 });
241 }
242
243 Ok(())
244 }
245
246 fn validate_location(location: &str) -> ValidationResult<()> {
248 if location.is_empty() {
249 return Err(ValidationError::InvalidLocationUri);
250 }
251
252 if !location.starts_with("http://") && !location.starts_with("https://") {
254 return Err(ValidationError::InvalidLocationUri);
255 }
256
257 Ok(())
258 }
259
260 fn validate_version(version: &str) -> ValidationResult<()> {
262 if version.is_empty() {
263 return Err(ValidationError::InvalidVersionFormat);
264 }
265
266 if !version
269 .chars()
270 .all(|c| c.is_alphanumeric() || matches!(c, '-' | '_' | '.' | '+' | '/' | '='))
271 {
272 return Err(ValidationError::InvalidVersionFormat);
273 }
274
275 Ok(())
276 }
277}
278
279impl fmt::Display for Meta {
280 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
281 write!(
282 f,
283 "Meta(resourceType={}, created={}, lastModified={})",
284 self.resource_type,
285 self.created.to_rfc3339(),
286 self.last_modified.to_rfc3339()
287 )
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294 use chrono::{TimeZone, Utc};
295 use serde_json;
296
297 #[test]
298 fn test_valid_meta_full() {
299 let created = Utc.with_ymd_and_hms(2023, 1, 1, 12, 0, 0).unwrap();
300 let modified = Utc.with_ymd_and_hms(2023, 1, 2, 12, 0, 0).unwrap();
301
302 let meta = Meta::new(
303 "User".to_string(),
304 created,
305 modified,
306 Some("https://example.com/Users/123".to_string()),
307 Some("123-456".to_string()),
308 );
309 assert!(meta.is_ok());
310
311 let meta = meta.unwrap();
312 assert_eq!(meta.resource_type(), "User");
313 assert_eq!(meta.created(), created);
314 assert_eq!(meta.last_modified(), modified);
315 assert_eq!(meta.location(), Some("https://example.com/Users/123"));
316 assert_eq!(meta.version(), Some("123-456"));
317 }
318
319 #[test]
320 fn test_valid_meta_simple() {
321 let created = Utc.with_ymd_and_hms(2023, 1, 1, 12, 0, 0).unwrap();
322 let modified = Utc.with_ymd_and_hms(2023, 1, 1, 12, 30, 0).unwrap();
323
324 let meta = Meta::new_simple("Group".to_string(), created, modified);
325 assert!(meta.is_ok());
326
327 let meta = meta.unwrap();
328 assert_eq!(meta.resource_type(), "Group");
329 assert_eq!(meta.created(), created);
330 assert_eq!(meta.last_modified(), modified);
331 assert_eq!(meta.location(), None);
332 assert_eq!(meta.version(), None);
333 }
334
335 #[test]
336 fn test_new_for_creation() {
337 let meta = Meta::new_for_creation("User".to_string());
338 assert!(meta.is_ok());
339
340 let meta = meta.unwrap();
341 assert_eq!(meta.resource_type(), "User");
342 assert_eq!(meta.created(), meta.last_modified());
343 }
344
345 #[test]
346 fn test_empty_resource_type() {
347 let now = Utc::now();
348 let result = Meta::new_simple("".to_string(), now, now);
349 assert!(result.is_err());
350
351 match result.unwrap_err() {
352 ValidationError::MissingResourceType => {}
353 other => panic!("Expected MissingResourceType error, got: {:?}", other),
354 }
355 }
356
357 #[test]
358 fn test_invalid_resource_type() {
359 let now = Utc::now();
360 let result = Meta::new_simple("Invalid-Type!".to_string(), now, now);
361 assert!(result.is_err());
362
363 match result.unwrap_err() {
364 ValidationError::InvalidResourceType { resource_type } => {
365 assert_eq!(resource_type, "Invalid-Type!");
366 }
367 other => panic!("Expected InvalidResourceType error, got: {:?}", other),
368 }
369 }
370
371 #[test]
372 fn test_invalid_timestamps() {
373 let created = Utc.with_ymd_and_hms(2023, 1, 2, 12, 0, 0).unwrap();
374 let modified = Utc.with_ymd_and_hms(2023, 1, 1, 12, 0, 0).unwrap(); let result = Meta::new_simple("User".to_string(), created, modified);
377 assert!(result.is_err());
378
379 match result.unwrap_err() {
380 ValidationError::Custom { message } => {
381 assert!(message.contains("Last modified timestamp cannot be before created"));
382 }
383 other => panic!("Expected Custom error, got: {:?}", other),
384 }
385 }
386
387 #[test]
388 fn test_invalid_location() {
389 let now = Utc::now();
390
391 let result = Meta::new("User".to_string(), now, now, Some("".to_string()), None);
393 assert!(result.is_err());
394
395 let result = Meta::new(
397 "User".to_string(),
398 now,
399 now,
400 Some("not-a-uri".to_string()),
401 None,
402 );
403 assert!(result.is_err());
404 }
405
406 #[test]
407 fn test_invalid_version() {
408 let now = Utc::now();
409
410 let result = Meta::new("User".to_string(), now, now, None, Some("".to_string()));
412 assert!(result.is_err());
413
414 let result = Meta::new(
416 "User".to_string(),
417 now,
418 now,
419 None,
420 Some("invalid@version!".to_string()),
421 );
422 assert!(result.is_err());
423
424 match result.unwrap_err() {
425 ValidationError::InvalidVersionFormat => {}
426 other => panic!("Expected InvalidVersionFormat error, got: {:?}", other),
427 }
428 }
429
430 #[test]
431 fn test_with_updated_timestamp() {
432 let created = Utc.with_ymd_and_hms(2023, 1, 1, 12, 0, 0).unwrap();
433 let meta = Meta::new_simple("User".to_string(), created, created).unwrap();
434
435 std::thread::sleep(std::time::Duration::from_millis(10));
436 let updated_meta = meta.with_updated_timestamp();
437
438 assert_eq!(updated_meta.created(), created);
439 assert!(updated_meta.last_modified() > created);
440 assert_eq!(updated_meta.resource_type(), "User");
441 }
442
443 #[test]
444 fn test_with_location() {
445 let now = Utc::now();
446 let meta = Meta::new_simple("User".to_string(), now, now).unwrap();
447
448 let meta_with_location = meta
449 .clone()
450 .with_location("https://example.com/Users/123".to_string());
451 assert!(meta_with_location.is_ok());
452
453 let meta_with_location = meta_with_location.unwrap();
454 assert_eq!(
455 meta_with_location.location(),
456 Some("https://example.com/Users/123")
457 );
458
459 let invalid_result = meta.with_location("invalid-uri".to_string());
461 assert!(invalid_result.is_err());
462 }
463
464 #[test]
465 fn test_with_version() {
466 let now = Utc::now();
467 let meta = Meta::new_simple("User".to_string(), now, now).unwrap();
468
469 let meta_with_version = meta.clone().with_version("123-456".to_string());
470 assert!(meta_with_version.is_ok());
471
472 let meta_with_version = meta_with_version.unwrap();
473 assert_eq!(meta_with_version.version(), Some("123-456"));
474
475 let invalid_meta = meta.with_version("invalid@version".to_string());
477 assert!(invalid_meta.is_err());
478 }
479
480 #[test]
481 fn test_generate_location() {
482 let location = Meta::generate_location("https://example.com", "User", "123");
483 assert_eq!(location, "https://example.com/Users/123");
484
485 let location = Meta::generate_location("https://example.com/", "Group", "456");
487 assert_eq!(location, "https://example.com/Groups/456");
488 }
489
490 #[test]
491 fn test_display() {
492 let created = Utc.with_ymd_and_hms(2023, 1, 1, 12, 0, 0).unwrap();
493 let modified = Utc.with_ymd_and_hms(2023, 1, 2, 12, 0, 0).unwrap();
494
495 let meta = Meta::new_simple("User".to_string(), created, modified).unwrap();
496 let display_str = format!("{}", meta);
497
498 assert!(display_str.contains("User"));
499 assert!(display_str.contains("2023-01-01T12:00:00"));
500 assert!(display_str.contains("2023-01-02T12:00:00"));
501 }
502
503 #[test]
504 fn test_serialization() {
505 let created = Utc.with_ymd_and_hms(2023, 1, 1, 12, 0, 0).unwrap();
506 let modified = Utc.with_ymd_and_hms(2023, 1, 2, 12, 0, 0).unwrap();
507
508 let meta = Meta::new(
509 "User".to_string(),
510 created,
511 modified,
512 Some("https://example.com/Users/123".to_string()),
513 Some("123-456".to_string()),
514 )
515 .unwrap();
516
517 let json = serde_json::to_string(&meta).unwrap();
518 assert!(json.contains("\"resourceType\":\"User\""));
519 assert!(json.contains("\"lastModified\""));
520 assert!(json.contains("\"location\":\"https://example.com/Users/123\""));
521 assert!(json.contains("\"version\":\"123-456\""));
522 }
523
524 #[test]
525 fn test_deserialization() {
526 let json = r#"{
527 "resourceType": "Group",
528 "created": "2023-01-01T12:00:00Z",
529 "lastModified": "2023-01-02T12:00:00Z",
530 "location": "https://example.com/Groups/456",
531 "version": "456-789"
532 }"#;
533
534 let meta: Meta = serde_json::from_str(json).unwrap();
535 assert_eq!(meta.resource_type(), "Group");
536 assert_eq!(meta.location(), Some("https://example.com/Groups/456"));
537 assert_eq!(meta.version(), Some("456-789"));
538 }
539
540 #[test]
541 fn test_equality() {
542 let created = Utc.with_ymd_and_hms(2023, 1, 1, 12, 0, 0).unwrap();
543 let modified = Utc.with_ymd_and_hms(2023, 1, 2, 12, 0, 0).unwrap();
544
545 let meta1 = Meta::new_simple("User".to_string(), created, modified).unwrap();
546 let meta2 = Meta::new_simple("User".to_string(), created, modified).unwrap();
547 let meta3 = Meta::new_simple("Group".to_string(), created, modified).unwrap();
548
549 assert_eq!(meta1, meta2);
550 assert_ne!(meta1, meta3);
551 }
552
553 #[test]
554 fn test_clone() {
555 let created = Utc.with_ymd_and_hms(2023, 1, 1, 12, 0, 0).unwrap();
556 let modified = Utc.with_ymd_and_hms(2023, 1, 2, 12, 0, 0).unwrap();
557
558 let meta = Meta::new(
559 "User".to_string(),
560 created,
561 modified,
562 Some("https://example.com/Users/123".to_string()),
563 Some("123-456".to_string()),
564 )
565 .unwrap();
566
567 let cloned = meta.clone();
568 assert_eq!(meta, cloned);
569 assert_eq!(meta.resource_type(), cloned.resource_type());
570 assert_eq!(meta.created(), cloned.created());
571 assert_eq!(meta.last_modified(), cloned.last_modified());
572 assert_eq!(meta.location(), cloned.location());
573 assert_eq!(meta.version(), cloned.version());
574 }
575}