1use serde::{Deserialize, Serialize};
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
26#[serde(rename_all = "lowercase")]
27pub enum PageStatus {
28 #[default]
30 Draft,
31 Published,
33 Scheduled,
35}
36
37impl PageStatus {
38 pub fn is_published(&self) -> bool {
49 matches!(self, PageStatus::Published)
50 }
51
52 pub fn is_draft(&self) -> bool {
62 matches!(self, PageStatus::Draft)
63 }
64
65 pub fn is_scheduled(&self) -> bool {
75 matches!(self, PageStatus::Scheduled)
76 }
77}
78
79#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
101pub struct Page {
102 pub id: String,
104 #[serde(skip_serializing_if = "Option::is_none")]
106 pub uuid: Option<String>,
107 pub title: String,
109 pub slug: String,
111 #[serde(skip_serializing_if = "Option::is_none")]
113 pub html: Option<String>,
114 #[serde(skip_serializing_if = "Option::is_none")]
116 pub plaintext: Option<String>,
117 #[serde(skip_serializing_if = "Option::is_none")]
119 pub feature_image: Option<String>,
120 #[serde(skip_serializing_if = "Option::is_none")]
122 pub feature_image_alt: Option<String>,
123 #[serde(skip_serializing_if = "Option::is_none")]
125 pub feature_image_caption: Option<String>,
126 #[serde(default)]
128 pub featured: bool,
129 #[serde(default)]
131 pub status: PageStatus,
132 #[serde(skip_serializing_if = "Option::is_none")]
134 pub visibility: Option<String>,
135 #[serde(skip_serializing_if = "Option::is_none")]
137 pub created_at: Option<String>,
138 #[serde(skip_serializing_if = "Option::is_none")]
140 pub updated_at: Option<String>,
141 #[serde(skip_serializing_if = "Option::is_none")]
143 pub published_at: Option<String>,
144 #[serde(skip_serializing_if = "Option::is_none")]
146 pub custom_excerpt: Option<String>,
147 #[serde(skip_serializing_if = "Option::is_none")]
149 pub excerpt: Option<String>,
150 #[serde(skip_serializing_if = "Option::is_none")]
152 pub reading_time: Option<u32>,
153 #[serde(default)]
155 pub access: bool,
156 #[serde(skip_serializing_if = "Option::is_none")]
158 pub canonical_url: Option<String>,
159 #[serde(skip_serializing_if = "Option::is_none")]
161 pub url: Option<String>,
162 #[serde(skip_serializing_if = "Option::is_none")]
164 pub og_image: Option<String>,
165 #[serde(skip_serializing_if = "Option::is_none")]
167 pub og_title: Option<String>,
168 #[serde(skip_serializing_if = "Option::is_none")]
170 pub og_description: Option<String>,
171 #[serde(skip_serializing_if = "Option::is_none")]
173 pub twitter_image: Option<String>,
174 #[serde(skip_serializing_if = "Option::is_none")]
176 pub twitter_title: Option<String>,
177 #[serde(skip_serializing_if = "Option::is_none")]
179 pub twitter_description: Option<String>,
180 #[serde(skip_serializing_if = "Option::is_none")]
182 pub meta_title: Option<String>,
183 #[serde(skip_serializing_if = "Option::is_none")]
185 pub meta_description: Option<String>,
186 #[serde(skip_serializing_if = "Option::is_none")]
188 pub custom_template: Option<String>,
189}
190
191impl Page {
192 pub fn is_published(&self) -> bool {
194 self.status.is_published()
195 }
196
197 pub fn is_draft(&self) -> bool {
199 self.status.is_draft()
200 }
201
202 pub fn is_scheduled(&self) -> bool {
204 self.status.is_scheduled()
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211 use serde_json::json;
212
213 #[test]
214 fn test_page_status_default() {
215 assert_eq!(PageStatus::default(), PageStatus::Draft);
216 }
217
218 #[test]
219 fn test_page_status_is_published() {
220 assert!(PageStatus::Published.is_published());
221 assert!(!PageStatus::Draft.is_published());
222 assert!(!PageStatus::Scheduled.is_published());
223 }
224
225 #[test]
226 fn test_page_status_is_draft() {
227 assert!(PageStatus::Draft.is_draft());
228 assert!(!PageStatus::Published.is_draft());
229 }
230
231 #[test]
232 fn test_page_status_is_scheduled() {
233 assert!(PageStatus::Scheduled.is_scheduled());
234 assert!(!PageStatus::Published.is_scheduled());
235 }
236
237 #[test]
238 fn test_page_status_serialization() {
239 assert_eq!(
240 serde_json::to_string(&PageStatus::Published).unwrap(),
241 "\"published\""
242 );
243 assert_eq!(
244 serde_json::to_string(&PageStatus::Draft).unwrap(),
245 "\"draft\""
246 );
247 assert_eq!(
248 serde_json::to_string(&PageStatus::Scheduled).unwrap(),
249 "\"scheduled\""
250 );
251 }
252
253 #[test]
254 fn test_page_status_deserialization() {
255 let s: PageStatus = serde_json::from_str("\"published\"").unwrap();
256 assert_eq!(s, PageStatus::Published);
257 let d: PageStatus = serde_json::from_str("\"draft\"").unwrap();
258 assert_eq!(d, PageStatus::Draft);
259 }
260
261 #[test]
262 fn test_page_minimal_deserialization() {
263 let json = json!({
264 "id": "abc123",
265 "title": "About",
266 "slug": "about"
267 });
268 let page: Page = serde_json::from_value(json).unwrap();
269 assert_eq!(page.id, "abc123");
270 assert_eq!(page.title, "About");
271 assert_eq!(page.slug, "about");
272 assert_eq!(page.status, PageStatus::Draft);
273 assert!(!page.featured);
274 }
275
276 #[test]
277 fn test_page_full_deserialization() {
278 let json = json!({
279 "id": "abc123",
280 "uuid": "a-b-c-d",
281 "title": "About",
282 "slug": "about",
283 "html": "<p>Hello</p>",
284 "plaintext": "Hello",
285 "feature_image": "https://example.com/img.jpg",
286 "featured": false,
287 "status": "published",
288 "visibility": "public",
289 "created_at": "2021-01-01T00:00:00.000Z",
290 "updated_at": "2021-01-02T00:00:00.000Z",
291 "published_at": "2021-01-02T00:00:00.000Z",
292 "custom_excerpt": "Custom",
293 "excerpt": "Auto",
294 "reading_time": 2,
295 "access": true,
296 "url": "https://example.com/about/"
297 });
298 let page: Page = serde_json::from_value(json).unwrap();
299 assert_eq!(page.status, PageStatus::Published);
300 assert!(page.is_published());
301 assert_eq!(page.reading_time, Some(2));
302 assert!(page.access);
303 }
304
305 #[test]
306 fn test_page_default() {
307 let page = Page::default();
308 assert_eq!(page.status, PageStatus::Draft);
309 assert!(page.id.is_empty());
310 assert!(!page.featured);
311 assert!(!page.access);
312 }
313
314 #[test]
315 fn test_page_is_methods() {
316 let published = Page {
317 status: PageStatus::Published,
318 ..Default::default()
319 };
320 assert!(published.is_published());
321 assert!(!published.is_draft());
322 assert!(!published.is_scheduled());
323
324 let draft = Page {
325 status: PageStatus::Draft,
326 ..Default::default()
327 };
328 assert!(draft.is_draft());
329
330 let scheduled = Page {
331 status: PageStatus::Scheduled,
332 ..Default::default()
333 };
334 assert!(scheduled.is_scheduled());
335 }
336
337 #[test]
338 fn test_page_serialization_skips_none() {
339 let page = Page {
340 id: "1".to_string(),
341 title: "T".to_string(),
342 slug: "t".to_string(),
343 ..Default::default()
344 };
345 let json = serde_json::to_value(&page).unwrap();
346 let obj = json.as_object().unwrap();
347 assert!(!obj.contains_key("html"));
348 assert!(!obj.contains_key("feature_image"));
349 assert!(!obj.contains_key("og_image"));
350 }
351
352 #[test]
353 fn test_page_clone_eq() {
354 let page = Page {
355 id: "1".to_string(),
356 title: "T".to_string(),
357 slug: "t".to_string(),
358 status: PageStatus::Published,
359 ..Default::default()
360 };
361 assert_eq!(page, page.clone());
362 }
363}