1use serde::{Deserialize, Serialize};
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
47#[serde(rename_all = "lowercase")]
48pub enum PostStatus {
49 #[default]
51 Draft,
52 Published,
54 Scheduled,
56 Sent,
58}
59
60impl PostStatus {
61 pub fn is_published(&self) -> bool {
72 matches!(self, PostStatus::Published)
73 }
74
75 pub fn is_draft(&self) -> bool {
86 matches!(self, PostStatus::Draft)
87 }
88
89 pub fn is_scheduled(&self) -> bool {
100 matches!(self, PostStatus::Scheduled)
101 }
102
103 pub fn is_sent(&self) -> bool {
114 matches!(self, PostStatus::Sent)
115 }
116}
117
118#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
164pub struct Post {
165 #[serde(skip_serializing_if = "String::is_empty", default)]
168 pub id: String,
169
170 #[serde(skip_serializing_if = "Option::is_none")]
172 pub uuid: Option<String>,
173
174 #[serde(skip_serializing_if = "String::is_empty", default)]
176 pub slug: String,
177
178 #[serde(skip_serializing_if = "Option::is_none")]
180 pub comment_id: Option<String>,
181
182 pub title: String,
185
186 #[serde(skip_serializing_if = "Option::is_none")]
188 pub lexical: Option<String>,
189
190 #[serde(skip_serializing_if = "Option::is_none")]
192 pub html: Option<String>,
193
194 #[serde(skip_serializing_if = "Option::is_none")]
196 pub excerpt: Option<String>,
197
198 #[serde(skip_serializing_if = "Option::is_none")]
200 pub custom_excerpt: Option<String>,
201
202 #[serde(default)]
205 pub status: PostStatus,
206
207 #[serde(skip_serializing_if = "Option::is_none")]
209 pub visibility: Option<String>,
210
211 #[serde(skip_serializing_if = "Option::is_none")]
213 pub email_only: Option<bool>,
214
215 #[serde(skip_serializing_if = "Option::is_none")]
218 pub created_at: Option<String>,
219
220 #[serde(skip_serializing_if = "Option::is_none")]
222 pub updated_at: Option<String>,
223
224 #[serde(skip_serializing_if = "Option::is_none")]
226 pub published_at: Option<String>,
227
228 #[serde(skip_serializing_if = "Option::is_none")]
231 pub feature_image: Option<String>,
232
233 #[serde(skip_serializing_if = "Option::is_none")]
235 pub feature_image_alt: Option<String>,
236
237 #[serde(skip_serializing_if = "Option::is_none")]
239 pub feature_image_caption: Option<String>,
240
241 #[serde(skip_serializing_if = "Option::is_none")]
244 pub featured: Option<bool>,
245
246 #[serde(skip_serializing_if = "Option::is_none")]
251 pub tags: Option<serde_json::Value>,
252
253 #[serde(skip_serializing_if = "Option::is_none")]
255 pub authors: Option<serde_json::Value>,
256
257 #[serde(skip_serializing_if = "Option::is_none")]
259 pub primary_author: Option<serde_json::Value>,
260
261 #[serde(skip_serializing_if = "Option::is_none")]
263 pub primary_tag: Option<serde_json::Value>,
264
265 #[serde(skip_serializing_if = "Option::is_none")]
267 pub newsletter: Option<serde_json::Value>,
268
269 #[serde(skip_serializing_if = "Option::is_none")]
271 pub email: Option<serde_json::Value>,
272
273 #[serde(skip_serializing_if = "Option::is_none")]
276 pub url: Option<String>,
277
278 #[serde(skip_serializing_if = "Option::is_none")]
280 pub canonical_url: Option<String>,
281
282 #[serde(skip_serializing_if = "Option::is_none")]
285 pub meta_title: Option<String>,
286
287 #[serde(skip_serializing_if = "Option::is_none")]
289 pub meta_description: Option<String>,
290
291 #[serde(skip_serializing_if = "Option::is_none")]
294 pub og_image: Option<String>,
295
296 #[serde(skip_serializing_if = "Option::is_none")]
298 pub og_title: Option<String>,
299
300 #[serde(skip_serializing_if = "Option::is_none")]
302 pub og_description: Option<String>,
303
304 #[serde(skip_serializing_if = "Option::is_none")]
307 pub twitter_image: Option<String>,
308
309 #[serde(skip_serializing_if = "Option::is_none")]
311 pub twitter_title: Option<String>,
312
313 #[serde(skip_serializing_if = "Option::is_none")]
315 pub twitter_description: Option<String>,
316
317 #[serde(skip_serializing_if = "Option::is_none")]
320 pub codeinjection_head: Option<String>,
321
322 #[serde(skip_serializing_if = "Option::is_none")]
324 pub codeinjection_foot: Option<String>,
325
326 #[serde(skip_serializing_if = "Option::is_none")]
328 pub custom_template: Option<String>,
329}
330
331impl Post {
332 pub fn is_published(&self) -> bool {
344 self.status.is_published()
345 }
346
347 pub fn is_draft(&self) -> bool {
359 self.status.is_draft()
360 }
361
362 pub fn is_scheduled(&self) -> bool {
374 self.status.is_scheduled()
375 }
376
377 pub fn is_sent(&self) -> bool {
389 self.status.is_sent()
390 }
391
392 pub fn is_featured(&self) -> bool {
404 self.featured.unwrap_or(false)
405 }
406
407 pub fn is_email_only(&self) -> bool {
419 self.email_only.unwrap_or(false)
420 }
421}
422
423#[cfg(test)]
424mod tests {
425 use super::*;
426 use serde_json::json;
427
428 #[test]
429 fn test_post_status_deserialization() {
430 assert_eq!(
431 serde_json::from_str::<PostStatus>("\"draft\"").unwrap(),
432 PostStatus::Draft
433 );
434 assert_eq!(
435 serde_json::from_str::<PostStatus>("\"published\"").unwrap(),
436 PostStatus::Published
437 );
438 assert_eq!(
439 serde_json::from_str::<PostStatus>("\"scheduled\"").unwrap(),
440 PostStatus::Scheduled
441 );
442 assert_eq!(
443 serde_json::from_str::<PostStatus>("\"sent\"").unwrap(),
444 PostStatus::Sent
445 );
446 }
447
448 #[test]
449 fn test_post_status_serialization() {
450 assert_eq!(
451 serde_json::to_string(&PostStatus::Draft).unwrap(),
452 "\"draft\""
453 );
454 assert_eq!(
455 serde_json::to_string(&PostStatus::Published).unwrap(),
456 "\"published\""
457 );
458 assert_eq!(
459 serde_json::to_string(&PostStatus::Scheduled).unwrap(),
460 "\"scheduled\""
461 );
462 assert_eq!(
463 serde_json::to_string(&PostStatus::Sent).unwrap(),
464 "\"sent\""
465 );
466 }
467
468 #[test]
469 fn test_post_status_methods() {
470 assert!(PostStatus::Draft.is_draft());
471 assert!(!PostStatus::Draft.is_published());
472 assert!(!PostStatus::Draft.is_scheduled());
473 assert!(!PostStatus::Draft.is_sent());
474
475 assert!(!PostStatus::Published.is_draft());
476 assert!(PostStatus::Published.is_published());
477 assert!(!PostStatus::Published.is_scheduled());
478 assert!(!PostStatus::Published.is_sent());
479
480 assert!(!PostStatus::Scheduled.is_draft());
481 assert!(!PostStatus::Scheduled.is_published());
482 assert!(PostStatus::Scheduled.is_scheduled());
483 assert!(!PostStatus::Scheduled.is_sent());
484
485 assert!(!PostStatus::Sent.is_draft());
486 assert!(!PostStatus::Sent.is_published());
487 assert!(!PostStatus::Sent.is_scheduled());
488 assert!(PostStatus::Sent.is_sent());
489 }
490
491 #[test]
492 fn test_post_minimal_deserialization() {
493 let json = json!({
494 "id": "5ddc9141c35e7700383b2937",
495 "title": "Welcome",
496 "slug": "welcome-short",
497 "status": "published"
498 });
499
500 let post: Post = serde_json::from_value(json).unwrap();
501 assert_eq!(post.id, "5ddc9141c35e7700383b2937");
502 assert_eq!(post.title, "Welcome");
503 assert_eq!(post.slug, "welcome-short");
504 assert_eq!(post.status, PostStatus::Published);
505 }
506
507 #[test]
508 fn test_post_full_deserialization() {
509 let json = json!({
510 "id": "5ddc9141c35e7700383b2937",
511 "uuid": "a5aa9bd8-ea31-415c-b452-3040dae1e730",
512 "title": "Welcome",
513 "slug": "welcome-short",
514 "status": "published",
515 "visibility": "public",
516 "created_at": "2019-11-26T02:43:13.000Z",
517 "updated_at": "2019-11-26T02:44:17.000Z",
518 "published_at": "2019-11-26T02:44:17.000Z",
519 "feature_image": "https://example.com/image.png",
520 "featured": true,
521 "excerpt": "Welcome excerpt",
522 "custom_excerpt": "Custom excerpt",
523 "meta_title": "Welcome | My Site",
524 "meta_description": "A welcoming post",
525 "og_title": "Welcome",
526 "og_description": "OG description",
527 "twitter_title": "Welcome on Twitter",
528 "email_only": false,
529 "canonical_url": "https://example.com/welcome"
530 });
531
532 let post: Post = serde_json::from_value(json).unwrap();
533 assert_eq!(post.id, "5ddc9141c35e7700383b2937");
534 assert_eq!(
535 post.uuid,
536 Some("a5aa9bd8-ea31-415c-b452-3040dae1e730".to_string())
537 );
538 assert_eq!(post.title, "Welcome");
539 assert_eq!(post.status, PostStatus::Published);
540 assert_eq!(post.visibility, Some("public".to_string()));
541 assert_eq!(post.featured, Some(true));
542 assert_eq!(post.email_only, Some(false));
543 assert_eq!(post.meta_title, Some("Welcome | My Site".to_string()));
544 }
545
546 #[test]
547 fn test_post_with_relationships() {
548 let json = json!({
549 "id": "123",
550 "title": "Post with tags",
551 "slug": "post-with-tags",
552 "status": "draft",
553 "tags": [
554 {"id": "tag1", "name": "Tech"},
555 {"id": "tag2", "name": "Rust"}
556 ],
557 "authors": [
558 {"id": "author1", "name": "John Doe", "email": "john@example.com"}
559 ]
560 });
561
562 let post: Post = serde_json::from_value(json).unwrap();
563 assert_eq!(post.title, "Post with tags");
564 assert!(post.tags.is_some());
565 assert!(post.authors.is_some());
566 }
567
568 #[test]
569 fn test_post_serialization() {
570 let post = Post {
571 id: "123".to_string(),
572 title: "Test Post".to_string(),
573 slug: "test-post".to_string(),
574 status: PostStatus::Published,
575 visibility: Some("public".to_string()),
576 featured: Some(true),
577 ..Default::default()
578 };
579
580 let json = serde_json::to_value(&post).unwrap();
581 assert_eq!(json["id"], "123");
582 assert_eq!(json["title"], "Test Post");
583 assert_eq!(json["slug"], "test-post");
584 assert_eq!(json["status"], "published");
585 assert_eq!(json["visibility"], "public");
586 assert_eq!(json["featured"], true);
587 }
588
589 #[test]
590 fn test_post_serialization_skips_none() {
591 let post = Post {
592 id: "123".to_string(),
593 title: "Minimal Post".to_string(),
594 slug: "minimal".to_string(),
595 status: PostStatus::Draft,
596 ..Default::default()
597 };
598
599 let json = serde_json::to_value(&post).unwrap();
600 assert!(!json.as_object().unwrap().contains_key("uuid"));
601 assert!(!json.as_object().unwrap().contains_key("feature_image"));
602 assert!(!json.as_object().unwrap().contains_key("tags"));
603 }
604
605 #[test]
606 fn test_post_default() {
607 let post = Post::default();
608 assert_eq!(post.id, "");
609 assert_eq!(post.title, "");
610 assert_eq!(post.slug, "");
611 assert_eq!(post.status, PostStatus::Draft);
612 assert_eq!(post.uuid, None);
613 assert_eq!(post.featured, None);
614 }
615
616 #[test]
617 fn test_post_is_published() {
618 let post = Post {
619 status: PostStatus::Published,
620 ..Default::default()
621 };
622 assert!(post.is_published());
623 assert!(!post.is_draft());
624 }
625
626 #[test]
627 fn test_post_is_draft() {
628 let post = Post {
629 status: PostStatus::Draft,
630 ..Default::default()
631 };
632 assert!(post.is_draft());
633 assert!(!post.is_published());
634 }
635
636 #[test]
637 fn test_post_is_scheduled() {
638 let post = Post {
639 status: PostStatus::Scheduled,
640 ..Default::default()
641 };
642 assert!(post.is_scheduled());
643 assert!(!post.is_published());
644 }
645
646 #[test]
647 fn test_post_is_sent() {
648 let post = Post {
649 status: PostStatus::Sent,
650 ..Default::default()
651 };
652 assert!(post.is_sent());
653 assert!(!post.is_published());
654 }
655
656 #[test]
657 fn test_post_is_featured() {
658 let mut post = Post::default();
659 assert!(!post.is_featured());
660
661 post.featured = Some(true);
662 assert!(post.is_featured());
663
664 post.featured = Some(false);
665 assert!(!post.is_featured());
666 }
667
668 #[test]
669 fn test_post_is_email_only() {
670 let mut post = Post::default();
671 assert!(!post.is_email_only());
672
673 post.email_only = Some(true);
674 assert!(post.is_email_only());
675
676 post.email_only = Some(false);
677 assert!(!post.is_email_only());
678 }
679
680 #[test]
681 fn test_post_clone() {
682 let post = Post {
683 id: "123".to_string(),
684 title: "Clone Test".to_string(),
685 slug: "clone-test".to_string(),
686 status: PostStatus::Published,
687 ..Default::default()
688 };
689
690 let cloned = post.clone();
691 assert_eq!(post, cloned);
692 }
693
694 #[test]
695 fn test_post_status_clone() {
696 let status = PostStatus::Published;
697 let cloned = status;
698 assert_eq!(status, cloned);
699 }
700}