1use crate::{BpiError, BpiResult};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub struct ArticleInfoParams {
6 id: i64,
7}
8
9impl ArticleInfoParams {
10 pub fn new(id: i64) -> BpiResult<Self> {
11 Ok(Self {
12 id: validate_positive_i64("id", id)?,
13 })
14 }
15
16 pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
17 vec![("id", self.id.to_string())]
18 }
19}
20
21#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct ArticleViewParams {
24 id: i64,
25 gaia_source: String,
26}
27
28impl ArticleViewParams {
29 pub fn new(id: i64) -> BpiResult<Self> {
30 Ok(Self {
31 id: validate_positive_i64("id", id)?,
32 gaia_source: "main_web".to_string(),
33 })
34 }
35
36 pub fn with_gaia_source(mut self, gaia_source: impl Into<String>) -> BpiResult<Self> {
37 let gaia_source = gaia_source.into();
38 validate_non_blank("gaia_source", &gaia_source)?;
39 self.gaia_source = gaia_source;
40 Ok(self)
41 }
42
43 pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
44 vec![
45 ("id", self.id.to_string()),
46 ("gaia_source", self.gaia_source.clone()),
47 ]
48 }
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct ArticleCardsParams {
54 ids: String,
55 web_location: String,
56}
57
58impl ArticleCardsParams {
59 pub fn new(ids: impl Into<String>) -> BpiResult<Self> {
60 let ids = ids.into();
61 validate_non_blank("ids", &ids)?;
62 Ok(Self {
63 ids,
64 web_location: "333.1305".to_string(),
65 })
66 }
67
68 pub fn with_web_location(mut self, web_location: impl Into<String>) -> BpiResult<Self> {
69 let web_location = web_location.into();
70 validate_non_blank("web_location", &web_location)?;
71 self.web_location = web_location;
72 Ok(self)
73 }
74
75 pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
76 vec![
77 ("ids", self.ids.clone()),
78 ("web_location", self.web_location.clone()),
79 ]
80 }
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub struct ArticleArticlesInfoParams {
86 id: i64,
87}
88
89impl ArticleArticlesInfoParams {
90 pub fn new(id: i64) -> BpiResult<Self> {
91 Ok(Self {
92 id: validate_positive_i64("id", id)?,
93 })
94 }
95
96 pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
97 vec![("id", self.id.to_string())]
98 }
99}
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103pub struct ArticleLikeParams {
104 id: i64,
105 like: bool,
106}
107
108impl ArticleLikeParams {
109 pub fn new(id: i64, like: bool) -> BpiResult<Self> {
110 Ok(Self {
111 id: validate_positive_i64("id", id)?,
112 like,
113 })
114 }
115
116 pub(crate) fn form_pairs(&self, csrf: &str) -> Vec<(&'static str, String)> {
117 vec![
118 ("id", self.id.to_string()),
119 ("type", if self.like { "1" } else { "2" }.to_string()),
120 ("csrf", csrf.to_string()),
121 ]
122 }
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub struct ArticleCoinParams {
128 aid: u64,
129 upid: u64,
130 multiply: u32,
131}
132
133impl ArticleCoinParams {
134 pub fn new(aid: u64, upid: u64, multiply: u32) -> BpiResult<Self> {
135 Ok(Self {
136 aid: validate_positive_u64("aid", aid)?,
137 upid: validate_positive_u64("upid", upid)?,
138 multiply: validate_coin_multiply(multiply)?,
139 })
140 }
141
142 pub(crate) fn form_pairs(&self, csrf: &str) -> Vec<(&'static str, String)> {
143 vec![
144 ("aid", self.aid.to_string()),
145 ("upid", self.upid.to_string()),
146 ("multiply", self.multiply.to_string()),
147 ("avtype", "2".to_string()),
148 ("csrf", csrf.to_string()),
149 ]
150 }
151}
152
153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155pub struct ArticleFavoriteParams {
156 id: i64,
157}
158
159impl ArticleFavoriteParams {
160 pub fn new(id: i64) -> BpiResult<Self> {
161 Ok(Self {
162 id: validate_positive_i64("id", id)?,
163 })
164 }
165
166 pub(crate) fn form_pairs(&self, csrf: &str) -> Vec<(&'static str, String)> {
167 vec![("id", self.id.to_string()), ("csrf", csrf.to_string())]
168 }
169}
170
171fn validate_positive_i64(field: &'static str, value: i64) -> BpiResult<i64> {
172 if value <= 0 {
173 return Err(BpiError::invalid_parameter(field, "value must be positive"));
174 }
175
176 Ok(value)
177}
178
179fn validate_positive_u64(field: &'static str, value: u64) -> BpiResult<u64> {
180 if value == 0 {
181 return Err(BpiError::invalid_parameter(field, "value must be non-zero"));
182 }
183
184 Ok(value)
185}
186
187fn validate_coin_multiply(value: u32) -> BpiResult<u32> {
188 if matches!(value, 1 | 2) {
189 return Ok(value);
190 }
191
192 Err(BpiError::invalid_parameter(
193 "multiply",
194 "value must be 1 or 2",
195 ))
196}
197
198fn validate_non_blank(field: &'static str, value: &str) -> BpiResult<()> {
199 if value.trim().is_empty() {
200 return Err(BpiError::invalid_parameter(field, "value cannot be blank"));
201 }
202
203 Ok(())
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 #[test]
211 fn article_info_params_serializes_id() -> BpiResult<()> {
212 let params = ArticleInfoParams::new(2)?;
213
214 assert_eq!(params.query_pairs(), vec![("id", "2".to_string())]);
215 Ok(())
216 }
217
218 #[test]
219 fn article_info_params_rejects_zero_id() {
220 let err = ArticleInfoParams::new(0).unwrap_err();
221
222 assert!(matches!(
223 err,
224 BpiError::InvalidParameter { field: "id", .. }
225 ));
226 }
227
228 #[test]
229 fn article_view_params_serializes_default_source() -> BpiResult<()> {
230 let params = ArticleViewParams::new(2)?;
231
232 assert_eq!(
233 params.query_pairs(),
234 vec![
235 ("id", "2".to_string()),
236 ("gaia_source", "main_web".to_string()),
237 ]
238 );
239 Ok(())
240 }
241
242 #[test]
243 fn article_view_params_serializes_custom_source() -> BpiResult<()> {
244 let params = ArticleViewParams::new(2)?.with_gaia_source("article_test")?;
245
246 assert_eq!(
247 params.query_pairs(),
248 vec![
249 ("id", "2".to_string()),
250 ("gaia_source", "article_test".to_string()),
251 ]
252 );
253 Ok(())
254 }
255
256 #[test]
257 fn article_cards_params_serializes_defaults() -> BpiResult<()> {
258 let params = ArticleCardsParams::new("av2,cv1,cv2")?;
259
260 assert_eq!(
261 params.query_pairs(),
262 vec![
263 ("ids", "av2,cv1,cv2".to_string()),
264 ("web_location", "333.1305".to_string()),
265 ]
266 );
267 Ok(())
268 }
269
270 #[test]
271 fn article_cards_params_rejects_blank_ids() {
272 let err = ArticleCardsParams::new(" ").unwrap_err();
273
274 assert!(matches!(
275 err,
276 BpiError::InvalidParameter { field: "ids", .. }
277 ));
278 }
279
280 #[test]
281 fn article_articles_info_params_serializes_id() -> BpiResult<()> {
282 let params = ArticleArticlesInfoParams::new(207146)?;
283
284 assert_eq!(params.query_pairs(), vec![("id", "207146".to_string())]);
285 Ok(())
286 }
287
288 #[test]
289 fn article_like_params_serializes_type() -> BpiResult<()> {
290 let params = ArticleLikeParams::new(2, true)?;
291
292 assert_eq!(
293 params.form_pairs("csrf-token"),
294 vec![
295 ("id", "2".to_string()),
296 ("type", "1".to_string()),
297 ("csrf", "csrf-token".to_string()),
298 ]
299 );
300 Ok(())
301 }
302
303 #[test]
304 fn article_coin_params_rejects_invalid_multiply() {
305 let err = ArticleCoinParams::new(2, 7792521, 3).unwrap_err();
306
307 assert!(matches!(
308 err,
309 BpiError::InvalidParameter {
310 field: "multiply",
311 ..
312 }
313 ));
314 }
315
316 #[test]
317 fn article_favorite_params_rejects_zero_id() {
318 let err = ArticleFavoriteParams::new(0).unwrap_err();
319
320 assert!(matches!(
321 err,
322 BpiError::InvalidParameter { field: "id", .. }
323 ));
324 }
325}