alpaca_data/corporate_actions/
request.rs1use std::fmt::{self, Display, Formatter};
2
3use crate::Error;
4use crate::common::enums::Sort;
5use crate::common::query::QueryWriter;
6use crate::transport::pagination::PaginatedRequest;
7
8#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
9pub enum CorporateActionType {
10 #[default]
11 ForwardSplit,
12 ReverseSplit,
13 UnitSplit,
14 StockDividend,
15 CashDividend,
16 SpinOff,
17 CashMerger,
18 StockMerger,
19 StockAndCashMerger,
20 Redemption,
21 NameChange,
22 WorthlessRemoval,
23 RightsDistribution,
24 ContractAdjustment,
25 PartialCall,
26}
27
28impl Display for CorporateActionType {
29 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
30 formatter.write_str(match self {
31 Self::ForwardSplit => "forward_split",
32 Self::ReverseSplit => "reverse_split",
33 Self::UnitSplit => "unit_split",
34 Self::StockDividend => "stock_dividend",
35 Self::CashDividend => "cash_dividend",
36 Self::SpinOff => "spin_off",
37 Self::CashMerger => "cash_merger",
38 Self::StockMerger => "stock_merger",
39 Self::StockAndCashMerger => "stock_and_cash_merger",
40 Self::Redemption => "redemption",
41 Self::NameChange => "name_change",
42 Self::WorthlessRemoval => "worthless_removal",
43 Self::RightsDistribution => "rights_distribution",
44 Self::ContractAdjustment => "contract_adjustment",
45 Self::PartialCall => "partial_call",
46 })
47 }
48}
49
50#[derive(Clone, Debug, Default)]
51pub struct ListRequest {
52 pub symbols: Option<Vec<String>>,
53 pub cusips: Option<Vec<String>>,
54 pub types: Option<Vec<CorporateActionType>>,
55 pub start: Option<String>,
56 pub end: Option<String>,
57 pub ids: Option<Vec<String>>,
58 pub limit: Option<u32>,
59 pub sort: Option<Sort>,
60 pub page_token: Option<String>,
61}
62
63impl ListRequest {
64 pub(crate) fn validate(&self) -> Result<(), Error> {
65 validate_limit(self.limit, 1, 1_000)?;
66
67 if self.ids.is_some()
68 && (self.symbols.is_some()
69 || self.cusips.is_some()
70 || self.types.is_some()
71 || self.start.is_some()
72 || self.end.is_some())
73 {
74 return Err(Error::InvalidRequest(
75 "ids cannot be combined with other corporate actions filters".into(),
76 ));
77 }
78
79 Ok(())
80 }
81
82 pub(crate) fn to_query(self) -> Vec<(String, String)> {
83 let mut query = QueryWriter::default();
84 if let Some(symbols) = self.symbols {
85 query.push_csv("symbols", symbols);
86 }
87 if let Some(cusips) = self.cusips {
88 query.push_csv("cusips", cusips);
89 }
90 if let Some(types) = self.types {
91 query.push_csv("types", types.into_iter().map(|value| value.to_string()));
92 }
93 query.push_opt("start", self.start);
94 query.push_opt("end", self.end);
95 if let Some(ids) = self.ids {
96 query.push_csv("ids", ids);
97 }
98 query.push_opt("limit", self.limit);
99 query.push_opt("sort", self.sort);
100 query.push_opt("page_token", self.page_token);
101 query.finish()
102 }
103}
104
105fn validate_limit(limit: Option<u32>, min: u32, max: u32) -> Result<(), Error> {
106 if let Some(limit) = limit {
107 if !(min..=max).contains(&limit) {
108 return Err(Error::InvalidRequest(format!(
109 "limit must be between {min} and {max}"
110 )));
111 }
112 }
113
114 Ok(())
115}
116
117impl PaginatedRequest for ListRequest {
118 fn with_page_token(&self, page_token: Option<String>) -> Self {
119 let mut next = self.clone();
120 next.page_token = page_token;
121 next
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::{CorporateActionType, ListRequest};
128 use crate::{Error, common::enums::Sort};
129
130 #[test]
131 fn corporate_action_type_serializes_to_official_query_words() {
132 assert_eq!(
133 CorporateActionType::ForwardSplit.to_string(),
134 "forward_split"
135 );
136 assert_eq!(
137 CorporateActionType::ReverseSplit.to_string(),
138 "reverse_split"
139 );
140 assert_eq!(CorporateActionType::UnitSplit.to_string(), "unit_split");
141 assert_eq!(
142 CorporateActionType::StockDividend.to_string(),
143 "stock_dividend"
144 );
145 assert_eq!(
146 CorporateActionType::CashDividend.to_string(),
147 "cash_dividend"
148 );
149 assert_eq!(CorporateActionType::SpinOff.to_string(), "spin_off");
150 assert_eq!(CorporateActionType::CashMerger.to_string(), "cash_merger");
151 assert_eq!(CorporateActionType::StockMerger.to_string(), "stock_merger");
152 assert_eq!(
153 CorporateActionType::StockAndCashMerger.to_string(),
154 "stock_and_cash_merger"
155 );
156 assert_eq!(CorporateActionType::Redemption.to_string(), "redemption");
157 assert_eq!(CorporateActionType::NameChange.to_string(), "name_change");
158 assert_eq!(
159 CorporateActionType::WorthlessRemoval.to_string(),
160 "worthless_removal"
161 );
162 assert_eq!(
163 CorporateActionType::RightsDistribution.to_string(),
164 "rights_distribution"
165 );
166 assert_eq!(
167 CorporateActionType::ContractAdjustment.to_string(),
168 "contract_adjustment"
169 );
170 assert_eq!(CorporateActionType::PartialCall.to_string(), "partial_call");
171 }
172
173 #[test]
174 fn list_request_serializes_official_query_words() {
175 let query = ListRequest {
176 symbols: Some(vec!["AAPL".into(), "TSLA".into()]),
177 cusips: Some(vec!["037833100".into(), "88160R101".into()]),
178 types: Some(vec![
179 CorporateActionType::CashDividend,
180 CorporateActionType::NameChange,
181 ]),
182 start: Some("2024-08-01".into()),
183 end: Some("2024-08-20".into()),
184 ids: Some(vec!["ca-1".into(), "ca-2".into()]),
185 limit: Some(2),
186 sort: Some(Sort::Desc),
187 page_token: Some("page-2".into()),
188 }
189 .to_query();
190
191 assert_eq!(
192 query,
193 vec![
194 ("symbols".to_string(), "AAPL,TSLA".to_string()),
195 ("cusips".to_string(), "037833100,88160R101".to_string()),
196 ("types".to_string(), "cash_dividend,name_change".to_string(),),
197 ("start".to_string(), "2024-08-01".to_string()),
198 ("end".to_string(), "2024-08-20".to_string()),
199 ("ids".to_string(), "ca-1,ca-2".to_string()),
200 ("limit".to_string(), "2".to_string()),
201 ("sort".to_string(), "desc".to_string()),
202 ("page_token".to_string(), "page-2".to_string()),
203 ]
204 );
205 }
206
207 #[test]
208 fn list_request_rejects_limits_outside_documented_range() {
209 let low = ListRequest {
210 limit: Some(0),
211 ..ListRequest::default()
212 }
213 .validate()
214 .expect_err("limit below one must fail");
215 assert!(matches!(
216 low,
217 Error::InvalidRequest(message)
218 if message.contains("limit") && message.contains("1000")
219 ));
220
221 let high = ListRequest {
222 limit: Some(1001),
223 ..ListRequest::default()
224 }
225 .validate()
226 .expect_err("limit above one thousand must fail");
227 assert!(matches!(
228 high,
229 Error::InvalidRequest(message)
230 if message.contains("limit") && message.contains("1000")
231 ));
232 }
233
234 #[test]
235 fn ids_remain_mutually_exclusive_with_other_filters() {
236 let error = ListRequest {
237 symbols: Some(vec!["AAPL".into()]),
238 ids: Some(vec!["ca-1".into()]),
239 ..ListRequest::default()
240 }
241 .validate()
242 .expect_err("ids plus symbols must fail");
243 assert!(matches!(
244 error,
245 Error::InvalidRequest(message)
246 if message.contains("ids") && message.contains("filters")
247 ));
248
249 let error = ListRequest {
250 cusips: Some(vec!["037833100".into()]),
251 ids: Some(vec!["ca-1".into()]),
252 ..ListRequest::default()
253 }
254 .validate()
255 .expect_err("ids plus cusips must fail");
256 assert!(matches!(
257 error,
258 Error::InvalidRequest(message)
259 if message.contains("ids") && message.contains("filters")
260 ));
261 }
262}