1use crate::objects::{Collection, CollectionIterator, Object, ObjectOperations, ObjectResponse};
2use crate::{Client, Result};
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct Comment {
8 #[serde(flatten)]
9 pub object: Object<CommentAttributes>,
10}
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct CommentAttributes {
14 pub text: String,
15
16 #[serde(skip_serializing_if = "Option::is_none")]
17 pub date: Option<i64>,
18
19 #[serde(skip_serializing_if = "Option::is_none")]
20 pub votes: Option<CommentVotes>,
21
22 #[serde(skip_serializing_if = "Option::is_none")]
23 pub html: Option<String>,
24
25 #[serde(skip_serializing_if = "Option::is_none")]
26 pub tags: Option<Vec<String>>,
27
28 #[serde(flatten)]
29 pub additional_attributes: HashMap<String, serde_json::Value>,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct CommentVotes {
34 pub positive: u32,
35 pub negative: u32,
36 pub abuse: u32,
37}
38
39impl ObjectOperations for Comment {
40 type Attributes = CommentAttributes;
41
42 fn collection_name() -> &'static str {
43 "comments"
44 }
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct CreateCommentRequest {
49 pub data: CreateCommentData,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct CreateCommentData {
54 #[serde(rename = "type")]
55 pub object_type: String,
56 pub attributes: CreateCommentAttributes,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct CreateCommentAttributes {
61 pub text: String,
62}
63
64impl CreateCommentRequest {
65 pub fn new(text: impl Into<String>) -> Self {
66 Self {
67 data: CreateCommentData {
68 object_type: "comment".to_string(),
69 attributes: CreateCommentAttributes { text: text.into() },
70 },
71 }
72 }
73}
74
75pub struct CommentIterator<'a> {
76 inner: CollectionIterator<'a, Comment>,
77}
78
79impl<'a> CommentIterator<'a> {
80 pub fn new(client: &'a crate::Client, url: impl Into<String>) -> Self {
81 Self {
82 inner: CollectionIterator::new(client, url),
83 }
84 }
85
86 pub fn with_limit(mut self, limit: u32) -> Self {
87 self.inner = self.inner.with_limit(limit);
88 self
89 }
90
91 pub async fn next_batch(&mut self) -> crate::Result<Vec<Comment>> {
92 self.inner.next_batch().await
93 }
94
95 pub async fn collect_all(self) -> crate::Result<Vec<Comment>> {
96 self.inner.collect_all().await
97 }
98}
99
100#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
101#[serde(rename_all = "lowercase")]
102pub enum CommentVoteType {
103 Positive,
104 Negative,
105 Abuse,
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct VoteCommentRequest {
110 pub data: String,
111}
112
113impl VoteCommentRequest {
114 pub fn new(vote_type: CommentVoteType) -> Self {
115 let vote_str = match vote_type {
116 CommentVoteType::Positive => "positive",
117 CommentVoteType::Negative => "negative",
118 CommentVoteType::Abuse => "abuse",
119 };
120 Self {
121 data: vote_str.to_string(),
122 }
123 }
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct VoteCommentResponse {
128 pub data: CommentVotes,
129}
130
131pub struct CommentsClient<'a> {
133 pub(crate) client: &'a Client,
134}
135
136impl<'a> CommentsClient<'a> {
137 pub fn new(client: &'a Client) -> Self {
138 Self { client }
139 }
140
141 pub async fn get_latest(
144 &self,
145 filter: Option<&str>,
146 limit: Option<u32>,
147 ) -> Result<Collection<Comment>> {
148 let mut endpoint = "comments".to_string();
149 let mut params = Vec::new();
150
151 if let Some(f) = filter {
152 params.push(format!("filter={}", f));
153 }
154 if let Some(l) = limit {
155 params.push(format!("limit={}", l));
156 }
157
158 if !params.is_empty() {
159 endpoint.push('?');
160 endpoint.push_str(¶ms.join("&"));
161 }
162
163 self.client.get(&endpoint).await
164 }
165
166 pub fn get_latest_iterator(&self, filter: Option<String>) -> CommentIterator<'_> {
168 let mut url = "comments".to_string();
169 if let Some(f) = filter {
170 url.push_str(&format!("?filter={}", f));
171 }
172 CommentIterator::new(self.client, url)
173 }
174
175 pub async fn get(&self, comment_id: &str) -> Result<Comment> {
177 let url = Comment::object_url(comment_id);
178 let response: ObjectResponse<CommentAttributes> = self.client.get(&url).await?;
179 Ok(Comment {
180 object: response.data,
181 })
182 }
183
184 pub async fn delete(&self, comment_id: &str) -> Result<()> {
186 let url = Comment::object_url(comment_id);
187 self.client.delete(&url).await
188 }
189
190 pub async fn vote(
192 &self,
193 comment_id: &str,
194 vote_type: CommentVoteType,
195 ) -> Result<VoteCommentResponse> {
196 let endpoint = format!("{}/{}/vote", Comment::collection_name(), comment_id);
197 let request = VoteCommentRequest::new(vote_type);
198 self.client.post(&endpoint, &request).await
199 }
200
201 pub async fn get_relationship<T>(
203 &self,
204 comment_id: &str,
205 relationship: &str,
206 ) -> Result<Collection<T>>
207 where
208 T: for<'de> Deserialize<'de>,
209 {
210 let url = Comment::relationship_objects_url(comment_id, relationship);
211 self.client.get(&url).await
212 }
213
214 pub async fn get_relationship_descriptors(
216 &self,
217 comment_id: &str,
218 relationship: &str,
219 ) -> Result<Collection<serde_json::Value>> {
220 let url = Comment::relationships_url(comment_id, relationship);
221 self.client.get(&url).await
222 }
223
224 pub fn get_relationship_iterator<T>(
226 &self,
227 comment_id: &str,
228 relationship: &str,
229 ) -> CollectionIterator<'_, T>
230 where
231 T: for<'de> Deserialize<'de> + Clone,
232 {
233 let url = Comment::relationship_objects_url(comment_id, relationship);
234 CollectionIterator::new(self.client, url)
235 }
236
237 pub fn parse_comment_id(comment_id: &str) -> Option<(char, String, String)> {
240 let parts: Vec<&str> = comment_id.splitn(3, '-').collect();
241 if parts.len() == 3 {
242 let item_type = parts[0].chars().next()?;
243 let item_id = parts[1].to_string();
244 let random_string = parts[2].to_string();
245 Some((item_type, item_id, random_string))
246 } else {
247 None
248 }
249 }
250
251 pub fn get_item_type(comment_id: &str) -> Option<char> {
254 comment_id.chars().next()
255 }
256}
257
258impl Client {
259 pub fn comments(&self) -> CommentsClient<'_> {
261 CommentsClient::new(self)
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268
269 #[test]
270 fn test_create_comment_request() {
271 let request = CreateCommentRequest::new("This is a test comment");
272 assert_eq!(request.data.object_type, "comment");
273 assert_eq!(request.data.attributes.text, "This is a test comment");
274 }
275
276 #[test]
277 fn test_comment_vote_serialization() {
278 let vote = CommentVoteType::Positive;
279 let json = serde_json::to_string(&vote).unwrap();
280 assert_eq!(json, "\"positive\"");
281 }
282
283 #[test]
284 fn test_vote_comment_request() {
285 let request = VoteCommentRequest::new(CommentVoteType::Abuse);
286 assert_eq!(request.data, "abuse");
287 }
288
289 #[test]
290 fn test_comment_collection_name() {
291 assert_eq!(Comment::collection_name(), "comments");
292 }
293
294 #[test]
295 fn test_parse_comment_id() {
296 let comment_id = "f-abc123-random456";
297 let parsed = CommentsClient::parse_comment_id(comment_id);
298 assert!(parsed.is_some());
299 let (item_type, item_id, random_string) = parsed.unwrap();
300 assert_eq!(item_type, 'f');
301 assert_eq!(item_id, "abc123");
302 assert_eq!(random_string, "random456");
303 }
304
305 #[test]
306 fn test_parse_comment_id_domain() {
307 let comment_id = "d-example.com-xyz789";
308 let parsed = CommentsClient::parse_comment_id(comment_id);
309 assert!(parsed.is_some());
310 let (item_type, item_id, _) = parsed.unwrap();
311 assert_eq!(item_type, 'd');
312 assert_eq!(item_id, "example.com");
313 }
314
315 #[test]
316 fn test_parse_comment_id_url() {
317 let comment_id =
318 "u-011915942db556bbab5137f761efe61fed2b00598fea900360b800b193a7bf31-d94d7c8a";
319 let parsed = CommentsClient::parse_comment_id(comment_id);
320 assert!(parsed.is_some());
321 let (item_type, item_id, random_string) = parsed.unwrap();
322 assert_eq!(item_type, 'u');
323 assert_eq!(
324 item_id,
325 "011915942db556bbab5137f761efe61fed2b00598fea900360b800b193a7bf31"
326 );
327 assert_eq!(random_string, "d94d7c8a");
328 }
329
330 #[test]
331 fn test_get_item_type() {
332 assert_eq!(CommentsClient::get_item_type("f-abc123-random"), Some('f'));
333 assert_eq!(
334 CommentsClient::get_item_type("d-example.com-xyz"),
335 Some('d')
336 );
337 assert_eq!(
338 CommentsClient::get_item_type("i-192.168.1.1-abc"),
339 Some('i')
340 );
341 assert_eq!(CommentsClient::get_item_type("g-graph123-def"), Some('g'));
342 assert_eq!(CommentsClient::get_item_type("u-url123-ghi"), Some('u'));
343 assert_eq!(CommentsClient::get_item_type(""), None);
344 }
345
346 #[test]
347 fn test_comment_votes_structure() {
348 let votes = CommentVotes {
349 positive: 10,
350 negative: 2,
351 abuse: 1,
352 };
353 assert_eq!(votes.positive, 10);
354 assert_eq!(votes.negative, 2);
355 assert_eq!(votes.abuse, 1);
356 }
357
358 #[test]
359 fn test_vote_comment_response() {
360 let response = VoteCommentResponse {
361 data: CommentVotes {
362 positive: 5,
363 negative: 0,
364 abuse: 0,
365 },
366 };
367 assert_eq!(response.data.positive, 5);
368 assert_eq!(response.data.negative, 0);
369 assert_eq!(response.data.abuse, 0);
370 }
371}