Skip to main content

shopify_sdk/rest/resources/v2026_04/
comment.rs

1//! Comment resource implementation.
2//!
3//! This module provides the [`Comment`] resource for managing blog article
4//! comments in a Shopify store. Comments can be moderated, approved, marked
5//! as spam, or removed.
6//!
7//! # Comment Moderation
8//!
9//! Comments have several moderation methods:
10//! - `approve()` - Approve a pending comment for publication
11//! - `spam()` - Mark a comment as spam
12//! - `not_spam()` - Mark a comment as not spam
13//! - `remove()` - Remove a comment from publication
14//! - `restore()` - Restore a removed comment
15//!
16//! # Example
17//!
18//! ```rust,ignore
19//! use shopify_sdk::rest::{RestResource, ResourceResponse};
20//! use shopify_sdk::rest::resources::v2026_04::{Comment, CommentListParams};
21//!
22//! // List all comments
23//! let comments = Comment::all(&client, None).await?;
24//!
25//! // Moderate a comment
26//! let comment = Comment::find(&client, 653537639, None).await?.into_inner();
27//! let approved = comment.approve(&client).await?;
28//!
29//! // Mark a comment as spam
30//! let marked = comment.spam(&client).await?;
31//!
32//! // Filter comments by status
33//! let params = CommentListParams {
34//!     status: Some("pending".to_string()),
35//!     ..Default::default()
36//! };
37//! let pending = Comment::all(&client, Some(params)).await?;
38//! ```
39
40use 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/// A comment on a blog article.
53///
54/// Comments can be moderated through various methods. The status
55/// determines the visibility and state of the comment.
56///
57/// # Moderation Methods
58///
59/// - `approve()` - Change status to "published"
60/// - `spam()` - Mark as spam
61/// - `not_spam()` - Remove spam flag
62/// - `remove()` - Change status to "removed"
63/// - `restore()` - Change status from "removed" to previous state
64///
65/// # Fields
66///
67/// ## Read-Only Fields
68/// - `id` - The unique identifier
69/// - `article_id` - The article the comment belongs to
70/// - `blog_id` - The blog the article belongs to
71/// - `status` - The comment status (pending, published, spam, removed)
72/// - `ip` - The IP address of the commenter
73/// - `user_agent` - The browser user agent of the commenter
74/// - `published_at` - When the comment was published
75/// - `created_at` - When the comment was created
76/// - `updated_at` - When the comment was last updated
77///
78/// ## Writable Fields
79/// - `author` - The name of the comment author
80/// - `email` - The email of the comment author
81/// - `body` - The comment text
82/// - `body_html` - The comment text in HTML
83#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
84pub struct Comment {
85    /// The unique identifier of the comment.
86    /// Read-only field.
87    #[serde(skip_serializing)]
88    pub id: Option<u64>,
89
90    /// The ID of the article this comment belongs to.
91    /// Read-only field.
92    #[serde(skip_serializing)]
93    pub article_id: Option<u64>,
94
95    /// The ID of the blog this comment belongs to.
96    /// Read-only field.
97    #[serde(skip_serializing)]
98    pub blog_id: Option<u64>,
99
100    /// The name of the comment author.
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub author: Option<String>,
103
104    /// The email address of the comment author.
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub email: Option<String>,
107
108    /// The text of the comment.
109    #[serde(skip_serializing_if = "Option::is_none")]
110    pub body: Option<String>,
111
112    /// The text of the comment in HTML format.
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub body_html: Option<String>,
115
116    /// The status of the comment: pending, published, spam, removed.
117    /// Read-only field - use moderation methods to change.
118    #[serde(skip_serializing)]
119    pub status: Option<String>,
120
121    /// The IP address of the commenter.
122    /// Read-only field.
123    #[serde(skip_serializing)]
124    pub ip: Option<String>,
125
126    /// The browser user agent of the commenter.
127    /// Read-only field.
128    #[serde(skip_serializing)]
129    pub user_agent: Option<String>,
130
131    /// When the comment was published.
132    /// Read-only field.
133    #[serde(skip_serializing)]
134    pub published_at: Option<DateTime<Utc>>,
135
136    /// When the comment was created.
137    /// Read-only field.
138    #[serde(skip_serializing)]
139    pub created_at: Option<DateTime<Utc>>,
140
141    /// When the comment was last updated.
142    /// Read-only field.
143    #[serde(skip_serializing)]
144    pub updated_at: Option<DateTime<Utc>>,
145}
146
147impl Comment {
148    /// Approves a comment for publication.
149    ///
150    /// Changes the comment status to "published".
151    ///
152    /// # Errors
153    ///
154    /// Returns an error if the comment has no ID.
155    ///
156    /// # Example
157    ///
158    /// ```rust,ignore
159    /// let comment = Comment::find(&client, 653537639, None).await?.into_inner();
160    /// let approved = comment.approve(&client).await?;
161    /// ```
162    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    /// Marks a comment as spam.
189    ///
190    /// # Errors
191    ///
192    /// Returns an error if the comment has no ID.
193    ///
194    /// # Example
195    ///
196    /// ```rust,ignore
197    /// let comment = Comment::find(&client, 653537639, None).await?.into_inner();
198    /// let marked = comment.spam(&client).await?;
199    /// ```
200    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    /// Marks a comment as not spam.
224    ///
225    /// # Errors
226    ///
227    /// Returns an error if the comment has no ID.
228    ///
229    /// # Example
230    ///
231    /// ```rust,ignore
232    /// let comment = Comment::find(&client, 653537639, None).await?.into_inner();
233    /// let marked = comment.not_spam(&client).await?;
234    /// ```
235    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    /// Removes a comment from publication.
262    ///
263    /// Changes the comment status to "removed".
264    ///
265    /// # Errors
266    ///
267    /// Returns an error if the comment has no ID.
268    ///
269    /// # Example
270    ///
271    /// ```rust,ignore
272    /// let comment = Comment::find(&client, 653537639, None).await?.into_inner();
273    /// let removed = comment.remove(&client).await?;
274    /// ```
275    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    /// Restores a removed comment.
302    ///
303    /// Returns the comment to its previous state before removal.
304    ///
305    /// # Errors
306    ///
307    /// Returns an error if the comment has no ID.
308    ///
309    /// # Example
310    ///
311    /// ```rust,ignore
312    /// let comment = Comment::find(&client, 653537639, None).await?.into_inner();
313    /// let restored = comment.restore(&client).await?;
314    /// ```
315    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    /// Counts comments under a specific article.
342    ///
343    /// # Arguments
344    ///
345    /// * `client` - The REST client
346    /// * `article_id` - The article ID
347    /// * `params` - Optional count parameters
348    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        // Build query params
367        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    /// Paths for the Comment resource.
441    ///
442    /// Full CRUD operations plus article-specific paths.
443    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        // Article-specific paths
471        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/// Parameters for finding a single comment.
491#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
492pub struct CommentFindParams {
493    /// Comma-separated list of fields to include in the response.
494    #[serde(skip_serializing_if = "Option::is_none")]
495    pub fields: Option<String>,
496}
497
498/// Parameters for listing comments.
499#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
500pub struct CommentListParams {
501    /// Maximum number of results to return (default: 50, max: 250).
502    #[serde(skip_serializing_if = "Option::is_none")]
503    pub limit: Option<u32>,
504
505    /// Return comments after this ID.
506    #[serde(skip_serializing_if = "Option::is_none")]
507    pub since_id: Option<u64>,
508
509    /// Show comments created after this date.
510    #[serde(skip_serializing_if = "Option::is_none")]
511    pub created_at_min: Option<DateTime<Utc>>,
512
513    /// Show comments created before this date.
514    #[serde(skip_serializing_if = "Option::is_none")]
515    pub created_at_max: Option<DateTime<Utc>>,
516
517    /// Show comments updated after this date.
518    #[serde(skip_serializing_if = "Option::is_none")]
519    pub updated_at_min: Option<DateTime<Utc>>,
520
521    /// Show comments updated before this date.
522    #[serde(skip_serializing_if = "Option::is_none")]
523    pub updated_at_max: Option<DateTime<Utc>>,
524
525    /// Show comments published after this date.
526    #[serde(skip_serializing_if = "Option::is_none")]
527    pub published_at_min: Option<DateTime<Utc>>,
528
529    /// Show comments published before this date.
530    #[serde(skip_serializing_if = "Option::is_none")]
531    pub published_at_max: Option<DateTime<Utc>>,
532
533    /// Filter comments by status: pending, published, spam, removed.
534    #[serde(skip_serializing_if = "Option::is_none")]
535    pub status: Option<String>,
536
537    /// Comma-separated list of fields to include in the response.
538    #[serde(skip_serializing_if = "Option::is_none")]
539    pub fields: Option<String>,
540}
541
542/// Parameters for counting comments.
543#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
544pub struct CommentCountParams {
545    /// Show comments created after this date.
546    #[serde(skip_serializing_if = "Option::is_none")]
547    pub created_at_min: Option<DateTime<Utc>>,
548
549    /// Show comments created before this date.
550    #[serde(skip_serializing_if = "Option::is_none")]
551    pub created_at_max: Option<DateTime<Utc>>,
552
553    /// Show comments updated after this date.
554    #[serde(skip_serializing_if = "Option::is_none")]
555    pub updated_at_min: Option<DateTime<Utc>>,
556
557    /// Show comments updated before this date.
558    #[serde(skip_serializing_if = "Option::is_none")]
559    pub updated_at_max: Option<DateTime<Utc>>,
560
561    /// Show comments published after this date.
562    #[serde(skip_serializing_if = "Option::is_none")]
563    pub published_at_min: Option<DateTime<Utc>>,
564
565    /// Show comments published before this date.
566    #[serde(skip_serializing_if = "Option::is_none")]
567    pub published_at_max: Option<DateTime<Utc>>,
568
569    /// Filter comments by status: pending, published, spam, removed.
570    #[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        // Writable fields should be present
613        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        // Read-only fields should be omitted
619        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        // The moderation methods use URLs like:
665        // POST /comments/{id}/approve
666        // POST /comments/{id}/spam
667        // POST /comments/{id}/not_spam
668        // POST /comments/{id}/remove
669        // POST /comments/{id}/restore
670        //
671        // These are implemented as instance methods that construct URLs directly
672
673        let comment = Comment {
674            id: Some(653537639),
675            ..Default::default()
676        };
677
678        // Verify that the ID is available for URL construction
679        assert_eq!(comment.id, Some(653537639));
680        // URLs would be: comments/653537639/approve, etc.
681    }
682
683    #[test]
684    fn test_comment_full_crud_paths() {
685        // Find by ID
686        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        // List all
691        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        // Count
696        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        // Create
701        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        // Update
706        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        // Delete
711        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        // Comments for an article
719        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        // Count comments for an article
727        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(&params).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}