Skip to main content

bpi_rs/article/
params.rs

1use crate::{BpiError, BpiResult};
2
3/// Parameters for `/x/article/viewinfo`.
4#[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/// Parameters for `/x/article/view`.
22#[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/// Parameters for `/x/article/cards`.
52#[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/// Parameters for `/x/article/list/web/articles`.
84#[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/// Parameters for `/x/article/like`.
102#[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/// Parameters for `/x/web-interface/coin/add` article coin operations.
126#[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/// Parameters for article favorite add/remove operations.
154#[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}