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(&self, client: &RestClient) -> Result<ResourceResponse<Self>, ResourceError> {
163 let id = self.get_id().ok_or(ResourceError::PathResolutionFailed {
164 resource: Self::NAME,
165 operation: "approve",
166 })?;
167
168 let url = format!("comments/{id}/approve");
169 let response = client.post(&url, serde_json::json!({}), None).await?;
170
171 if !response.is_ok() {
172 return Err(ResourceError::from_http_response(
173 response.code,
174 &response.body,
175 Self::NAME,
176 Some(&id.to_string()),
177 response.request_id(),
178 ));
179 }
180
181 let key = Self::resource_key();
182 ResourceResponse::from_http_response(response, &key)
183 }
184
185 pub async fn spam(&self, client: &RestClient) -> Result<ResourceResponse<Self>, ResourceError> {
198 let id = self.get_id().ok_or(ResourceError::PathResolutionFailed {
199 resource: Self::NAME,
200 operation: "spam",
201 })?;
202
203 let url = format!("comments/{id}/spam");
204 let response = client.post(&url, serde_json::json!({}), None).await?;
205
206 if !response.is_ok() {
207 return Err(ResourceError::from_http_response(
208 response.code,
209 &response.body,
210 Self::NAME,
211 Some(&id.to_string()),
212 response.request_id(),
213 ));
214 }
215
216 let key = Self::resource_key();
217 ResourceResponse::from_http_response(response, &key)
218 }
219
220 pub async fn not_spam(&self, client: &RestClient) -> Result<ResourceResponse<Self>, ResourceError> {
233 let id = self.get_id().ok_or(ResourceError::PathResolutionFailed {
234 resource: Self::NAME,
235 operation: "not_spam",
236 })?;
237
238 let url = format!("comments/{id}/not_spam");
239 let response = client.post(&url, serde_json::json!({}), None).await?;
240
241 if !response.is_ok() {
242 return Err(ResourceError::from_http_response(
243 response.code,
244 &response.body,
245 Self::NAME,
246 Some(&id.to_string()),
247 response.request_id(),
248 ));
249 }
250
251 let key = Self::resource_key();
252 ResourceResponse::from_http_response(response, &key)
253 }
254
255 pub async fn remove(&self, client: &RestClient) -> Result<ResourceResponse<Self>, ResourceError> {
270 let id = self.get_id().ok_or(ResourceError::PathResolutionFailed {
271 resource: Self::NAME,
272 operation: "remove",
273 })?;
274
275 let url = format!("comments/{id}/remove");
276 let response = client.post(&url, serde_json::json!({}), None).await?;
277
278 if !response.is_ok() {
279 return Err(ResourceError::from_http_response(
280 response.code,
281 &response.body,
282 Self::NAME,
283 Some(&id.to_string()),
284 response.request_id(),
285 ));
286 }
287
288 let key = Self::resource_key();
289 ResourceResponse::from_http_response(response, &key)
290 }
291
292 pub async fn restore(&self, client: &RestClient) -> Result<ResourceResponse<Self>, ResourceError> {
307 let id = self.get_id().ok_or(ResourceError::PathResolutionFailed {
308 resource: Self::NAME,
309 operation: "restore",
310 })?;
311
312 let url = format!("comments/{id}/restore");
313 let response = client.post(&url, serde_json::json!({}), None).await?;
314
315 if !response.is_ok() {
316 return Err(ResourceError::from_http_response(
317 response.code,
318 &response.body,
319 Self::NAME,
320 Some(&id.to_string()),
321 response.request_id(),
322 ));
323 }
324
325 let key = Self::resource_key();
326 ResourceResponse::from_http_response(response, &key)
327 }
328
329 pub async fn count_for_article(
337 client: &RestClient,
338 article_id: u64,
339 params: Option<CommentCountParams>,
340 ) -> Result<u64, ResourceError> {
341 let mut ids: HashMap<&str, String> = HashMap::new();
342 ids.insert("article_id", article_id.to_string());
343
344 let available_ids: Vec<&str> = ids.keys().copied().collect();
345 let path = get_path(Self::PATHS, ResourceOperation::Count, &available_ids).ok_or(
346 ResourceError::PathResolutionFailed {
347 resource: Self::NAME,
348 operation: "count",
349 },
350 )?;
351
352 let url = build_path(path.template, &ids);
353
354 let query = params
356 .map(|p| {
357 let value = serde_json::to_value(&p).map_err(|e| {
358 ResourceError::Http(crate::clients::HttpError::Response(
359 crate::clients::HttpResponseError {
360 code: 400,
361 message: format!("Failed to serialize params: {e}"),
362 error_reference: None,
363 },
364 ))
365 })?;
366
367 let mut query = HashMap::new();
368 if let serde_json::Value::Object(map) = value {
369 for (key, val) in map {
370 match val {
371 serde_json::Value::String(s) => {
372 query.insert(key, s);
373 }
374 serde_json::Value::Number(n) => {
375 query.insert(key, n.to_string());
376 }
377 serde_json::Value::Bool(b) => {
378 query.insert(key, b.to_string());
379 }
380 _ => {}
381 }
382 }
383 }
384 Ok::<_, ResourceError>(query)
385 })
386 .transpose()?
387 .filter(|q| !q.is_empty());
388
389 let response = client.get(&url, query).await?;
390
391 if !response.is_ok() {
392 return Err(ResourceError::from_http_response(
393 response.code,
394 &response.body,
395 Self::NAME,
396 None,
397 response.request_id(),
398 ));
399 }
400
401 let count = response
402 .body
403 .get("count")
404 .and_then(serde_json::Value::as_u64)
405 .ok_or_else(|| {
406 ResourceError::Http(crate::clients::HttpError::Response(
407 crate::clients::HttpResponseError {
408 code: response.code,
409 message: "Missing 'count' in response".to_string(),
410 error_reference: response.request_id().map(ToString::to_string),
411 },
412 ))
413 })?;
414
415 Ok(count)
416 }
417}
418
419impl RestResource for Comment {
420 type Id = u64;
421 type FindParams = CommentFindParams;
422 type AllParams = CommentListParams;
423 type CountParams = CommentCountParams;
424
425 const NAME: &'static str = "Comment";
426 const PLURAL: &'static str = "comments";
427
428 const PATHS: &'static [ResourcePath] = &[
432 ResourcePath::new(
433 HttpMethod::Get,
434 ResourceOperation::Find,
435 &["id"],
436 "comments/{id}",
437 ),
438 ResourcePath::new(HttpMethod::Get, ResourceOperation::All, &[], "comments"),
439 ResourcePath::new(
440 HttpMethod::Get,
441 ResourceOperation::Count,
442 &[],
443 "comments/count",
444 ),
445 ResourcePath::new(
446 HttpMethod::Post,
447 ResourceOperation::Create,
448 &[],
449 "comments",
450 ),
451 ResourcePath::new(
452 HttpMethod::Put,
453 ResourceOperation::Update,
454 &["id"],
455 "comments/{id}",
456 ),
457 ResourcePath::new(
458 HttpMethod::Delete,
459 ResourceOperation::Delete,
460 &["id"],
461 "comments/{id}",
462 ),
463 ResourcePath::new(
465 HttpMethod::Get,
466 ResourceOperation::All,
467 &["article_id"],
468 "articles/{article_id}/comments",
469 ),
470 ResourcePath::new(
471 HttpMethod::Get,
472 ResourceOperation::Count,
473 &["article_id"],
474 "articles/{article_id}/comments/count",
475 ),
476 ];
477
478 fn get_id(&self) -> Option<Self::Id> {
479 self.id
480 }
481}
482
483#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
485pub struct CommentFindParams {
486 #[serde(skip_serializing_if = "Option::is_none")]
488 pub fields: Option<String>,
489}
490
491#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
493pub struct CommentListParams {
494 #[serde(skip_serializing_if = "Option::is_none")]
496 pub limit: Option<u32>,
497
498 #[serde(skip_serializing_if = "Option::is_none")]
500 pub since_id: Option<u64>,
501
502 #[serde(skip_serializing_if = "Option::is_none")]
504 pub created_at_min: Option<DateTime<Utc>>,
505
506 #[serde(skip_serializing_if = "Option::is_none")]
508 pub created_at_max: Option<DateTime<Utc>>,
509
510 #[serde(skip_serializing_if = "Option::is_none")]
512 pub updated_at_min: Option<DateTime<Utc>>,
513
514 #[serde(skip_serializing_if = "Option::is_none")]
516 pub updated_at_max: Option<DateTime<Utc>>,
517
518 #[serde(skip_serializing_if = "Option::is_none")]
520 pub published_at_min: Option<DateTime<Utc>>,
521
522 #[serde(skip_serializing_if = "Option::is_none")]
524 pub published_at_max: Option<DateTime<Utc>>,
525
526 #[serde(skip_serializing_if = "Option::is_none")]
528 pub status: Option<String>,
529
530 #[serde(skip_serializing_if = "Option::is_none")]
532 pub fields: Option<String>,
533}
534
535#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
537pub struct CommentCountParams {
538 #[serde(skip_serializing_if = "Option::is_none")]
540 pub created_at_min: Option<DateTime<Utc>>,
541
542 #[serde(skip_serializing_if = "Option::is_none")]
544 pub created_at_max: Option<DateTime<Utc>>,
545
546 #[serde(skip_serializing_if = "Option::is_none")]
548 pub updated_at_min: Option<DateTime<Utc>>,
549
550 #[serde(skip_serializing_if = "Option::is_none")]
552 pub updated_at_max: Option<DateTime<Utc>>,
553
554 #[serde(skip_serializing_if = "Option::is_none")]
556 pub published_at_min: Option<DateTime<Utc>>,
557
558 #[serde(skip_serializing_if = "Option::is_none")]
560 pub published_at_max: Option<DateTime<Utc>>,
561
562 #[serde(skip_serializing_if = "Option::is_none")]
564 pub status: Option<String>,
565}
566
567#[cfg(test)]
568mod tests {
569 use super::*;
570 use crate::rest::{get_path, ResourceOperation};
571
572 #[test]
573 fn test_comment_serialization() {
574 let comment = Comment {
575 id: Some(653537639),
576 article_id: Some(134645308),
577 blog_id: Some(241253187),
578 author: Some("John Doe".to_string()),
579 email: Some("john@example.com".to_string()),
580 body: Some("Great article!".to_string()),
581 body_html: Some("<p>Great article!</p>".to_string()),
582 status: Some("published".to_string()),
583 ip: Some("192.168.1.1".to_string()),
584 user_agent: Some("Mozilla/5.0".to_string()),
585 published_at: Some(
586 DateTime::parse_from_rfc3339("2024-06-15T10:30:00Z")
587 .unwrap()
588 .with_timezone(&Utc),
589 ),
590 created_at: Some(
591 DateTime::parse_from_rfc3339("2024-06-15T10:00:00Z")
592 .unwrap()
593 .with_timezone(&Utc),
594 ),
595 updated_at: Some(
596 DateTime::parse_from_rfc3339("2024-06-15T10:30:00Z")
597 .unwrap()
598 .with_timezone(&Utc),
599 ),
600 };
601
602 let json = serde_json::to_string(&comment).unwrap();
603 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
604
605 assert_eq!(parsed["author"], "John Doe");
607 assert_eq!(parsed["email"], "john@example.com");
608 assert_eq!(parsed["body"], "Great article!");
609 assert_eq!(parsed["body_html"], "<p>Great article!</p>");
610
611 assert!(parsed.get("id").is_none());
613 assert!(parsed.get("article_id").is_none());
614 assert!(parsed.get("blog_id").is_none());
615 assert!(parsed.get("status").is_none());
616 assert!(parsed.get("ip").is_none());
617 assert!(parsed.get("user_agent").is_none());
618 assert!(parsed.get("published_at").is_none());
619 assert!(parsed.get("created_at").is_none());
620 assert!(parsed.get("updated_at").is_none());
621 }
622
623 #[test]
624 fn test_comment_deserialization() {
625 let json = r#"{
626 "id": 653537639,
627 "article_id": 134645308,
628 "blog_id": 241253187,
629 "author": "John Doe",
630 "email": "john@example.com",
631 "body": "Great article!",
632 "body_html": "<p>Great article!</p>",
633 "status": "published",
634 "ip": "192.168.1.1",
635 "user_agent": "Mozilla/5.0",
636 "published_at": "2024-06-15T10:30:00Z",
637 "created_at": "2024-06-15T10:00:00Z",
638 "updated_at": "2024-06-15T10:30:00Z"
639 }"#;
640
641 let comment: Comment = serde_json::from_str(json).unwrap();
642
643 assert_eq!(comment.id, Some(653537639));
644 assert_eq!(comment.article_id, Some(134645308));
645 assert_eq!(comment.blog_id, Some(241253187));
646 assert_eq!(comment.author, Some("John Doe".to_string()));
647 assert_eq!(comment.email, Some("john@example.com".to_string()));
648 assert_eq!(comment.body, Some("Great article!".to_string()));
649 assert_eq!(comment.status, Some("published".to_string()));
650 assert_eq!(comment.ip, Some("192.168.1.1".to_string()));
651 assert!(comment.published_at.is_some());
652 assert!(comment.created_at.is_some());
653 }
654
655 #[test]
656 fn test_comment_moderation_methods_path_construction() {
657 let comment = Comment {
667 id: Some(653537639),
668 ..Default::default()
669 };
670
671 assert_eq!(comment.id, Some(653537639));
673 }
675
676 #[test]
677 fn test_comment_full_crud_paths() {
678 let find_path = get_path(Comment::PATHS, ResourceOperation::Find, &["id"]);
680 assert!(find_path.is_some());
681 assert_eq!(find_path.unwrap().template, "comments/{id}");
682
683 let all_path = get_path(Comment::PATHS, ResourceOperation::All, &[]);
685 assert!(all_path.is_some());
686 assert_eq!(all_path.unwrap().template, "comments");
687
688 let count_path = get_path(Comment::PATHS, ResourceOperation::Count, &[]);
690 assert!(count_path.is_some());
691 assert_eq!(count_path.unwrap().template, "comments/count");
692
693 let create_path = get_path(Comment::PATHS, ResourceOperation::Create, &[]);
695 assert!(create_path.is_some());
696 assert_eq!(create_path.unwrap().template, "comments");
697
698 let update_path = get_path(Comment::PATHS, ResourceOperation::Update, &["id"]);
700 assert!(update_path.is_some());
701 assert_eq!(update_path.unwrap().template, "comments/{id}");
702
703 let delete_path = get_path(Comment::PATHS, ResourceOperation::Delete, &["id"]);
705 assert!(delete_path.is_some());
706 assert_eq!(delete_path.unwrap().template, "comments/{id}");
707 }
708
709 #[test]
710 fn test_comment_article_specific_paths() {
711 let article_comments = get_path(Comment::PATHS, ResourceOperation::All, &["article_id"]);
713 assert!(article_comments.is_some());
714 assert_eq!(
715 article_comments.unwrap().template,
716 "articles/{article_id}/comments"
717 );
718
719 let article_count = get_path(Comment::PATHS, ResourceOperation::Count, &["article_id"]);
721 assert!(article_count.is_some());
722 assert_eq!(
723 article_count.unwrap().template,
724 "articles/{article_id}/comments/count"
725 );
726 }
727
728 #[test]
729 fn test_comment_list_params() {
730 let params = CommentListParams {
731 limit: Some(50),
732 status: Some("pending".to_string()),
733 since_id: Some(100),
734 ..Default::default()
735 };
736
737 let json = serde_json::to_value(¶ms).unwrap();
738
739 assert_eq!(json["limit"], 50);
740 assert_eq!(json["status"], "pending");
741 assert_eq!(json["since_id"], 100);
742 }
743
744 #[test]
745 fn test_comment_constants() {
746 assert_eq!(Comment::NAME, "Comment");
747 assert_eq!(Comment::PLURAL, "comments");
748 }
749
750 #[test]
751 fn test_comment_get_id() {
752 let comment_with_id = Comment {
753 id: Some(653537639),
754 ..Default::default()
755 };
756 assert_eq!(comment_with_id.get_id(), Some(653537639));
757
758 let comment_without_id = Comment::default();
759 assert_eq!(comment_without_id.get_id(), None);
760 }
761}