marketstack/api/
intraday.rs

1//! Implemented endpoints for `intraday`, `intraday/latest` and `intraday/[date]`
2//!
3//! # Example
4//!
5//! ```rust,no_run
6//! use marketstack::api::common::Interval;
7//! use marketstack::api::{self, Query};
8//! use marketstack::api::intraday::Intraday;
9//! use marketstack::{Marketstack, IntradayData};
10//!
11//! // Create an insecure client.
12//! let client = Marketstack::new_insecure("api.marketstack.com", "private-token").unwrap();
13//!
14//! // Create the `intraday` endpoint.
15//! let endpoint = Intraday::builder()
16//!     .symbol("AAPL")
17//!     .interval(Interval::ThirtyMinutes)
18//!     .build()
19//!     .unwrap();
20//!
21//! // Call the endpoint. The return type decides how to represent the value.
22//! let intraday_data: IntradayData = endpoint.query(&client).unwrap();
23//!
24//! // Data has been deserialized for you into `IntradayData`
25//! assert_eq!(intraday_data.data.len(), 100);
26//! assert!(intraday_data.data.iter().all(|intraday| intraday.symbol == "AAPL"));
27//! ```
28//!
29//! Similar to the `eod` endpoint, `intraday` supports the same endpoint features.
30//!
31//! # Using Intraday Features
32//!
33//! ```rust,no_run
34//! use chrono::NaiveDate;
35//!
36//! use marketstack::api::common::Interval;
37//! use marketstack::api::{self, Query};
38//! use marketstack::api::intraday::Intraday;
39//! use marketstack::{Marketstack, IntradayData};
40//!
41//! let client = Marketstack::new_insecure("api.marketstack.com", "private-token").unwrap();
42//!
43//! // Create endpoint for `intraday/latest`.
44//! let endpoint = Intraday::builder().symbol("AAPL").latest(true).build().unwrap();
45//!
46//! // OR create endpoint for `intraday/[date]`.
47//! let endpoint = Intraday::builder()
48//!     .symbol("AAPL")
49//!     .date(NaiveDate::from_ymd(2019, 1, 1))
50//!     .interval(Interval::ThirtyMinutes)
51//!     .build()
52//!     .unwrap();
53//!
54//! // Call the endpoint. The return type decides how to represent the value.
55//! let intraday_data: IntradayData = endpoint.query(&client).unwrap();
56//!
57//! // Data has been deserialized for you into `IntradayData`
58//! assert_eq!(intraday_data.data.len(), 14);  // 14 30-minute trading intervals in a day
59//! ```
60
61use std::collections::BTreeSet;
62
63use chrono::NaiveDate;
64use derive_builder::Builder;
65
66use crate::api::common::{Interval, SortOrder};
67use crate::api::paged::PaginationError;
68use crate::api::{endpoint_prelude::*, ApiError};
69
70/// Query for `intraday` endpoint
71#[derive(Debug, Clone, Builder)]
72#[builder(setter(strip_option), build_fn(validate = "Self::validate"))]
73pub struct Intraday<'a> {
74    /// Search for eod for a symbol.
75    #[builder(setter(name = "_symbols"), default)]
76    symbols: BTreeSet<Cow<'a, str>>,
77    /// Exchange to filer symbol by.
78    #[builder(setter(into), default)]
79    exchange: Option<Cow<'a, str>>,
80    /// Preferred data interval.
81    #[builder(default)]
82    interval: Option<Interval>,
83    /// The sort order for the return results.
84    #[builder(default)]
85    sort: Option<SortOrder>,
86    /// Date to query EOD data from.
87    #[builder(default)]
88    date_from: Option<NaiveDate>,
89    /// Date to query EOD date to.
90    #[builder(default)]
91    date_to: Option<NaiveDate>,
92    /// Pagination limit for API request.
93    #[builder(setter(name = "_limit"), default)]
94    limit: Option<PageLimit>,
95    /// Pagination offset value for API request.
96    #[builder(default)]
97    offset: Option<u64>,
98    /// Used when desired endpoint is `intraday/latest`
99    #[builder(default)]
100    latest: Option<bool>,
101    /// Used when desired endpoint is `intraday/[date]`
102    #[builder(default)]
103    date: Option<NaiveDate>,
104}
105
106impl<'a> Intraday<'a> {
107    /// Create a builder for the endpoint.
108    pub fn builder() -> IntradayBuilder<'a> {
109        IntradayBuilder::default()
110    }
111}
112
113impl<'a> IntradayBuilder<'a> {
114    /// Search the given symbol.
115    ///
116    /// This provides sane defaults for the user to call symbol()
117    /// on the builder without needing to wrap his symbol in a
118    /// BTreeSet beforehand.
119    pub fn symbol(&mut self, symbol: &'a str) -> &mut Self {
120        self.symbols
121            .get_or_insert_with(BTreeSet::new)
122            .insert(symbol.into());
123        self
124    }
125
126    /// Search the given symbols.
127    pub fn symbols<I, V>(&mut self, iter: I) -> &mut Self
128    where
129        I: Iterator<Item = V>,
130        V: Into<Cow<'a, str>>,
131    {
132        self.symbols
133            .get_or_insert_with(BTreeSet::new)
134            .extend(iter.map(|v| v.into()));
135        self
136    }
137
138    /// Limit the number of results returned.
139    pub fn limit(&mut self, limit: u16) -> Result<&mut Self, ApiError<PaginationError>> {
140        let new = self;
141        new.limit = Some(Some(PageLimit::new(limit)?));
142        Ok(new)
143    }
144
145    /// Check that `Intraday` contains valid endpoint combinations.
146    fn validate(&self) -> Result<(), String> {
147        if self.date.is_some() && self.latest.is_some() {
148            Err("Cannot use both `date` and `latest`".into())
149        } else {
150            Ok(())
151        }
152    }
153}
154
155impl<'a> Endpoint for Intraday<'a> {
156    fn method(&self) -> Method {
157        Method::GET
158    }
159
160    fn endpoint(&self) -> Cow<'static, str> {
161        if self.latest.is_some() {
162            "intraday/latest".into()
163        } else if self.date.is_some() {
164            // Panics on invalid date -> irrecoverable and illegal to proceed
165            format!("intraday/{}", self.date.unwrap()).into()
166        } else {
167            "intraday".into()
168        }
169    }
170
171    fn parameters(&self) -> QueryParams {
172        let mut params = QueryParams::default();
173
174        params
175            .extend(self.symbols.iter().map(|value| ("symbols", value)))
176            .push_opt("exchange", self.exchange.as_ref())
177            .push_opt("interval", self.interval.clone())
178            .push_opt("sort", self.sort)
179            .push_opt("date_from", self.date_from)
180            .push_opt("date_to", self.date_to)
181            .push_opt("limit", self.limit.clone())
182            .push_opt("offset", self.offset);
183
184        params
185    }
186}
187
188#[cfg(test)]
189mod tests {
190
191    use chrono::NaiveDate;
192
193    use crate::api::common::{Interval, SortOrder};
194    use crate::api::intraday::Intraday;
195    use crate::api::{self, Query};
196    use crate::test::client::{ExpectedUrl, SingleTestClient};
197
198    #[test]
199    fn intraday_defaults_are_sufficient() {
200        Intraday::builder().build().unwrap();
201    }
202
203    #[test]
204    fn intraday_endpoint() {
205        let endpoint = ExpectedUrl::builder().endpoint("intraday").build().unwrap();
206        let client = SingleTestClient::new_raw(endpoint, "");
207
208        let endpoint = Intraday::builder().build().unwrap();
209        api::ignore(endpoint).query(&client).unwrap();
210    }
211
212    #[test]
213    fn intraday_symbol() {
214        let endpoint = ExpectedUrl::builder()
215            .endpoint("intraday")
216            .add_query_params(&[("symbols", "AAPL")])
217            .build()
218            .unwrap();
219        let client = SingleTestClient::new_raw(endpoint, "");
220
221        let endpoint = Intraday::builder().symbol("AAPL").build().unwrap();
222        api::ignore(endpoint).query(&client).unwrap();
223    }
224
225    #[test]
226    fn intraday_symbols() {
227        let endpoint = ExpectedUrl::builder()
228            .endpoint("intraday")
229            .add_query_params(&[("symbols", "AAPL"), ("symbols", "MSFT")])
230            .build()
231            .unwrap();
232        let client = SingleTestClient::new_raw(endpoint, "");
233
234        let endpoint = Intraday::builder()
235            .symbols(["AAPL", "MSFT"].iter().copied())
236            .build()
237            .unwrap();
238        api::ignore(endpoint).query(&client).unwrap();
239    }
240
241    #[test]
242    fn intraday_exchange() {
243        let endpoint = ExpectedUrl::builder()
244            .endpoint("intraday")
245            .add_query_params(&[("exchange", "NYSE")])
246            .build()
247            .unwrap();
248        let client = SingleTestClient::new_raw(endpoint, "");
249
250        let endpoint = Intraday::builder().exchange("NYSE").build().unwrap();
251        api::ignore(endpoint).query(&client).unwrap();
252    }
253
254    #[test]
255    fn intraday_interval() {
256        let endpoint = ExpectedUrl::builder()
257            .endpoint("intraday")
258            .add_query_params(&[("interval", "5min")])
259            .build()
260            .unwrap();
261        let client = SingleTestClient::new_raw(endpoint, "");
262
263        let endpoint = Intraday::builder()
264            .interval(Interval::FiveMinutes)
265            .build()
266            .unwrap();
267        api::ignore(endpoint).query(&client).unwrap();
268    }
269
270    #[test]
271    fn intraday_sort() {
272        let endpoint = ExpectedUrl::builder()
273            .endpoint("intraday")
274            .add_query_params(&[("sort", "ASC")])
275            .build()
276            .unwrap();
277        let client = SingleTestClient::new_raw(endpoint, "");
278
279        let endpoint = Intraday::builder()
280            .sort(SortOrder::Ascending)
281            .build()
282            .unwrap();
283        api::ignore(endpoint).query(&client).unwrap();
284    }
285
286    #[test]
287    fn intraday_date_from_and_to() {
288        let endpoint = ExpectedUrl::builder()
289            .endpoint("intraday")
290            .add_query_params(&[("date_from", "2019-01-01"), ("date_to", "2019-01-02")])
291            .build()
292            .unwrap();
293        let client = SingleTestClient::new_raw(endpoint, "");
294
295        let endpoint = Intraday::builder()
296            .date_from(NaiveDate::from_ymd_opt(2019, 1, 1).unwrap())
297            .date_to(NaiveDate::from_ymd_opt(2019, 1, 2).unwrap())
298            .build()
299            .unwrap();
300        api::ignore(endpoint).query(&client).unwrap();
301    }
302
303    #[test]
304    fn intraday_limit_and_offset() {
305        let endpoint = ExpectedUrl::builder()
306            .endpoint("intraday")
307            .add_query_params(&[("limit", "5"), ("offset", "3")])
308            .build()
309            .unwrap();
310        let client = SingleTestClient::new_raw(endpoint, "");
311
312        let endpoint = Intraday::builder()
313            .limit(5)
314            .unwrap()
315            .offset(3)
316            .build()
317            .unwrap();
318        api::ignore(endpoint).query(&client).unwrap();
319    }
320
321    #[test]
322    fn intraday_over_limit() {
323        assert!(Intraday::builder().limit(5000).is_err());
324    }
325
326    #[test]
327    fn intraday_latest_defaults_are_sufficient() {
328        Intraday::builder().latest(true).build().unwrap();
329    }
330
331    #[test]
332    fn intraday_latest_endpoint() {
333        let endpoint = ExpectedUrl::builder()
334            .endpoint("intraday/latest")
335            .build()
336            .unwrap();
337        let client = SingleTestClient::new_raw(endpoint, "");
338
339        let endpoint = Intraday::builder().latest(true).build().unwrap();
340        api::ignore(endpoint).query(&client).unwrap();
341    }
342
343    #[test]
344    fn intraday_date_defaults_are_sufficient() {
345        Intraday::builder()
346            .date(NaiveDate::from_ymd_opt(2019, 1, 1).unwrap())
347            .build()
348            .unwrap();
349    }
350
351    #[test]
352    fn intraday_date_endpoint() {
353        let endpoint = ExpectedUrl::builder()
354            .endpoint("intraday/2019-01-01")
355            .build()
356            .unwrap();
357        let client = SingleTestClient::new_raw(endpoint, "");
358
359        let endpoint = Intraday::builder()
360            .date(NaiveDate::from_ymd_opt(2019, 1, 1).unwrap())
361            .build()
362            .unwrap();
363        api::ignore(endpoint).query(&client).unwrap();
364    }
365}