Skip to main content

ghost_io_api/models/
author.rs

1//! Author model for Ghost API.
2//!
3//! Authors are the people who write posts and pages in Ghost.
4//!
5//! # Example
6//!
7//! ```
8//! use ghost_io_api::models::author::Author;
9//!
10//! let author = Author {
11//!     id: "1".to_string(),
12//!     name: "Jane Doe".to_string(),
13//!     slug: "jane-doe".to_string(),
14//!     ..Default::default()
15//! };
16//!
17//! assert_eq!(author.name, "Jane Doe");
18//! ```
19
20use serde::{Deserialize, Serialize};
21
22use crate::models::tag::PostCount;
23
24/// A Ghost author resource.
25///
26/// # Example
27///
28/// ```
29/// use ghost_io_api::models::author::Author;
30/// use serde_json::json;
31///
32/// let json = json!({
33///     "id": "1",
34///     "name": "Jane Doe",
35///     "slug": "jane-doe",
36///     "profile_image": "https://example.com/avatar.jpg"
37/// });
38/// let author: Author = serde_json::from_value(json).unwrap();
39/// assert_eq!(author.name, "Jane Doe");
40/// ```
41#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
42pub struct Author {
43    /// Ghost object ID.
44    pub id: String,
45    /// Display name.
46    pub name: String,
47    /// URL slug.
48    pub slug: String,
49    /// Profile image URL.
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub profile_image: Option<String>,
52    /// Cover image URL.
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub cover_image: Option<String>,
55    /// Author bio.
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub bio: Option<String>,
58    /// Personal website URL.
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub website: Option<String>,
61    /// Location string.
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub location: Option<String>,
64    /// Facebook profile URL or username.
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub facebook: Option<String>,
67    /// Twitter handle (without `@`).
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub twitter: Option<String>,
70    /// Meta title override.
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub meta_title: Option<String>,
73    /// Meta description override.
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub meta_description: Option<String>,
76    /// Public URL of the author's archive page.
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub url: Option<String>,
79    /// ISO 8601 creation timestamp.
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub created_at: Option<String>,
82    /// ISO 8601 last-updated timestamp.
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub updated_at: Option<String>,
85    /// Post count (populated when `include=count.posts`).
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub count: Option<PostCount>,
88}
89
90impl Author {
91    /// Returns `true` if the author has a profile image set.
92    ///
93    /// # Example
94    ///
95    /// ```
96    /// use ghost_io_api::models::author::Author;
97    ///
98    /// let author = Author {
99    ///     profile_image: Some("https://example.com/img.jpg".to_string()),
100    ///     ..Default::default()
101    /// };
102    /// assert!(author.has_profile_image());
103    /// ```
104    pub fn has_profile_image(&self) -> bool {
105        self.profile_image.is_some()
106    }
107
108    /// Returns the number of published posts by this author, if available.
109    ///
110    /// Returns `None` unless `include=count.posts` was in the request.
111    ///
112    /// # Example
113    ///
114    /// ```
115    /// use ghost_io_api::models::author::Author;
116    /// use ghost_io_api::models::tag::PostCount;
117    ///
118    /// let author = Author {
119    ///     count: Some(PostCount { posts: 5 }),
120    ///     ..Default::default()
121    /// };
122    /// assert_eq!(author.post_count(), Some(5));
123    /// ```
124    pub fn post_count(&self) -> Option<u32> {
125        self.count.as_ref().map(|c| c.posts)
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132    use serde_json::json;
133
134    #[test]
135    fn test_author_minimal_deserialization() {
136        let json = json!({ "id": "1", "name": "Jane Doe", "slug": "jane-doe" });
137        let author: Author = serde_json::from_value(json).unwrap();
138        assert_eq!(author.id, "1");
139        assert_eq!(author.name, "Jane Doe");
140        assert_eq!(author.slug, "jane-doe");
141    }
142
143    #[test]
144    fn test_author_full_deserialization() {
145        let json = json!({
146            "id": "1",
147            "name": "Jane Doe",
148            "slug": "jane-doe",
149            "profile_image": "https://example.com/avatar.jpg",
150            "cover_image": "https://example.com/cover.jpg",
151            "bio": "Writer",
152            "website": "https://janedoe.com",
153            "location": "New York",
154            "facebook": "janedoe",
155            "twitter": "@janedoe",
156            "meta_title": "Jane Doe - Writer",
157            "meta_description": "Posts by Jane",
158            "url": "https://example.com/author/jane-doe/",
159            "created_at": "2021-01-01T00:00:00.000Z",
160            "updated_at": "2021-06-01T00:00:00.000Z",
161            "count": { "posts": 12 }
162        });
163        let author: Author = serde_json::from_value(json).unwrap();
164        assert_eq!(author.bio, Some("Writer".to_string()));
165        assert!(author.has_profile_image());
166        assert_eq!(author.post_count(), Some(12));
167    }
168
169    #[test]
170    fn test_has_profile_image_true() {
171        let author = Author {
172            profile_image: Some("https://example.com/img.jpg".to_string()),
173            ..Default::default()
174        };
175        assert!(author.has_profile_image());
176    }
177
178    #[test]
179    fn test_has_profile_image_false() {
180        let author = Author::default();
181        assert!(!author.has_profile_image());
182    }
183
184    #[test]
185    fn test_post_count_some() {
186        let author = Author {
187            count: Some(PostCount { posts: 7 }),
188            ..Default::default()
189        };
190        assert_eq!(author.post_count(), Some(7));
191    }
192
193    #[test]
194    fn test_post_count_none() {
195        let author = Author::default();
196        assert_eq!(author.post_count(), None);
197    }
198
199    #[test]
200    fn test_author_serialization_skips_none() {
201        let author = Author {
202            id: "1".to_string(),
203            name: "J".to_string(),
204            slug: "j".to_string(),
205            ..Default::default()
206        };
207        let json = serde_json::to_value(&author).unwrap();
208        let obj = json.as_object().unwrap();
209        assert!(!obj.contains_key("bio"));
210        assert!(!obj.contains_key("profile_image"));
211        assert!(!obj.contains_key("count"));
212    }
213
214    #[test]
215    fn test_author_default() {
216        let author = Author::default();
217        assert!(author.id.is_empty());
218        assert!(author.count.is_none());
219        assert!(!author.has_profile_image());
220    }
221
222    #[test]
223    fn test_author_clone_eq() {
224        let author = Author {
225            id: "1".to_string(),
226            name: "J".to_string(),
227            slug: "j".to_string(),
228            ..Default::default()
229        };
230        assert_eq!(author, author.clone());
231    }
232}