alpaca_data/news/
request.rs1use 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}