alpaca_data/corporate_actions/
response.rs

1use crate::{Error, transport::pagination::PaginatedResponse};
2
3use super::CorporateActions;
4
5#[derive(Clone, Debug, Default, PartialEq, serde::Deserialize)]
6pub struct ListResponse {
7    pub corporate_actions: CorporateActions,
8    pub next_page_token: Option<String>,
9}
10
11impl PaginatedResponse for ListResponse {
12    fn next_page_token(&self) -> Option<&str> {
13        self.next_page_token.as_deref()
14    }
15
16    fn merge_page(&mut self, next: Self) -> Result<(), Error> {
17        self.corporate_actions.merge(next.corporate_actions);
18        self.next_page_token = next.next_page_token;
19        Ok(())
20    }
21
22    fn clear_next_page_token(&mut self) {
23        self.next_page_token = None;
24    }
25}
26
27#[cfg(test)]
28mod tests {
29    use std::str::FromStr;
30
31    use super::ListResponse;
32    use crate::transport::pagination::PaginatedResponse;
33    use rust_decimal::Decimal;
34
35    #[test]
36    fn list_response_deserializes_official_bucketed_wrapper_shape() {
37        let response: ListResponse = serde_json::from_str(
38            r#"{"corporate_actions":{"cash_dividends":[{"id":"e2b597ca-c2cb-47af-9315-cafb8708766d","symbol":"640CVR031","cusip":"640CVR031","rate":0.055284,"special":false,"foreign":false,"process_date":"2024-08-14","ex_date":"2024-08-07","record_date":"2024-08-07","payable_date":"2024-08-15"}],"name_changes":[{"id":"564620f3-dac4-4558-a227-5c8dd6f4d82e","old_symbol":"007975113","old_cusip":"007975113","new_symbol":"22112H119","new_cusip":"22112H119","process_date":"2024-08-13"}],"contract_adjustments":[{"id":"ca-undocumented","memo":"undocumented family"}],"mystery_bucket":[{"id":"mystery-1","field":"value"}]},"next_page_token":"page-2"}"#,
39        )
40        .expect("response should deserialize");
41
42        assert_eq!(response.corporate_actions.cash_dividends.len(), 1);
43        assert_eq!(
44            response.corporate_actions.cash_dividends[0].rate,
45            Decimal::from_str("0.055284").expect("decimal literal should parse")
46        );
47        assert_eq!(
48            response.corporate_actions.name_changes[0].new_symbol,
49            "22112H119"
50        );
51        assert_eq!(response.corporate_actions.name_changes.len(), 1);
52        assert_eq!(response.corporate_actions.contract_adjustments.len(), 1);
53        assert_eq!(
54            response
55                .corporate_actions
56                .other
57                .get("mystery_bucket")
58                .map(Vec::len),
59            Some(1)
60        );
61        assert_eq!(response.next_page_token.as_deref(), Some("page-2"));
62    }
63
64    #[test]
65    fn list_response_merge_appends_bucketed_pages_and_clears_next_page_token() {
66        let mut first: ListResponse = serde_json::from_str(
67            r#"{"corporate_actions":{"cash_dividends":[{"id":"ca-1","symbol":"AAA","cusip":"111111111","rate":0.1,"special":false,"foreign":false,"process_date":"2024-08-01","ex_date":"2024-08-01"}],"contract_adjustments":[{"id":"undoc-1"}],"mystery_bucket":[{"id":"mystery-1"}]},"next_page_token":"page-2"}"#,
68        )
69        .expect("first response should deserialize");
70        let second: ListResponse = serde_json::from_str(
71            r#"{"corporate_actions":{"cash_dividends":[{"id":"ca-2","symbol":"BBB","cusip":"222222222","rate":0.2,"special":false,"foreign":false,"process_date":"2024-08-02","ex_date":"2024-08-02"}],"name_changes":[{"id":"name-1","old_symbol":"OLD","old_cusip":"333333333","new_symbol":"NEW","new_cusip":"444444444","process_date":"2024-08-02"}],"contract_adjustments":[{"id":"undoc-2"}],"mystery_bucket":[{"id":"mystery-2"}]},"next_page_token":null}"#,
72        )
73        .expect("second response should deserialize");
74
75        first
76            .merge_page(second)
77            .expect("merge should append bucketed corporate action pages");
78        first.clear_next_page_token();
79
80        assert_eq!(first.corporate_actions.cash_dividends.len(), 2);
81        assert_eq!(
82            first.corporate_actions.cash_dividends[1].rate,
83            Decimal::from_str("0.2").expect("decimal literal should parse")
84        );
85        assert_eq!(first.corporate_actions.name_changes.len(), 1);
86        assert_eq!(first.corporate_actions.contract_adjustments.len(), 2);
87        assert_eq!(
88            first
89                .corporate_actions
90                .other
91                .get("mystery_bucket")
92                .map(Vec::len),
93            Some(2)
94        );
95        assert_eq!(first.next_page_token, None);
96    }
97
98    #[test]
99    fn list_response_deserializes_non_dividend_decimal_rate_fields() {
100        let response: ListResponse = serde_json::from_str(
101            r#"{"corporate_actions":{"forward_splits":[{"id":"fs-1","symbol":"ABC","cusip":"000000001","new_rate":3.0,"old_rate":2.0,"process_date":"2024-08-14","ex_date":"2024-08-07","record_date":"2024-08-07","payable_date":"2024-08-15"}],"stock_and_cash_mergers":[{"id":"scm-1","acquirer_symbol":"AAA","acquirer_cusip":"111111111","acquirer_rate":0.75,"acquiree_symbol":"BBB","acquiree_cusip":"222222222","acquiree_rate":1.0,"cash_rate":4.25,"process_date":"2024-08-14","effective_date":"2024-08-15","payable_date":"2024-08-16"}]},"next_page_token":null}"#,
102        )
103        .expect("response should deserialize");
104
105        assert_eq!(
106            response.corporate_actions.forward_splits[0].new_rate,
107            Decimal::from_str("3.0").expect("decimal literal should parse")
108        );
109        assert_eq!(
110            response.corporate_actions.stock_and_cash_mergers[0].cash_rate,
111            Decimal::from_str("4.25").expect("decimal literal should parse")
112        );
113    }
114}