marketstack/api/
tickers.rs

1//! Implemented for `tickers` and associated endpoints.
2
3use std::borrow::Cow;
4
5use derive_builder::Builder;
6
7use crate::api::dividends::Dividends;
8use crate::api::eod::Eod;
9use crate::api::paged::PaginationError;
10use crate::api::splits::Splits;
11use crate::api::{endpoint_prelude::*, ApiError};
12
13/// Base for `tickers`.
14#[derive(Debug, Builder, Clone)]
15#[builder(setter(strip_option), build_fn(validate = "Self::validate"))]
16pub struct Tickers<'a> {
17    /// Ticker symbol.
18    #[builder(setter(into), default)]
19    ticker: Option<Cow<'a, str>>,
20    /// To filter your results based on a specific stock exchange, use this parameter to specify the MIC identification of a stock exchange. Example: `XNAS`
21    #[builder(setter(into), default)]
22    exchange: Option<Cow<'a, str>>,
23    /// Use this parameter to search stock tickers by name or ticker symbol.
24    #[builder(setter(into), default)]
25    search: Option<Cow<'a, str>>,
26    /// Pagination limit for API request.
27    #[builder(setter(name = "_limit"), default)]
28    limit: Option<PageLimit>,
29    /// Pagination offset value for API request.
30    #[builder(default)]
31    offset: Option<u64>,
32    /// `Eod` struct being built, and held by the `Tickers` struct.
33    #[builder(setter(into), default)]
34    eod: Option<Eod<'a>>,
35    /// `Splits` struct being built, and held by the `Tickers` struct.
36    #[builder(setter(into), default)]
37    splits: Option<Splits<'a>>,
38    /// `Dividends` struct being built, and held by the `Tickers` struct.
39    #[builder(setter(into), default)]
40    dividends: Option<Dividends<'a>>,
41}
42
43impl<'a> Tickers<'a> {
44    /// Create a builder for the endpoint.
45    pub fn builder() -> TickersBuilder<'a> {
46        TickersBuilder::default()
47    }
48}
49
50impl<'a> Endpoint for Tickers<'a> {
51    fn method(&self) -> Method {
52        Method::GET
53    }
54
55    fn endpoint(&self) -> Cow<'static, str> {
56        let mut endpoint = "tickers".to_owned();
57        if let Some(ticker) = &self.ticker {
58            endpoint.push_str(&format!("/{}", ticker));
59
60            // NOTE: validator will ensure only one can be active.
61            if let Some(eod) = &self.eod {
62                endpoint.push_str(&format!("/{}", eod.endpoint().as_ref()));
63            }
64            if let Some(splits) = &self.splits {
65                endpoint.push_str(&format!("/{}", splits.endpoint().as_ref()));
66            }
67            if let Some(dividends) = &self.dividends {
68                endpoint.push_str(&format!("/{}", dividends.endpoint().as_ref()));
69            }
70        }
71
72        endpoint.into()
73    }
74
75    fn parameters(&self) -> QueryParams {
76        let mut params = QueryParams::default();
77
78        // NOTE: Not the most ergonomic way I want to go about this, but its okay for now since
79        // only one "extension" endpoint like `eod` or `splits` can be active per `tickers`
80        // endpoint query to Marketstack.
81        if let Some(eod) = &self.eod {
82            params = eod.parameters().clone();
83        }
84        if let Some(splits) = &self.splits {
85            params = splits.parameters().clone();
86        }
87        if let Some(dividends) = &self.dividends {
88            params = dividends.parameters().clone();
89        }
90
91        // Push params from the `tickers` endpoint.
92        params
93            .push_opt("exchange", self.exchange.as_ref())
94            .push_opt("search", self.search.as_ref())
95            .push_opt("limit", self.limit.clone())
96            .push_opt("offset", self.offset);
97
98        params
99    }
100}
101
102impl<'a> TickersBuilder<'a> {
103    /// Limit the number of results returned.
104    pub fn limit(&mut self, limit: u16) -> Result<&mut Self, ApiError<PaginationError>> {
105        let new = self;
106        new.limit = Some(Some(PageLimit::new(limit)?));
107        Ok(new)
108    }
109
110    /// Check that `Tickers` contains valid endpoint combinations.
111    fn validate(&self) -> Result<(), String> {
112        let active_fields = [
113            self.eod.is_some(),
114            self.splits.is_some(),
115            self.dividends.is_some(),
116        ];
117        let count = active_fields.iter().filter(|x| **x).count();
118        if count > 1 {
119            Err("Invalid combinations of `eod`, `splits` or `dividends`".into())
120        } else {
121            Ok(())
122        }
123    }
124}
125
126#[cfg(test)]
127mod tests {
128
129    use chrono::NaiveDate;
130
131    use crate::api::common::SortOrder;
132    use crate::api::dividends::Dividends;
133    use crate::api::eod::Eod;
134    use crate::api::splits::Splits;
135    use crate::api::tickers::Tickers;
136    use crate::api::{self, Query};
137    use crate::test::client::{ExpectedUrl, SingleTestClient};
138
139    #[test]
140    fn tickers_defaults_are_sufficient() {
141        Tickers::builder().build().unwrap();
142    }
143
144    #[test]
145    fn tickers_ticker() {
146        let endpoint = ExpectedUrl::builder()
147            .endpoint("tickers/AAPL")
148            .build()
149            .unwrap();
150        let client = SingleTestClient::new_raw(endpoint, "");
151
152        let endpoint = Tickers::builder().ticker("AAPL").build().unwrap();
153        api::ignore(endpoint).query(&client).unwrap();
154    }
155
156    #[test]
157    fn tickers_eod_endpoint() {
158        let endpoint = ExpectedUrl::builder()
159            .endpoint("tickers/AAPL/eod")
160            .build()
161            .unwrap();
162        let client = SingleTestClient::new_raw(endpoint, "");
163
164        let endpoint = Tickers::builder()
165            .ticker("AAPL")
166            .eod(Eod::builder().build().unwrap())
167            .build()
168            .unwrap();
169        api::ignore(endpoint).query(&client).unwrap();
170    }
171
172    #[test]
173    fn tickers_eod_latest_endpoint() {
174        let endpoint = ExpectedUrl::builder()
175            .endpoint("tickers/AAPL/eod/latest")
176            .build()
177            .unwrap();
178        let client = SingleTestClient::new_raw(endpoint, "");
179
180        let endpoint = Tickers::builder()
181            .ticker("AAPL")
182            .eod(Eod::builder().latest(true).build().unwrap())
183            .build()
184            .unwrap();
185        api::ignore(endpoint).query(&client).unwrap();
186    }
187
188    #[test]
189    fn tickers_eod_date_endpoint() {
190        let endpoint = ExpectedUrl::builder()
191            .endpoint("tickers/AAPL/eod/2023-09-27")
192            .build()
193            .unwrap();
194        let client = SingleTestClient::new_raw(endpoint, "");
195
196        let endpoint = Tickers::builder()
197            .ticker("AAPL")
198            .eod(
199                Eod::builder()
200                    .date(NaiveDate::from_ymd_opt(2023, 9, 27).unwrap())
201                    .build()
202                    .unwrap(),
203            )
204            .build()
205            .unwrap();
206        api::ignore(endpoint).query(&client).unwrap();
207    }
208
209    #[test]
210    fn tickers_eod_params() {
211        let endpoint = ExpectedUrl::builder()
212            .endpoint("tickers/AAPL/eod")
213            .add_query_params(&[("sort", "ASC")])
214            .build()
215            .unwrap();
216        let client = SingleTestClient::new_raw(endpoint, "");
217
218        let endpoint = Tickers::builder()
219            .ticker("AAPL")
220            .eod(Eod::builder().sort(SortOrder::Ascending).build().unwrap())
221            .build()
222            .unwrap();
223        api::ignore(endpoint).query(&client).unwrap();
224    }
225
226    #[test]
227    fn tickers_splits() {
228        let endpoint = ExpectedUrl::builder()
229            .endpoint("tickers/AAPL/splits")
230            .add_query_params(&[("date_from", "2023-09-27"), ("date_to", "2023-09-30")])
231            .build()
232            .unwrap();
233        let client = SingleTestClient::new_raw(endpoint, "");
234
235        let endpoint = Tickers::builder()
236            .ticker("AAPL")
237            .splits(
238                Splits::builder()
239                    .date_from(NaiveDate::from_ymd_opt(2023, 9, 27).unwrap())
240                    .date_to(NaiveDate::from_ymd_opt(2023, 9, 30).unwrap())
241                    .build()
242                    .unwrap(),
243            )
244            .build()
245            .unwrap();
246        api::ignore(endpoint).query(&client).unwrap();
247    }
248
249    #[test]
250    fn tickers_dividends() {
251        let endpoint = ExpectedUrl::builder()
252            .endpoint("tickers/AAPL/dividends")
253            .build()
254            .unwrap();
255        let client = SingleTestClient::new_raw(endpoint, "");
256
257        let endpoint = Tickers::builder()
258            .ticker("AAPL")
259            .dividends(Dividends::builder().build().unwrap())
260            .build()
261            .unwrap();
262        api::ignore(endpoint).query(&client).unwrap();
263    }
264
265    #[test]
266    fn tickers_validator() {
267        let endpoint = Tickers::builder()
268            .eod(Eod::builder().build().unwrap())
269            .splits(Splits::builder().build().unwrap())
270            .build();
271        assert!(endpoint.is_err());
272        assert!(endpoint.err().unwrap().to_string().contains("Invalid"));
273    }
274}