alpaca_data/news/
request.rs

1use crate::Error;
2use crate::common::enums::Sort;
3use crate::common::query::QueryWriter;
4use crate::transport::pagination::PaginatedRequest;
5
6#[derive(Clone, Debug, Default)]
7pub struct ListRequest {
8    pub start: Option<String>,
9    pub end: Option<String>,
10    pub sort: Option<Sort>,
11    pub symbols: Option<Vec<String>>,
12    pub limit: Option<u32>,
13    pub include_content: Option<bool>,
14    pub exclude_contentless: Option<bool>,
15    pub page_token: Option<String>,
16}
17
18impl ListRequest {
19    pub(crate) fn validate(&self) -> Result<(), Error> {
20        validate_limit(self.limit, 1, 50)
21    }
22
23    pub(crate) fn to_query(self) -> Vec<(String, String)> {
24        let mut query = QueryWriter::default();
25        query.push_opt("start", self.start);
26        query.push_opt("end", self.end);
27        query.push_opt("sort", self.sort);
28        if let Some(symbols) = self.symbols {
29            query.push_csv("symbols", symbols);
30        }
31        query.push_opt("limit", self.limit);
32        query.push_opt("include_content", self.include_content);
33        query.push_opt("exclude_contentless", self.exclude_contentless);
34        query.push_opt("page_token", self.page_token);
35        query.finish()
36    }
37}
38
39fn validate_limit(limit: Option<u32>, min: u32, max: u32) -> Result<(), Error> {
40    if let Some(limit) = limit {
41        if !(min..=max).contains(&limit) {
42            return Err(Error::InvalidRequest(format!(
43                "limit must be between {min} and {max}"
44            )));
45        }
46    }
47
48    Ok(())
49}
50
51impl PaginatedRequest for ListRequest {
52    fn with_page_token(&self, page_token: Option<String>) -> Self {
53        let mut next = self.clone();
54        next.page_token = page_token;
55        next
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use super::ListRequest;
62    use crate::{Error, common::enums::Sort};
63
64    #[test]
65    fn list_request_serializes_official_query_words() {
66        let query = ListRequest {
67            start: Some("2026-04-01T00:00:00Z".into()),
68            end: Some("2026-04-04T00:00:00Z".into()),
69            sort: Some(Sort::Desc),
70            symbols: Some(vec!["AAPL".into(), "BTCUSD".into()]),
71            limit: Some(2),
72            include_content: Some(false),
73            exclude_contentless: Some(true),
74            page_token: Some("page-2".into()),
75        }
76        .to_query();
77
78        assert_eq!(
79            query,
80            vec![
81                ("start".to_string(), "2026-04-01T00:00:00Z".to_string()),
82                ("end".to_string(), "2026-04-04T00:00:00Z".to_string()),
83                ("sort".to_string(), "desc".to_string()),
84                ("symbols".to_string(), "AAPL,BTCUSD".to_string()),
85                ("limit".to_string(), "2".to_string()),
86                ("include_content".to_string(), "false".to_string()),
87                ("exclude_contentless".to_string(), "true".to_string()),
88                ("page_token".to_string(), "page-2".to_string()),
89            ]
90        );
91    }
92
93    #[test]
94    fn list_request_rejects_limits_outside_documented_range() {
95        let low = ListRequest {
96            limit: Some(0),
97            ..ListRequest::default()
98        }
99        .validate()
100        .expect_err("limit below one must fail");
101        assert!(matches!(
102            low,
103            Error::InvalidRequest(message) if message.contains("limit") && message.contains("50")
104        ));
105
106        let high = ListRequest {
107            limit: Some(51),
108            ..ListRequest::default()
109        }
110        .validate()
111        .expect_err("limit above fifty must fail");
112        assert!(matches!(
113            high,
114            Error::InvalidRequest(message) if message.contains("limit") && message.contains("50")
115        ));
116    }
117}