mockforge_plugin_registry/
reviews.rs1use crate::{RegistryError, Result};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct Review {
10 pub id: String,
12
13 pub plugin_name: String,
15
16 pub version: String,
18
19 pub user: UserInfo,
21
22 pub rating: u8,
24
25 pub comment: String,
27
28 pub title: Option<String>,
30
31 pub helpful_count: u32,
33
34 pub unhelpful_count: u32,
36
37 pub verified: bool,
39
40 pub created_at: String,
42
43 pub updated_at: String,
45
46 pub author_response: Option<AuthorResponse>,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct UserInfo {
53 pub id: String,
54 pub name: String,
55 pub avatar_url: Option<String>,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct AuthorResponse {
61 pub text: String,
62 pub created_at: String,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct SubmitReviewRequest {
68 pub plugin_name: String,
69 pub version: String,
70 pub rating: u8,
71 pub title: Option<String>,
72 pub comment: String,
73}
74
75impl SubmitReviewRequest {
76 pub fn validate(&self) -> Result<()> {
78 if self.rating < 1 || self.rating > 5 {
79 return Err(RegistryError::InvalidManifest(
80 "Rating must be between 1 and 5".to_string(),
81 ));
82 }
83
84 if self.comment.len() < 10 {
85 return Err(RegistryError::InvalidManifest(
86 "Review comment must be at least 10 characters".to_string(),
87 ));
88 }
89
90 if self.comment.len() > 5000 {
91 return Err(RegistryError::InvalidManifest(
92 "Review comment must be less than 5000 characters".to_string(),
93 ));
94 }
95
96 if let Some(title) = &self.title {
97 if title.len() > 100 {
98 return Err(RegistryError::InvalidManifest(
99 "Review title must be less than 100 characters".to_string(),
100 ));
101 }
102 }
103
104 Ok(())
105 }
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct UpdateReviewRequest {
111 pub rating: Option<u8>,
112 pub title: Option<String>,
113 pub comment: Option<String>,
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct VoteRequest {
119 pub helpful: bool,
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct ReviewStats {
125 pub total_reviews: u32,
126 pub average_rating: f64,
127 pub rating_distribution: HashMap<u8, u32>,
128}
129
130impl ReviewStats {
131 pub fn empty() -> Self {
133 Self {
134 total_reviews: 0,
135 average_rating: 0.0,
136 rating_distribution: HashMap::new(),
137 }
138 }
139
140 pub fn from_reviews(reviews: &[Review]) -> Self {
142 let mut distribution = HashMap::new();
143 let mut total_rating = 0u32;
144
145 for review in reviews {
146 *distribution.entry(review.rating).or_insert(0) += 1;
147 total_rating += review.rating as u32;
148 }
149
150 let average_rating = if reviews.is_empty() {
151 0.0
152 } else {
153 total_rating as f64 / reviews.len() as f64
154 };
155
156 Self {
157 total_reviews: reviews.len() as u32,
158 average_rating,
159 rating_distribution: distribution,
160 }
161 }
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct ReviewQuery {
167 pub plugin_name: String,
168 pub version: Option<String>,
169 pub min_rating: Option<u8>,
170 pub sort_by: ReviewSortOrder,
171 pub page: usize,
172 pub per_page: usize,
173}
174
175impl Default for ReviewQuery {
176 fn default() -> Self {
177 Self {
178 plugin_name: String::new(),
179 version: None,
180 min_rating: None,
181 sort_by: ReviewSortOrder::MostHelpful,
182 page: 0,
183 per_page: 20,
184 }
185 }
186}
187
188#[derive(Debug, Clone, Serialize, Deserialize)]
190#[serde(rename_all = "snake_case")]
191pub enum ReviewSortOrder {
192 MostHelpful,
193 MostRecent,
194 HighestRated,
195 LowestRated,
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct ReviewResults {
201 pub reviews: Vec<Review>,
202 pub stats: ReviewStats,
203 pub total: usize,
204 pub page: usize,
205 pub per_page: usize,
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize)]
210#[serde(rename_all = "snake_case")]
211pub enum ModerationAction {
212 Approve,
213 Reject,
214 Flag,
215 Delete,
216}
217
218#[derive(Debug, Clone, Serialize, Deserialize)]
220pub struct ModerationRequest {
221 pub action: ModerationAction,
222 pub reason: Option<String>,
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[test]
230 fn test_review_validation() {
231 let valid = SubmitReviewRequest {
232 plugin_name: "test-plugin".to_string(),
233 version: "1.0.0".to_string(),
234 rating: 4,
235 title: Some("Great plugin!".to_string()),
236 comment: "This plugin works great for my use case.".to_string(),
237 };
238 assert!(valid.validate().is_ok());
239
240 let invalid_rating = SubmitReviewRequest {
241 rating: 6,
242 ..valid.clone()
243 };
244 assert!(invalid_rating.validate().is_err());
245
246 let short_comment = SubmitReviewRequest {
247 comment: "Too short".to_string(),
248 ..valid.clone()
249 };
250 assert!(short_comment.validate().is_err());
251 }
252
253 #[test]
254 fn test_review_stats_calculation() {
255 let reviews = vec![
256 Review {
257 id: "1".to_string(),
258 plugin_name: "test".to_string(),
259 version: "1.0.0".to_string(),
260 user: UserInfo {
261 id: "u1".to_string(),
262 name: "User 1".to_string(),
263 avatar_url: None,
264 },
265 rating: 5,
266 comment: "Excellent!".to_string(),
267 title: None,
268 helpful_count: 10,
269 unhelpful_count: 0,
270 verified: true,
271 created_at: "2025-01-01T00:00:00Z".to_string(),
272 updated_at: "2025-01-01T00:00:00Z".to_string(),
273 author_response: None,
274 },
275 Review {
276 id: "2".to_string(),
277 plugin_name: "test".to_string(),
278 version: "1.0.0".to_string(),
279 user: UserInfo {
280 id: "u2".to_string(),
281 name: "User 2".to_string(),
282 avatar_url: None,
283 },
284 rating: 3,
285 comment: "It's okay".to_string(),
286 title: None,
287 helpful_count: 5,
288 unhelpful_count: 2,
289 verified: false,
290 created_at: "2025-01-02T00:00:00Z".to_string(),
291 updated_at: "2025-01-02T00:00:00Z".to_string(),
292 author_response: None,
293 },
294 ];
295
296 let stats = ReviewStats::from_reviews(&reviews);
297 assert_eq!(stats.total_reviews, 2);
298 assert_eq!(stats.average_rating, 4.0);
299 assert_eq!(stats.rating_distribution.get(&5), Some(&1));
300 assert_eq!(stats.rating_distribution.get(&3), Some(&1));
301 }
302}