1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6use super::post::{Post, PostNav, PostStatus};
7
8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
9pub struct PublicPost {
10 pub id: i32,
11 pub author_id: i32,
12 pub title: String,
13 pub slug: String,
14 pub summary: Option<String>,
15 pub content_html: Option<String>,
16 #[cfg_attr(
17 target_arch = "wasm32",
18 serde(
19 default,
20 deserialize_with = "crate::models::rfc3339::deserialize_option"
21 )
22 )]
23 pub published_at: Option<DateTime<Utc>>,
24 #[cfg_attr(
25 target_arch = "wasm32",
26 serde(deserialize_with = "crate::models::rfc3339::deserialize")
27 )]
28 pub created_at: DateTime<Utc>,
29 #[cfg_attr(
30 target_arch = "wasm32",
31 serde(deserialize_with = "crate::models::rfc3339::deserialize")
32 )]
33 pub updated_at: DateTime<Utc>,
34 pub tags: Vec<String>,
35 pub cover_image: Option<String>,
36 pub reading_time: u32,
37 pub word_count: u32,
38 pub toc_html: Option<String>,
39 pub prev_post: Option<PostNav>,
40 pub next_post: Option<PostNav>,
41}
42
43impl From<Post> for PublicPost {
44 fn from(post: Post) -> Self {
45 Self {
46 id: post.id,
47 author_id: post.author_id,
48 title: post.title,
49 slug: post.slug,
50 summary: post.summary,
51 content_html: post.content_html,
52 published_at: post.published_at,
53 created_at: post.created_at,
54 updated_at: post.updated_at,
55 tags: post.tags,
56 cover_image: post.cover_image,
57 reading_time: post.reading_time,
58 word_count: post.word_count,
59 toc_html: post.toc_html,
60 prev_post: post.prev_post,
61 next_post: post.next_post,
62 }
63 }
64}
65
66impl PublicPost {
67 pub fn into_display_post(self) -> Post {
69 Post {
70 id: self.id,
71 author_id: self.author_id,
72 title: self.title,
73 slug: self.slug,
74 summary: self.summary,
75 content_md: String::new(),
76 content_html: self.content_html,
77 status: PostStatus::Published,
78 published_at: self.published_at,
79 created_at: self.created_at,
80 updated_at: self.updated_at,
81 deleted_at: None,
82 tags: self.tags,
83 cover_image: self.cover_image,
84 reading_time: self.reading_time,
85 word_count: self.word_count,
86 toc_html: self.toc_html,
87 prev_post: self.prev_post,
88 next_post: self.next_post,
89 }
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96
97 #[test]
98 fn reading_payload_omits_markdown_and_preserves_rendered_content() {
99 let now = Utc::now();
100 let full = Post {
101 id: 42,
102 author_id: 7,
103 title: "文章".into(),
104 slug: "post".into(),
105 summary: Some("摘要".into()),
106 content_md: "仅编辑需要的原文".into(),
107 content_html: Some("<p>正文</p>".into()),
108 status: PostStatus::Published,
109 published_at: Some(now),
110 created_at: now,
111 updated_at: now,
112 deleted_at: None,
113 tags: vec!["Rust".into()],
114 cover_image: None,
115 reading_time: 2,
116 word_count: 300,
117 toc_html: Some("<nav>目录</nav>".into()),
118 prev_post: Some(PostNav {
119 title: "上一篇".into(),
120 slug: "prev".into(),
121 }),
122 next_post: None,
123 };
124 let public = PublicPost::from(full.clone());
125 let payload = serde_json::to_value(&public).unwrap();
126 assert!(payload.get("content_md").is_none());
127 assert_eq!(
128 serde_json::to_value(&full).unwrap()["content_md"],
129 full.content_md
130 );
131 let display = serde_json::from_value::<PublicPost>(payload)
132 .unwrap()
133 .into_display_post();
134 assert_eq!(display.content_html, full.content_html);
135 assert_eq!(display.toc_html, full.toc_html);
136 assert_eq!(display.prev_post, full.prev_post);
137 assert_eq!(display.author_id, full.author_id);
138 assert_eq!(display.updated_at, full.updated_at);
139 }
140}