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 pub fn generate_version(resource_id: &str, last_modified: DateTime<Utc>) -> String {
218 let timestamp = last_modified.timestamp_millis();
219 format!("W/\"{}-{}\"", resource_id, timestamp)
220 }
221
222 fn validate_resource_type(resource_type: &str) -> ValidationResult<()> {
224 if resource_type.is_empty() {
225 return Err(ValidationError::MissingResourceType);
226 }
227
228 if !resource_type
230 .chars()
231 .all(|c| c.is_alphanumeric() || c == '_')
232 {
233 return Err(ValidationError::InvalidResourceType {
234 resource_type: resource_type.to_string(),
235 });
236 }
237
238 Ok(())
239 }
240
241 fn validate_timestamps(
243 created: DateTime<Utc>,
244 last_modified: DateTime<Utc>,
245 ) -> ValidationResult<()> {
246 if last_modified < created {
247 return Err(ValidationError::Custom {
248 message: "Last modified timestamp cannot be before created timestamp".to_string(),
249 });
250 }
251
252 Ok(())
253 }
254
255 fn validate_location(location: &str) -> ValidationResult<()> {
257 if location.is_empty() {
258 return Err(ValidationError::InvalidLocationUri);
259 }
260
261 if !location.starts_with("http://") && !location.starts_with("https://") {
263 return Err(ValidationError::InvalidLocationUri);
264 }
265
266 Ok(())
267 }
268
269 fn validate_version(version: &str) -> ValidationResult<()> {
271 if version.is_empty() {
272 return Err(ValidationError::InvalidVersionFormat);
273 }
274
275 if !version.starts_with("W/\"") && !version.starts_with('"') {
277 return Err(ValidationError::InvalidVersionFormat);
278 }
279
280 if !version.ends_with('"') {
281 return Err(ValidationError::InvalidVersionFormat);
282 }
283
284 Ok(())
285 }
286}
287
288impl fmt::Display for Meta {
289 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
290 write!(
291 f,
292 "Meta(resourceType={}, created={}, lastModified={})",
293 self.resource_type,
294 self.created.to_rfc3339(),
295 self.last_modified.to_rfc3339()
296 )
297 }
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303 use chrono::{TimeZone, Utc};
304 use serde_json;
305
306 #[test]
307 fn test_valid_meta_full() {
308 let created = Utc.with_ymd_and_hms(2023, 1, 1, 12, 0, 0).unwrap();
309 let modified = Utc.with_ymd_and_hms(2023, 1, 2, 12, 0, 0).unwrap();
310
311 let meta = Meta::new(
312 "User".to_string(),
313 created,
314 modified,
315 Some("https://example.com/Users/123".to_string()),
316 Some("W/\"123-456\"".to_string()),
317 );
318 assert!(meta.is_ok());
319
320 let meta = meta.unwrap();
321 assert_eq!(meta.resource_type(), "User");
322 assert_eq!(meta.created(), created);
323 assert_eq!(meta.last_modified(), modified);
324 assert_eq!(meta.location(), Some("https://example.com/Users/123"));
325 assert_eq!(meta.version(), Some("W/\"123-456\""));
326 }
327
328 #[test]
329 fn test_valid_meta_simple() {
330 let created = Utc.with_ymd_and_hms(2023, 1, 1, 12, 0, 0).unwrap();
331 let modified = Utc.with_ymd_and_hms(2023, 1, 1, 12, 30, 0).unwrap();
332
333 let meta = Meta::new_simple("Group".to_string(), created, modified);
334 assert!(meta.is_ok());
335
336 let meta = meta.unwrap();
337 assert_eq!(meta.resource_type(), "Group");
338 assert_eq!(meta.created(), created);
339 assert_eq!(meta.last_modified(), modified);
340 assert_eq!(meta.location(), None);
341 assert_eq!(meta.version(), None);
342 }
343
344 #[test]
345 fn test_new_for_creation() {
346 let meta = Meta::new_for_creation("User".to_string());
347 assert!(meta.is_ok());
348
349 let meta = meta.unwrap();
350 assert_eq!(meta.resource_type(), "User");
351 assert_eq!(meta.created(), meta.last_modified());
352 }
353
354 #[test]
355 fn test_empty_resource_type() {
356 let now = Utc::now();
357 let result = Meta::new_simple("".to_string(), now, now);
358 assert!(result.is_err());
359
360 match result.unwrap_err() {
361 ValidationError::MissingResourceType => {}
362 other => panic!("Expected MissingResourceType error, got: {:?}", other),
363 }
364 }
365
366 #[test]
367 fn test_invalid_resource_type() {
368 let now = Utc::now();
369 let result = Meta::new_simple("Invalid-Type!".to_string(), now, now);
370 assert!(result.is_err());
371
372 match result.unwrap_err() {
373 ValidationError::InvalidResourceType { resource_type } => {
374 assert_eq!(resource_type, "Invalid-Type!");
375 }
376 other => panic!("Expected InvalidResourceType error, got: {:?}", other),
377 }
378 }
379
380 #[test]
381 fn test_invalid_timestamps() {
382 let created = Utc.with_ymd_and_hms(2023, 1, 2, 12, 0, 0).unwrap();
383 let modified = Utc.with_ymd_and_hms(2023, 1, 1, 12, 0, 0).unwrap(); let result = Meta::new_simple("User".to_string(), created, modified);
386 assert!(result.is_err());
387
388 match result.unwrap_err() {
389 ValidationError::Custom { message } => {
390 assert!(message.contains("Last modified timestamp cannot be before created"));
391 }
392 other => panic!("Expected Custom error, got: {:?}", other),
393 }
394 }
395
396 #[test]
397 fn test_invalid_location() {
398 let now = Utc::now();
399
400 let result = Meta::new("User".to_string(), now, now, Some("".to_string()), None);
402 assert!(result.is_err());
403
404 let result = Meta::new(
406 "User".to_string(),
407 now,
408 now,
409 Some("not-a-uri".to_string()),
410 None,
411 );
412 assert!(result.is_err());
413 }
414
415 #[test]
416 fn test_invalid_version() {
417 let now = Utc::now();
418
419 let result = Meta::new("User".to_string(), now, now, None, Some("".to_string()));
421 assert!(result.is_err());
422
423 let result = Meta::new(
425 "User".to_string(),
426 now,
427 now,
428 None,
429 Some("invalid-etag".to_string()),
430 );
431 assert!(result.is_err());
432
433 match result.unwrap_err() {
434 ValidationError::InvalidVersionFormat => {}
435 other => panic!("Expected InvalidVersionFormat error, got: {:?}", other),
436 }
437 }
438
439 #[test]
440 fn test_with_updated_timestamp() {
441 let created = Utc.with_ymd_and_hms(2023, 1, 1, 12, 0, 0).unwrap();
442 let meta = Meta::new_simple("User".to_string(), created, created).unwrap();
443
444 std::thread::sleep(std::time::Duration::from_millis(10));
445 let updated_meta = meta.with_updated_timestamp();
446
447 assert_eq!(updated_meta.created(), created);
448 assert!(updated_meta.last_modified() > created);
449 assert_eq!(updated_meta.resource_type(), "User");
450 }
451
452 #[test]
453 fn test_with_location() {
454 let now = Utc::now();
455 let meta = Meta::new_simple("User".to_string(), now, now).unwrap();
456
457 let meta_with_location = meta
458 .clone()
459 .with_location("https://example.com/Users/123".to_string());
460 assert!(meta_with_location.is_ok());
461
462 let meta_with_location = meta_with_location.unwrap();
463 assert_eq!(
464 meta_with_location.location(),
465 Some("https://example.com/Users/123")
466 );
467
468 let invalid_result = meta.with_location("invalid-uri".to_string());
470 assert!(invalid_result.is_err());
471 }
472
473 #[test]
474 fn test_with_version() {
475 let now = Utc::now();
476 let meta = Meta::new_simple("User".to_string(), now, now).unwrap();
477
478 let meta_with_version = meta.clone().with_version("W/\"123-456\"".to_string());
479 assert!(meta_with_version.is_ok());
480
481 let meta_with_version = meta_with_version.unwrap();
482 assert_eq!(meta_with_version.version(), Some("W/\"123-456\""));
483
484 let invalid_result = meta.with_version("invalid-version".to_string());
486 assert!(invalid_result.is_err());
487 }
488
489 #[test]
490 fn test_generate_location() {
491 let location = Meta::generate_location("https://example.com", "User", "123");
492 assert_eq!(location, "https://example.com/Users/123");
493
494 let location = Meta::generate_location("https://example.com/", "Group", "456");
496 assert_eq!(location, "https://example.com/Groups/456");
497 }
498
499 #[test]
500 fn test_generate_version() {
501 let timestamp = Utc.with_ymd_and_hms(2023, 1, 1, 12, 0, 0).unwrap();
502 let version = Meta::generate_version("123", timestamp);
503 let expected_millis = timestamp.timestamp_millis();
504 assert_eq!(version, format!("W/\"123-{}\"", expected_millis));
505 }
506
507 #[test]
508 fn test_display() {
509 let created = Utc.with_ymd_and_hms(2023, 1, 1, 12, 0, 0).unwrap();
510 let modified = Utc.with_ymd_and_hms(2023, 1, 2, 12, 0, 0).unwrap();
511
512 let meta = Meta::new_simple("User".to_string(), created, modified).unwrap();
513 let display_str = format!("{}", meta);
514
515 assert!(display_str.contains("User"));
516 assert!(display_str.contains("2023-01-01T12:00:00"));
517 assert!(display_str.contains("2023-01-02T12:00:00"));
518 }
519
520 #[test]
521 fn test_serialization() {
522 let created = Utc.with_ymd_and_hms(2023, 1, 1, 12, 0, 0).unwrap();
523 let modified = Utc.with_ymd_and_hms(2023, 1, 2, 12, 0, 0).unwrap();
524
525 let meta = Meta::new(
526 "User".to_string(),
527 created,
528 modified,
529 Some("https://example.com/Users/123".to_string()),
530 Some("W/\"123-456\"".to_string()),
531 )
532 .unwrap();
533
534 let json = serde_json::to_string(&meta).unwrap();
535 assert!(json.contains("\"resourceType\":\"User\""));
536 assert!(json.contains("\"lastModified\""));
537 assert!(json.contains("\"location\":\"https://example.com/Users/123\""));
538 assert!(json.contains("\"version\":\"W/\\\"123-456\\\"\""));
539 }
540
541 #[test]
542 fn test_deserialization() {
543 let json = r#"{
544 "resourceType": "Group",
545 "created": "2023-01-01T12:00:00Z",
546 "lastModified": "2023-01-02T12:00:00Z",
547 "location": "https://example.com/Groups/456",
548 "version": "W/\"456-789\""
549 }"#;
550
551 let meta: Meta = serde_json::from_str(json).unwrap();
552 assert_eq!(meta.resource_type(), "Group");
553 assert_eq!(meta.location(), Some("https://example.com/Groups/456"));
554 assert_eq!(meta.version(), Some("W/\"456-789\""));
555 }
556
557 #[test]
558 fn test_equality() {
559 let created = Utc.with_ymd_and_hms(2023, 1, 1, 12, 0, 0).unwrap();
560 let modified = Utc.with_ymd_and_hms(2023, 1, 2, 12, 0, 0).unwrap();
561
562 let meta1 = Meta::new_simple("User".to_string(), created, modified).unwrap();
563 let meta2 = Meta::new_simple("User".to_string(), created, modified).unwrap();
564 let meta3 = Meta::new_simple("Group".to_string(), created, modified).unwrap();
565
566 assert_eq!(meta1, meta2);
567 assert_ne!(meta1, meta3);
568 }
569
570 #[test]
571 fn test_clone() {
572 let created = Utc.with_ymd_and_hms(2023, 1, 1, 12, 0, 0).unwrap();
573 let modified = Utc.with_ymd_and_hms(2023, 1, 2, 12, 0, 0).unwrap();
574
575 let meta = Meta::new(
576 "User".to_string(),
577 created,
578 modified,
579 Some("https://example.com/Users/123".to_string()),
580 Some("W/\"123-456\"".to_string()),
581 )
582 .unwrap();
583
584 let cloned = meta.clone();
585 assert_eq!(meta, cloned);
586 assert_eq!(meta.resource_type(), cloned.resource_type());
587 assert_eq!(meta.created(), cloned.created());
588 assert_eq!(meta.last_modified(), cloned.last_modified());
589 assert_eq!(meta.location(), cloned.location());
590 assert_eq!(meta.version(), cloned.version());
591 }
592}