1use std::collections::HashMap;
41
42use chrono::{DateTime, Utc};
43use serde::{Deserialize, Serialize};
44
45use crate::clients::RestClient;
46use crate::rest::{
47 build_path, get_path, ResourceError, ResourceOperation, ResourcePath, ResourceResponse,
48 RestResource,
49};
50use crate::HttpMethod;
51
52#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
84pub struct Comment {
85 #[serde(skip_serializing)]
88 pub id: Option<u64>,
89
90 #[serde(skip_serializing)]
93 pub article_id: Option<u64>,
94
95 #[serde(skip_serializing)]
98 pub blog_id: Option<u64>,
99
100 #[serde(skip_serializing_if = "Option::is_none")]
102 pub author: Option<String>,
103
104 #[serde(skip_serializing_if = "Option::is_none")]
106 pub email: Option<String>,
107
108 #[serde(skip_serializing_if = "Option::is_none")]
110 pub body: Option<String>,
111
112 #[serde(skip_serializing_if = "Option::is_none")]
114 pub body_html: Option<String>,
115
116 #[serde(skip_serializing)]
119 pub status: Option<String>,
120
121 #[serde(skip_serializing)]
124 pub ip: Option<String>,
125
126 #[serde(skip_serializing)]
129 pub user_agent: Option<String>,
130
131 #[serde(skip_serializing)]
134 pub published_at: Option<DateTime<Utc>>,
135
136 #[serde(skip_serializing)]
139 pub created_at: Option<DateTime<Utc>>,
140
141 #[serde(skip_serializing)]
144 pub updated_at: Option<DateTime<Utc>>,
145}
146
147impl Comment {
148 pub async fn approve(
163 &self,
164 client: &RestClient,
165 ) -> Result<ResourceResponse<Self>, ResourceError> {
166 let id = self.get_id().ok_or(ResourceError::PathResolutionFailed {
167 resource: Self::NAME,
168 operation: "approve",
169 })?;
170
171 let url = format!("comments/{id}/approve");
172 let response = client.post(&url, serde_json::json!({}), None).await?;
173
174 if !response.is_ok() {
175 return Err(ResourceError::from_http_response(
176 response.code,
177 &response.body,
178 Self::NAME,
179 Some(&id.to_string()),
180 response.request_id(),
181 ));
182 }
183
184 let key = Self::resource_key();
185 ResourceResponse::from_http_response(response, &key)
186 }
187
188 pub async fn spam(&self, client: &RestClient) -> Result<ResourceResponse<Self>, ResourceError> {
201 let id = self.get_id().ok_or(ResourceError::PathResolutionFailed {
202 resource: Self::NAME,
203 operation: "spam",
204 })?;
205
206 let url = format!("comments/{id}/spam");
207 let response = client.post(&url, serde_json::json!({}), None).await?;
208
209 if !response.is_ok() {
210 return Err(ResourceError::from_http_response(
211 response.code,
212 &response.body,
213 Self::NAME,
214 Some(&id.to_string()),
215 response.request_id(),
216 ));
217 }
218
219 let key = Self::resource_key();
220 ResourceResponse::from_http_response(response, &key)
221 }
222
223 pub async fn not_spam(
236 &self,
237 client: &RestClient,
238 ) -> Result<ResourceResponse<Self>, ResourceError> {
239 let id = self.get_id().ok_or(ResourceError::PathResolutionFailed {
240 resource: Self::NAME,
241 operation: "not_spam",
242 })?;
243
244 let url = format!("comments/{id}/not_spam");
245 let response = client.post(&url, serde_json::json!({}), None).await?;
246
247 if !response.is_ok() {
248 return Err(ResourceError::from_http_response(
249 response.code,
250 &response.body,
251 Self::NAME,
252 Some(&id.to_string()),
253 response.request_id(),
254 ));
255 }
256
257 let key = Self::resource_key();
258 ResourceResponse::from_http_response(response, &key)
259 }
260
261 pub async fn remove(
276 &self,
277 client: &RestClient,
278 ) -> Result<ResourceResponse<Self>, ResourceError> {
279 let id = self.get_id().ok_or(ResourceError::PathResolutionFailed {
280 resource: Self::NAME,
281 operation: "remove",
282 })?;
283
284 let url = format!("comments/{id}/remove");
285 let response = client.post(&url, serde_json::json!({}), None).await?;
286
287 if !response.is_ok() {
288 return Err(ResourceError::from_http_response(
289 response.code,
290 &response.body,
291 Self::NAME,
292 Some(&id.to_string()),
293 response.request_id(),
294 ));
295 }
296
297 let key = Self::resource_key();
298 ResourceResponse::from_http_response(response, &key)
299 }
300
301 pub async fn restore(
316 &self,
317 client: &RestClient,
318 ) -> Result<ResourceResponse<Self>, ResourceError> {
319 let id = self.get_id().ok_or(ResourceError::PathResolutionFailed {
320 resource: Self::NAME,
321 operation: "restore",
322 })?;
323
324 let url = format!("comments/{id}/restore");
325 let response = client.post(&url, serde_json::json!({}), None).await?;
326
327 if !response.is_ok() {
328 return Err(ResourceError::from_http_response(
329 response.code,
330 &response.body,
331 Self::NAME,
332 Some(&id.to_string()),
333 response.request_id(),
334 ));
335 }
336
337 let key = Self::resource_key();
338 ResourceResponse::from_http_response(response, &key)
339 }
340
341 pub async fn count_for_article(
349 client: &RestClient,
350 article_id: u64,
351 params: Option<CommentCountParams>,
352 ) -> Result<u64, ResourceError> {
353 let mut ids: HashMap<&str, String> = HashMap::new();
354 ids.insert("article_id", article_id.to_string());
355
356 let available_ids: Vec<&str> = ids.keys().copied().collect();
357 let path = get_path(Self::PATHS, ResourceOperation::Count, &available_ids).ok_or(
358 ResourceError::PathResolutionFailed {
359 resource: Self::NAME,
360 operation: "count",
361 },
362 )?;
363
364 let url = build_path(path.template, &ids);
365
366 let query = params
368 .map(|p| {
369 let value = serde_json::to_value(&p).map_err(|e| {
370 ResourceError::Http(crate::clients::HttpError::Response(
371 crate::clients::HttpResponseError {
372 code: 400,
373 message: format!("Failed to serialize params: {e}"),
374 error_reference: None,
375 },
376 ))
377 })?;
378
379 let mut query = HashMap::new();
380 if let serde_json::Value::Object(map) = value {
381 for (key, val) in map {
382 match val {
383 serde_json::Value::String(s) => {
384 query.insert(key, s);
385 }
386 serde_json::Value::Number(n) => {
387 query.insert(key, n.to_string());
388 }
389 serde_json::Value::Bool(b) => {
390 query.insert(key, b.to_string());
391 }
392 _ => {}
393 }
394 }
395 }
396 Ok::<_, ResourceError>(query)
397 })
398 .transpose()?
399 .filter(|q| !q.is_empty());
400
401 let response = client.get(&url, query).await?;
402
403 if !response.is_ok() {
404 return Err(ResourceError::from_http_response(
405 response.code,
406 &response.body,
407 Self::NAME,
408 None,
409 response.request_id(),
410 ));
411 }
412
413 let count = response
414 .body
415 .get("count")
416 .and_then(serde_json::Value::as_u64)
417 .ok_or_else(|| {
418 ResourceError::Http(crate::clients::HttpError::Response(
419 crate::clients::HttpResponseError {
420 code: response.code,
421 message: "Missing 'count' in response".to_string(),
422 error_reference: response.request_id().map(ToString::to_string),
423 },
424 ))
425 })?;
426
427 Ok(count)
428 }
429}
430
431impl RestResource for Comment {
432 type Id = u64;
433 type FindParams = CommentFindParams;
434 type AllParams = CommentListParams;
435 type CountParams = CommentCountParams;
436
437 const NAME: &'static str = "Comment";
438 const PLURAL: &'static str = "comments";
439
440 const PATHS: &'static [ResourcePath] = &[
444 ResourcePath::new(
445 HttpMethod::Get,
446 ResourceOperation::Find,
447 &["id"],
448 "comments/{id}",
449 ),
450 ResourcePath::new(HttpMethod::Get, ResourceOperation::All, &[], "comments"),
451 ResourcePath::new(
452 HttpMethod::Get,
453 ResourceOperation::Count,
454 &[],
455 "comments/count",
456 ),
457 ResourcePath::new(HttpMethod::Post, ResourceOperation::Create, &[], "comments"),
458 ResourcePath::new(
459 HttpMethod::Put,
460 ResourceOperation::Update,
461 &["id"],
462 "comments/{id}",
463 ),
464 ResourcePath::new(
465 HttpMethod::Delete,
466 ResourceOperation::Delete,
467 &["id"],
468 "comments/{id}",
469 ),
470 ResourcePath::new(
472 HttpMethod::Get,
473 ResourceOperation::All,
474 &["article_id"],
475 "articles/{article_id}/comments",
476 ),
477 ResourcePath::new(
478 HttpMethod::Get,
479 ResourceOperation::Count,
480 &["article_id"],
481 "articles/{article_id}/comments/count",
482 ),
483 ];
484
485 fn get_id(&self) -> Option<Self::Id> {
486 self.id
487 }
488}
489
490#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
492pub struct CommentFindParams {
493 #[serde(skip_serializing_if = "Option::is_none")]
495 pub fields: Option<String>,
496}
497
498#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
500pub struct CommentListParams {
501 #[serde(skip_serializing_if = "Option::is_none")]
503 pub limit: Option<u32>,
504
505 #[serde(skip_serializing_if = "Option::is_none")]
507 pub since_id: Option<u64>,
508
509 #[serde(skip_serializing_if = "Option::is_none")]
511 pub created_at_min: Option<DateTime<Utc>>,
512
513 #[serde(skip_serializing_if = "Option::is_none")]
515 pub created_at_max: Option<DateTime<Utc>>,
516
517 #[serde(skip_serializing_if = "Option::is_none")]
519 pub updated_at_min: Option<DateTime<Utc>>,
520
521 #[serde(skip_serializing_if = "Option::is_none")]
523 pub updated_at_max: Option<DateTime<Utc>>,
524
525 #[serde(skip_serializing_if = "Option::is_none")]
527 pub published_at_min: Option<DateTime<Utc>>,
528
529 #[serde(skip_serializing_if = "Option::is_none")]
531 pub published_at_max: Option<DateTime<Utc>>,
532
533 #[serde(skip_serializing_if = "Option::is_none")]
535 pub status: Option<String>,
536
537 #[serde(skip_serializing_if = "Option::is_none")]
539 pub fields: Option<String>,
540}
541
542#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
544pub struct CommentCountParams {
545 #[serde(skip_serializing_if = "Option::is_none")]
547 pub created_at_min: Option<DateTime<Utc>>,
548
549 #[serde(skip_serializing_if = "Option::is_none")]
551 pub created_at_max: Option<DateTime<Utc>>,
552
553 #[serde(skip_serializing_if = "Option::is_none")]
555 pub updated_at_min: Option<DateTime<Utc>>,
556
557 #[serde(skip_serializing_if = "Option::is_none")]
559 pub updated_at_max: Option<DateTime<Utc>>,
560
561 #[serde(skip_serializing_if = "Option::is_none")]
563 pub published_at_min: Option<DateTime<Utc>>,
564
565 #[serde(skip_serializing_if = "Option::is_none")]
567 pub published_at_max: Option<DateTime<Utc>>,
568
569 #[serde(skip_serializing_if = "Option::is_none")]
571 pub status: Option<String>,
572}
573
574#[cfg(test)]
575mod tests {
576 use super::*;
577 use crate::rest::{get_path, ResourceOperation};
578
579 #[test]
580 fn test_comment_serialization() {
581 let comment = Comment {
582 id: Some(653537639),
583 article_id: Some(134645308),
584 blog_id: Some(241253187),
585 author: Some("John Doe".to_string()),
586 email: Some("john@example.com".to_string()),
587 body: Some("Great article!".to_string()),
588 body_html: Some("<p>Great article!</p>".to_string()),
589 status: Some("published".to_string()),
590 ip: Some("192.168.1.1".to_string()),
591 user_agent: Some("Mozilla/5.0".to_string()),
592 published_at: Some(
593 DateTime::parse_from_rfc3339("2024-06-15T10:30:00Z")
594 .unwrap()
595 .with_timezone(&Utc),
596 ),
597 created_at: Some(
598 DateTime::parse_from_rfc3339("2024-06-15T10:00:00Z")
599 .unwrap()
600 .with_timezone(&Utc),
601 ),
602 updated_at: Some(
603 DateTime::parse_from_rfc3339("2024-06-15T10:30:00Z")
604 .unwrap()
605 .with_timezone(&Utc),
606 ),
607 };
608
609 let json = serde_json::to_string(&comment).unwrap();
610 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
611
612 assert_eq!(parsed["author"], "John Doe");
614 assert_eq!(parsed["email"], "john@example.com");
615 assert_eq!(parsed["body"], "Great article!");
616 assert_eq!(parsed["body_html"], "<p>Great article!</p>");
617
618 assert!(parsed.get("id").is_none());
620 assert!(parsed.get("article_id").is_none());
621 assert!(parsed.get("blog_id").is_none());
622 assert!(parsed.get("status").is_none());
623 assert!(parsed.get("ip").is_none());
624 assert!(parsed.get("user_agent").is_none());
625 assert!(parsed.get("published_at").is_none());
626 assert!(parsed.get("created_at").is_none());
627 assert!(parsed.get("updated_at").is_none());
628 }
629
630 #[test]
631 fn test_comment_deserialization() {
632 let json = r#"{
633 "id": 653537639,
634 "article_id": 134645308,
635 "blog_id": 241253187,
636 "author": "John Doe",
637 "email": "john@example.com",
638 "body": "Great article!",
639 "body_html": "<p>Great article!</p>",
640 "status": "published",
641 "ip": "192.168.1.1",
642 "user_agent": "Mozilla/5.0",
643 "published_at": "2024-06-15T10:30:00Z",
644 "created_at": "2024-06-15T10:00:00Z",
645 "updated_at": "2024-06-15T10:30:00Z"
646 }"#;
647
648 let comment: Comment = serde_json::from_str(json).unwrap();
649
650 assert_eq!(comment.id, Some(653537639));
651 assert_eq!(comment.article_id, Some(134645308));
652 assert_eq!(comment.blog_id, Some(241253187));
653 assert_eq!(comment.author, Some("John Doe".to_string()));
654 assert_eq!(comment.email, Some("john@example.com".to_string()));
655 assert_eq!(comment.body, Some("Great article!".to_string()));
656 assert_eq!(comment.status, Some("published".to_string()));
657 assert_eq!(comment.ip, Some("192.168.1.1".to_string()));
658 assert!(comment.published_at.is_some());
659 assert!(comment.created_at.is_some());
660 }
661
662 #[test]
663 fn test_comment_moderation_methods_path_construction() {
664 let comment = Comment {
674 id: Some(653537639),
675 ..Default::default()
676 };
677
678 assert_eq!(comment.id, Some(653537639));
680 }
682
683 #[test]
684 fn test_comment_full_crud_paths() {
685 let find_path = get_path(Comment::PATHS, ResourceOperation::Find, &["id"]);
687 assert!(find_path.is_some());
688 assert_eq!(find_path.unwrap().template, "comments/{id}");
689
690 let all_path = get_path(Comment::PATHS, ResourceOperation::All, &[]);
692 assert!(all_path.is_some());
693 assert_eq!(all_path.unwrap().template, "comments");
694
695 let count_path = get_path(Comment::PATHS, ResourceOperation::Count, &[]);
697 assert!(count_path.is_some());
698 assert_eq!(count_path.unwrap().template, "comments/count");
699
700 let create_path = get_path(Comment::PATHS, ResourceOperation::Create, &[]);
702 assert!(create_path.is_some());
703 assert_eq!(create_path.unwrap().template, "comments");
704
705 let update_path = get_path(Comment::PATHS, ResourceOperation::Update, &["id"]);
707 assert!(update_path.is_some());
708 assert_eq!(update_path.unwrap().template, "comments/{id}");
709
710 let delete_path = get_path(Comment::PATHS, ResourceOperation::Delete, &["id"]);
712 assert!(delete_path.is_some());
713 assert_eq!(delete_path.unwrap().template, "comments/{id}");
714 }
715
716 #[test]
717 fn test_comment_article_specific_paths() {
718 let article_comments = get_path(Comment::PATHS, ResourceOperation::All, &["article_id"]);
720 assert!(article_comments.is_some());
721 assert_eq!(
722 article_comments.unwrap().template,
723 "articles/{article_id}/comments"
724 );
725
726 let article_count = get_path(Comment::PATHS, ResourceOperation::Count, &["article_id"]);
728 assert!(article_count.is_some());
729 assert_eq!(
730 article_count.unwrap().template,
731 "articles/{article_id}/comments/count"
732 );
733 }
734
735 #[test]
736 fn test_comment_list_params() {
737 let params = CommentListParams {
738 limit: Some(50),
739 status: Some("pending".to_string()),
740 since_id: Some(100),
741 ..Default::default()
742 };
743
744 let json = serde_json::to_value(¶ms).unwrap();
745
746 assert_eq!(json["limit"], 50);
747 assert_eq!(json["status"], "pending");
748 assert_eq!(json["since_id"], 100);
749 }
750
751 #[test]
752 fn test_comment_constants() {
753 assert_eq!(Comment::NAME, "Comment");
754 assert_eq!(Comment::PLURAL, "comments");
755 }
756
757 #[test]
758 fn test_comment_get_id() {
759 let comment_with_id = Comment {
760 id: Some(653537639),
761 ..Default::default()
762 };
763 assert_eq!(comment_with_id.get_id(), Some(653537639));
764
765 let comment_without_id = Comment::default();
766 assert_eq!(comment_without_id.get_id(), None);
767 }
768}