1use chrono::{DateTime, Utc};
38use serde::{Deserialize, Serialize};
39
40use crate::rest::{ResourceOperation, ResourcePath, RestResource};
41use crate::HttpMethod;
42
43use super::common::BlogCommentable;
44
45#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
68pub struct Blog {
69 #[serde(skip_serializing)]
72 pub id: Option<u64>,
73
74 #[serde(skip_serializing_if = "Option::is_none")]
76 pub title: Option<String>,
77
78 #[serde(skip_serializing_if = "Option::is_none")]
81 pub handle: Option<String>,
82
83 #[serde(skip_serializing_if = "Option::is_none")]
85 pub commentable: Option<BlogCommentable>,
86
87 #[serde(skip_serializing_if = "Option::is_none")]
91 pub template_suffix: Option<String>,
92
93 #[serde(skip_serializing_if = "Option::is_none")]
96 pub feedburner: Option<String>,
97
98 #[serde(skip_serializing_if = "Option::is_none")]
100 pub feedburner_location: Option<String>,
101
102 #[serde(skip_serializing_if = "Option::is_none")]
104 pub tags: Option<String>,
105
106 #[serde(skip_serializing)]
109 pub created_at: Option<DateTime<Utc>>,
110
111 #[serde(skip_serializing)]
114 pub updated_at: Option<DateTime<Utc>>,
115
116 #[serde(skip_serializing)]
119 pub admin_graphql_api_id: Option<String>,
120}
121
122impl RestResource for Blog {
123 type Id = u64;
124 type FindParams = BlogFindParams;
125 type AllParams = BlogListParams;
126 type CountParams = BlogCountParams;
127
128 const NAME: &'static str = "Blog";
129 const PLURAL: &'static str = "blogs";
130
131 const PATHS: &'static [ResourcePath] = &[
132 ResourcePath::new(
133 HttpMethod::Get,
134 ResourceOperation::Find,
135 &["id"],
136 "blogs/{id}",
137 ),
138 ResourcePath::new(HttpMethod::Get, ResourceOperation::All, &[], "blogs"),
139 ResourcePath::new(
140 HttpMethod::Get,
141 ResourceOperation::Count,
142 &[],
143 "blogs/count",
144 ),
145 ResourcePath::new(HttpMethod::Post, ResourceOperation::Create, &[], "blogs"),
146 ResourcePath::new(
147 HttpMethod::Put,
148 ResourceOperation::Update,
149 &["id"],
150 "blogs/{id}",
151 ),
152 ResourcePath::new(
153 HttpMethod::Delete,
154 ResourceOperation::Delete,
155 &["id"],
156 "blogs/{id}",
157 ),
158 ];
159
160 fn get_id(&self) -> Option<Self::Id> {
161 self.id
162 }
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
167pub struct BlogFindParams {
168 #[serde(skip_serializing_if = "Option::is_none")]
170 pub fields: Option<String>,
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
175pub struct BlogListParams {
176 #[serde(skip_serializing_if = "Option::is_none")]
178 pub handle: Option<String>,
179
180 #[serde(skip_serializing_if = "Option::is_none")]
182 pub created_at_min: Option<DateTime<Utc>>,
183
184 #[serde(skip_serializing_if = "Option::is_none")]
186 pub created_at_max: Option<DateTime<Utc>>,
187
188 #[serde(skip_serializing_if = "Option::is_none")]
190 pub updated_at_min: Option<DateTime<Utc>>,
191
192 #[serde(skip_serializing_if = "Option::is_none")]
194 pub updated_at_max: Option<DateTime<Utc>>,
195
196 #[serde(skip_serializing_if = "Option::is_none")]
198 pub limit: Option<u32>,
199
200 #[serde(skip_serializing_if = "Option::is_none")]
202 pub since_id: Option<u64>,
203
204 #[serde(skip_serializing_if = "Option::is_none")]
206 pub page_info: Option<String>,
207
208 #[serde(skip_serializing_if = "Option::is_none")]
210 pub fields: Option<String>,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
215pub struct BlogCountParams {
216 }
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222 use crate::rest::{get_path, ResourceOperation};
223
224 #[test]
225 fn test_blog_struct_serialization() {
226 let blog = Blog {
227 id: Some(12345),
228 title: Some("Company News".to_string()),
229 handle: Some("news".to_string()),
230 commentable: Some(BlogCommentable::Moderate),
231 template_suffix: Some("custom".to_string()),
232 feedburner: Some("https://feeds.feedburner.com/example".to_string()),
233 feedburner_location: Some("example".to_string()),
234 tags: Some("news, updates".to_string()),
235 created_at: Some(
236 DateTime::parse_from_rfc3339("2024-01-10T08:00:00Z")
237 .unwrap()
238 .with_timezone(&Utc),
239 ),
240 updated_at: Some(
241 DateTime::parse_from_rfc3339("2024-06-20T15:45:00Z")
242 .unwrap()
243 .with_timezone(&Utc),
244 ),
245 admin_graphql_api_id: Some("gid://shopify/OnlineStoreBlog/12345".to_string()),
246 };
247
248 let json = serde_json::to_string(&blog).unwrap();
249 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
250
251 assert_eq!(parsed["title"], "Company News");
253 assert_eq!(parsed["handle"], "news");
254 assert_eq!(parsed["commentable"], "moderate");
255 assert_eq!(parsed["template_suffix"], "custom");
256 assert_eq!(parsed["feedburner"], "https://feeds.feedburner.com/example");
257 assert_eq!(parsed["feedburner_location"], "example");
258 assert_eq!(parsed["tags"], "news, updates");
259
260 assert!(parsed.get("id").is_none());
262 assert!(parsed.get("created_at").is_none());
263 assert!(parsed.get("updated_at").is_none());
264 assert!(parsed.get("admin_graphql_api_id").is_none());
265 }
266
267 #[test]
268 fn test_blog_deserialization_from_api_response() {
269 let json = r#"{
270 "id": 241253187,
271 "handle": "apple-blog",
272 "title": "Apple Blog",
273 "updated_at": "2024-06-20T15:45:00Z",
274 "commentable": "no",
275 "feedburner": null,
276 "feedburner_location": null,
277 "created_at": "2024-01-10T08:00:00Z",
278 "template_suffix": null,
279 "tags": "apple, tech",
280 "admin_graphql_api_id": "gid://shopify/OnlineStoreBlog/241253187"
281 }"#;
282
283 let blog: Blog = serde_json::from_str(json).unwrap();
284
285 assert_eq!(blog.id, Some(241253187));
286 assert_eq!(blog.handle, Some("apple-blog".to_string()));
287 assert_eq!(blog.title, Some("Apple Blog".to_string()));
288 assert_eq!(blog.commentable, Some(BlogCommentable::No));
289 assert!(blog.feedburner.is_none());
290 assert!(blog.feedburner_location.is_none());
291 assert!(blog.template_suffix.is_none());
292 assert_eq!(blog.tags, Some("apple, tech".to_string()));
293 assert!(blog.created_at.is_some());
294 assert!(blog.updated_at.is_some());
295 assert_eq!(
296 blog.admin_graphql_api_id,
297 Some("gid://shopify/OnlineStoreBlog/241253187".to_string())
298 );
299 }
300
301 #[test]
302 fn test_blog_commentable_enum_handling() {
303 let variants = [
305 (BlogCommentable::No, "no"),
306 (BlogCommentable::Moderate, "moderate"),
307 (BlogCommentable::Yes, "yes"),
308 ];
309
310 for (commentable, expected_str) in variants {
311 let blog = Blog {
312 title: Some("Test Blog".to_string()),
313 commentable: Some(commentable),
314 ..Default::default()
315 };
316
317 let json = serde_json::to_value(&blog).unwrap();
318 assert_eq!(json["commentable"], expected_str);
319 }
320 }
321
322 #[test]
323 fn test_blog_list_params_serialization() {
324 let params = BlogListParams {
325 handle: Some("news".to_string()),
326 limit: Some(50),
327 since_id: Some(100),
328 ..Default::default()
329 };
330
331 let json = serde_json::to_value(¶ms).unwrap();
332
333 assert_eq!(json["handle"], "news");
334 assert_eq!(json["limit"], 50);
335 assert_eq!(json["since_id"], 100);
336
337 assert!(json.get("created_at_min").is_none());
339 assert!(json.get("page_info").is_none());
340 }
341
342 #[test]
343 fn test_blog_path_constants_are_correct() {
344 let find_path = get_path(Blog::PATHS, ResourceOperation::Find, &["id"]);
346 assert!(find_path.is_some());
347 assert_eq!(find_path.unwrap().template, "blogs/{id}");
348 assert_eq!(find_path.unwrap().http_method, HttpMethod::Get);
349
350 let all_path = get_path(Blog::PATHS, ResourceOperation::All, &[]);
352 assert!(all_path.is_some());
353 assert_eq!(all_path.unwrap().template, "blogs");
354 assert_eq!(all_path.unwrap().http_method, HttpMethod::Get);
355
356 let count_path = get_path(Blog::PATHS, ResourceOperation::Count, &[]);
358 assert!(count_path.is_some());
359 assert_eq!(count_path.unwrap().template, "blogs/count");
360 assert_eq!(count_path.unwrap().http_method, HttpMethod::Get);
361
362 let create_path = get_path(Blog::PATHS, ResourceOperation::Create, &[]);
364 assert!(create_path.is_some());
365 assert_eq!(create_path.unwrap().template, "blogs");
366 assert_eq!(create_path.unwrap().http_method, HttpMethod::Post);
367
368 let update_path = get_path(Blog::PATHS, ResourceOperation::Update, &["id"]);
370 assert!(update_path.is_some());
371 assert_eq!(update_path.unwrap().template, "blogs/{id}");
372 assert_eq!(update_path.unwrap().http_method, HttpMethod::Put);
373
374 let delete_path = get_path(Blog::PATHS, ResourceOperation::Delete, &["id"]);
376 assert!(delete_path.is_some());
377 assert_eq!(delete_path.unwrap().template, "blogs/{id}");
378 assert_eq!(delete_path.unwrap().http_method, HttpMethod::Delete);
379
380 assert_eq!(Blog::NAME, "Blog");
382 assert_eq!(Blog::PLURAL, "blogs");
383 }
384
385 #[test]
386 fn test_blog_get_id_returns_correct_value() {
387 let blog_with_id = Blog {
389 id: Some(123456789),
390 title: Some("Test Blog".to_string()),
391 ..Default::default()
392 };
393 assert_eq!(blog_with_id.get_id(), Some(123456789));
394
395 let blog_without_id = Blog {
397 id: None,
398 title: Some("New Blog".to_string()),
399 ..Default::default()
400 };
401 assert_eq!(blog_without_id.get_id(), None);
402 }
403
404 #[test]
405 fn test_blog_tags_field_handling() {
406 let blog = Blog {
408 title: Some("Tech Blog".to_string()),
409 tags: Some("tech, programming, rust, web".to_string()),
410 ..Default::default()
411 };
412
413 let json = serde_json::to_value(&blog).unwrap();
414 assert_eq!(json["tags"], "tech, programming, rust, web");
415
416 let deserialized: Blog = serde_json::from_value(json).unwrap();
418 assert_eq!(
419 deserialized.tags,
420 Some("tech, programming, rust, web".to_string())
421 );
422 }
423}