1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::fmt;
6
7#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
9pub struct PostId(pub String);
10
11impl fmt::Display for PostId {
12 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13 write!(f, "{}", self.0)
14 }
15}
16
17impl<T: Into<String>> From<T> for PostId {
18 fn from(s: T) -> Self {
19 Self(s.into())
20 }
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct Post {
26 pub id: PostId,
28 pub provider: String,
30 pub platform: String,
32 #[serde(skip_serializing_if = "Option::is_none")]
34 pub message: Option<String>,
35 #[serde(skip_serializing_if = "Option::is_none")]
37 pub link: Option<String>,
38 #[serde(skip_serializing_if = "Option::is_none")]
40 pub image_url: Option<String>,
41 #[serde(skip_serializing_if = "Option::is_none")]
43 pub permalink: Option<String>,
44 pub created_at: DateTime<Utc>,
46 #[serde(skip_serializing_if = "Option::is_none")]
48 pub raw: Option<serde_json::Value>,
49}
50
51#[derive(Debug, Clone, Default, Serialize, Deserialize)]
53pub struct PublishPostInput {
54 pub platform: String,
56 #[serde(skip_serializing_if = "Option::is_none")]
58 pub message: Option<String>,
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub link: Option<String>,
62 #[serde(skip_serializing_if = "Option::is_none")]
64 pub image_url: Option<String>,
65 #[serde(skip_serializing_if = "Option::is_none")]
67 pub video_url: Option<String>,
68 #[serde(skip_serializing_if = "Option::is_none")]
70 pub extra: Option<serde_json::Value>,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct PromotePostInput {
79 pub adset_id: String,
81 #[serde(skip_serializing_if = "Option::is_none")]
83 pub name: Option<String>,
84 #[serde(skip_serializing_if = "Option::is_none")]
86 pub extra: Option<serde_json::Value>,
87}
88
89impl crate::output::Formattable for Post {
90 fn headers() -> Vec<String> {
91 vec![
92 "ID".into(),
93 "Platform".into(),
94 "Message".into(),
95 "Permalink".into(),
96 "Provider".into(),
97 ]
98 }
99
100 fn row(&self) -> Vec<String> {
101 vec![
102 self.id.to_string(),
103 self.platform.clone(),
104 self.message
105 .as_deref()
106 .unwrap_or("-")
107 .chars()
108 .take(60)
109 .collect(),
110 self.permalink.clone().unwrap_or_else(|| "-".to_string()),
111 self.provider.clone(),
112 ]
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[test]
121 fn post_id_display() {
122 let id = PostId("post_123".into());
123 assert_eq!(id.to_string(), "post_123");
124 }
125
126 #[test]
127 fn post_id_from_str() {
128 let id = PostId::from("post_456");
129 assert_eq!(id.0, "post_456");
130 }
131
132 #[test]
133 #[allow(clippy::expect_used)]
134 fn post_id_serde_roundtrip() {
135 let id = PostId("post_789".into());
136 let json = serde_json::to_string(&id).expect("serialize PostId");
137 let back: PostId = serde_json::from_str(&json).expect("deserialize PostId");
138 assert_eq!(id, back);
139 }
140
141 #[test]
142 fn publish_post_input_default() {
143 let input = PublishPostInput::default();
144 assert!(input.platform.is_empty());
145 assert!(input.message.is_none());
146 assert!(input.link.is_none());
147 assert!(input.image_url.is_none());
148 assert!(input.video_url.is_none());
149 assert!(input.extra.is_none());
150 }
151
152 #[test]
153 #[allow(clippy::expect_used)]
154 fn publish_post_input_skips_none_fields() {
155 let input = PublishPostInput {
156 platform: "facebook".into(),
157 message: Some("Hello!".into()),
158 link: None,
159 image_url: None,
160 video_url: None,
161 extra: None,
162 };
163 let json = serde_json::to_string(&input).expect("serialize PublishPostInput");
164 assert!(!json.contains("link"));
165 assert!(!json.contains("image_url"));
166 assert!(!json.contains("video_url"));
167 assert!(!json.contains("extra"));
168 }
169
170 #[test]
171 #[allow(clippy::expect_used)]
172 fn promote_post_input_serde_roundtrip() {
173 let input = PromotePostInput {
174 adset_id: "23845600000000001".into(),
175 name: Some("Boost — Summer Post".into()),
176 extra: None,
177 };
178 let json = serde_json::to_string(&input).expect("serialize PromotePostInput");
179 let back: PromotePostInput =
180 serde_json::from_str(&json).expect("deserialize PromotePostInput");
181 assert_eq!(back.adset_id, "23845600000000001");
182 assert_eq!(back.name.as_deref(), Some("Boost — Summer Post"));
183 assert!(back.extra.is_none());
184 }
185
186 #[test]
187 #[allow(clippy::expect_used)]
188 fn promote_post_input_optional_fields_skip_serializing_if_none() {
189 let input = PromotePostInput {
190 adset_id: "1".into(),
191 name: None,
192 extra: None,
193 };
194 let json = serde_json::to_string(&input).expect("serialize PromotePostInput");
195 assert!(!json.contains("name"));
196 assert!(!json.contains("extra"));
197 }
198
199 #[test]
200 #[allow(clippy::expect_used)]
201 fn post_optional_fields_skip_serializing_if_none() {
202 let now = chrono::Utc::now();
203 let post = Post {
204 id: PostId("p_1".into()),
205 provider: "meta".into(),
206 platform: "facebook".into(),
207 message: None,
208 link: None,
209 image_url: None,
210 permalink: None,
211 created_at: now,
212 raw: None,
213 };
214 let json = serde_json::to_string(&post).expect("serialize Post");
215 assert!(!json.contains("message"));
216 assert!(!json.contains("link"));
217 assert!(!json.contains("image_url"));
218 assert!(!json.contains("permalink"));
219 assert!(!json.contains("raw"));
220 }
221}