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