From 401a7d99b12c4c14df99e917baa58b6c1a7b1430 Mon Sep 17 00:00:00 2001 From: Reuben Wong Date: Fri, 20 Oct 2023 23:15:50 +0800 Subject: [PATCH] Dividends Endpoint (#17) * add types for dividends data * implement and unit test dividends endpoint * add integration tests for dividends endpoint --- Cargo.toml | 2 +- src/api.rs | 1 + src/api/dividends.rs | 235 +++++++++++++++++++++++++++++++++++++++++++ src/api/splits.rs | 4 +- src/marketstack.rs | 2 + src/types.rs | 69 ++++++++++++- tests/dividends.rs | 66 ++++++++++++ 7 files changed, 375 insertions(+), 4 deletions(-) create mode 100644 src/api/dividends.rs create mode 100644 tests/dividends.rs diff --git a/Cargo.toml b/Cargo.toml index 7c1221c..fc8dfcc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "marketstack" -version = "0.0.4" +version = "0.0.5" edition = "2021" license = "MIT" description = "Rust bindings for Marketstack REST API" diff --git a/src/api.rs b/src/api.rs index 457781c..48fd279 100644 --- a/src/api.rs +++ b/src/api.rs @@ -55,6 +55,7 @@ mod raw; pub mod endpoint_prelude; pub mod common; +pub mod dividends; pub mod eod; pub mod paged; pub mod splits; diff --git a/src/api/dividends.rs b/src/api/dividends.rs new file mode 100644 index 0000000..f834f3e --- /dev/null +++ b/src/api/dividends.rs @@ -0,0 +1,235 @@ +//! Implementation of the `dividends` API endpoint. + +use std::collections::BTreeSet; + +use chrono::NaiveDate; +use derive_builder::Builder; + +use crate::api::common::SortOrder; +use crate::api::paged::PaginationError; +use crate::api::{endpoint_prelude::*, ApiError}; + +/// Query for `dividends`. +#[derive(Debug, Builder, Clone)] +#[builder(setter(strip_option))] +pub struct Dividends<'a> { + /// Search for `dividends` for a symbol. + #[builder(setter(name = "_symbols"), default)] + symbols: BTreeSet>, + /// The sort order for the return results. + #[builder(default)] + sort: Option, + /// Date to query EOD data from. + #[builder(default)] + date_from: Option, + /// Date to query EOD date to. + #[builder(default)] + date_to: Option, + /// Pagination limit for API request. + #[builder(setter(name = "_limit"), default)] + limit: Option, + /// Pagination offset value for API request. + #[builder(default)] + offset: Option, +} + +impl<'a> Dividends<'a> { + /// Create a builder for this endpoint. + pub fn builder() -> DividendsBuilder<'a> { + DividendsBuilder::default() + } +} + +impl<'a> DividendsBuilder<'a> { + /// Search the given symbol. + /// + /// This provides sane defaults for the user to call symbol() + /// on the builder without needing to wrap his symbol in a + /// BTreeSet beforehand. + pub fn symbol(&mut self, symbol: &'a str) -> &mut Self { + self.symbols + .get_or_insert_with(BTreeSet::new) + .insert(symbol.into()); + self + } + + /// Search the given symbols. + pub fn symbols(&mut self, iter: I) -> &mut Self + where + I: Iterator, + V: Into>, + { + self.symbols + .get_or_insert_with(BTreeSet::new) + .extend(iter.map(|v| v.into())); + self + } + + /// Limit the number of results returned. + pub fn limit(&mut self, limit: u16) -> Result<&mut Self, ApiError> { + let new = self; + new.limit = Some(Some(PageLimit::new(limit)?)); + Ok(new) + } +} + +impl<'a> Endpoint for Dividends<'a> { + fn method(&self) -> Method { + Method::GET + } + + fn endpoint(&self) -> Cow<'static, str> { + "dividends".into() + } + + fn parameters(&self) -> QueryParams { + let mut params = QueryParams::default(); + + params + .extend(self.symbols.iter().map(|value| ("symbols", value))) + .push_opt("sort", self.sort) + .push_opt("date_from", self.date_from) + .push_opt("date_to", self.date_to) + .push_opt("limit", self.limit.clone()) + .push_opt("offset", self.offset); + + params + } +} + +#[cfg(test)] +mod tests { + + use chrono::NaiveDate; + + use crate::api::common::SortOrder; + use crate::api::dividends::Dividends; + use crate::api::{self, Query}; + use crate::test::client::{ExpectedUrl, SingleTestClient}; + + #[test] + fn dividends_defaults_are_sufficient() { + Dividends::builder().build().unwrap(); + } + + #[test] + fn dividends_endpoint() { + let endpoint = ExpectedUrl::builder() + .endpoint("dividends") + .build() + .unwrap(); + let client = SingleTestClient::new_raw(endpoint, ""); + + let endpoint = Dividends::builder().build().unwrap(); + api::ignore(endpoint).query(&client).unwrap(); + } + + #[test] + fn dividends_symbol() { + let endpoint = ExpectedUrl::builder() + .endpoint("dividends") + .add_query_params(&[("symbols", "AAPL")]) + .build() + .unwrap(); + let client = SingleTestClient::new_raw(endpoint, ""); + + let endpoint = Dividends::builder().symbol("AAPL").build().unwrap(); + api::ignore(endpoint).query(&client).unwrap(); + } + + #[test] + fn dividends_symbols() { + let endpoint = ExpectedUrl::builder() + .endpoint("dividends") + .add_query_params(&[("symbols", "AAPL"), ("symbols", "GOOG")]) + .build() + .unwrap(); + let client = SingleTestClient::new_raw(endpoint, ""); + + let endpoint = Dividends::builder() + .symbol("AAPL") + .symbols(["AAPL", "GOOG"].iter().copied()) + .build() + .unwrap(); + api::ignore(endpoint).query(&client).unwrap(); + } + + #[test] + fn dividends_sort() { + let endpoint = ExpectedUrl::builder() + .endpoint("dividends") + .add_query_params(&[("sort", "ASC")]) + .build() + .unwrap(); + let client = SingleTestClient::new_raw(endpoint, ""); + + let endpoint = Dividends::builder() + .sort(SortOrder::Ascending) + .build() + .unwrap(); + api::ignore(endpoint).query(&client).unwrap(); + } + + #[test] + fn dividends_date_from() { + let endpoint = ExpectedUrl::builder() + .endpoint("dividends") + .add_query_params(&[("date_from", "2020-01-01")]) + .build() + .unwrap(); + let client = SingleTestClient::new_raw(endpoint, ""); + + let endpoint = Dividends::builder() + .date_from(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap()) + .build() + .unwrap(); + api::ignore(endpoint).query(&client).unwrap(); + } + + #[test] + fn dividends_date_to() { + let endpoint = ExpectedUrl::builder() + .endpoint("dividends") + .add_query_params(&[("date_to", "2020-01-01")]) + .build() + .unwrap(); + let client = SingleTestClient::new_raw(endpoint, ""); + + let endpoint = Dividends::builder() + .date_to(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap()) + .build() + .unwrap(); + api::ignore(endpoint).query(&client).unwrap(); + } + + #[test] + fn dividends_limit() { + let endpoint = ExpectedUrl::builder() + .endpoint("dividends") + .add_query_params(&[("limit", "50")]) + .build() + .unwrap(); + let client = SingleTestClient::new_raw(endpoint, ""); + + let endpoint = Dividends::builder().limit(50).unwrap().build().unwrap(); + api::ignore(endpoint).query(&client).unwrap(); + } + + #[test] + fn dividends_over_limit() { + assert!(Dividends::builder().limit(9999).is_err()); + } + + #[test] + fn dividends_offset() { + let endpoint = ExpectedUrl::builder() + .endpoint("dividends") + .add_query_params(&[("offset", "2")]) + .build() + .unwrap(); + let client = SingleTestClient::new_raw(endpoint, ""); + + let endpoint = Dividends::builder().offset(2).build().unwrap(); + api::ignore(endpoint).query(&client).unwrap(); + } +} diff --git a/src/api/splits.rs b/src/api/splits.rs index d434f68..dc48183 100644 --- a/src/api/splits.rs +++ b/src/api/splits.rs @@ -1,4 +1,4 @@ -//! Implemented endpoints for `splits` +//! Implementation of the `splits` API endpoint. use std::collections::BTreeSet; @@ -34,7 +34,7 @@ pub struct Splits<'a> { } impl<'a> Splits<'a> { - /// Create a bulder for this endpoint. + /// Create a builder for this endpoint. pub fn builder() -> SplitsBuilder<'a> { SplitsBuilder::default() } diff --git a/src/marketstack.rs b/src/marketstack.rs index 580ecd5..7feb557 100644 --- a/src/marketstack.rs +++ b/src/marketstack.rs @@ -155,6 +155,8 @@ impl Marketstack { call().map_err(api::ApiError::client) } } + +/// Builder pattern implementation for Marketstack and AsyncMarketstack. pub struct MarketstackBuilder { protocol: &'static str, host: String, diff --git a/src/types.rs b/src/types.rs index ec096dc..e49a94b 100644 --- a/src/types.rs +++ b/src/types.rs @@ -81,11 +81,31 @@ pub struct SplitsData { pub data: Vec, } +/// Rust representation of single data item from Marketstack `dividends` response. +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct DividendsDataItem { + /// Exact date/time the given data was collected in ISO-8601 format. + pub date: NaiveDate, + /// Dividend for that symbol on the date. + pub dividend: f64, + /// Stock ticker symbol of the current data object. + pub symbol: String, +} + +/// Rust representation of the JSON response from `dividends` marketstack endpoint. +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct DividendsData { + /// Corresponds to pagination entry from JSON response from marketstack. + pub pagination: PaginationInfo, + /// Corresponds to data entry from JSON response from marketstack. + pub data: Vec, +} + #[cfg(test)] mod tests { use chrono::NaiveDate; - use super::{EodData, SplitsData}; + use crate::{DividendsData, EodData, SplitsData}; #[test] fn test_deserialize_eod() { @@ -176,4 +196,51 @@ mod tests { ); assert_eq!(splits_data.data[4].symbol, "AAPL"); } + + #[test] + fn test_deserialize_dividends() { + let json_data = r#"{ + "pagination": { + "limit": 5, + "offset": 0, + "count": 5, + "total": 68 + }, + "data": [ + { + "date": "2023-08-11", + "dividend": 0.24, + "symbol": "AAPL" + }, + { + "date": "2023-05-12", + "dividend": 0.24, + "symbol": "AAPL" + }, + { + "date": "2023-02-10", + "dividend": 0.23, + "symbol": "AAPL" + }, + { + "date": "2022-12-23", + "dividend": 0.17, + "symbol": "AAPL" + }, + { + "date": "2022-11-04", + "dividend": 0.23, + "symbol": "AAPL" + } + ] + }"#; + + let dividends_data: DividendsData = serde_json::from_str(json_data).unwrap(); + assert_eq!(dividends_data.pagination.limit, 5); + assert_eq!(dividends_data.data[0].dividend, 0.24); + assert_eq!( + dividends_data.data[0].date, + NaiveDate::from_ymd_opt(2023, 8, 11).unwrap() + ); + } } diff --git a/tests/dividends.rs b/tests/dividends.rs new file mode 100644 index 0000000..67ba474 --- /dev/null +++ b/tests/dividends.rs @@ -0,0 +1,66 @@ +use chrono::NaiveDate; + +use marketstack::api::{dividends, AsyncQuery, Query}; +use marketstack::{AsyncMarketstack, DividendsData, Marketstack}; + +mod setup; + +#[test] +#[ignore] +fn test_dividends() { + let api_key = setup::setup_key(); + let client = Marketstack::new_insecure("api.marketstack.com", api_key).unwrap(); + + let endpoint = dividends::Dividends::builder() + .symbol("AAPL") + .limit(5) + .unwrap() + .build() + .unwrap(); + let eod_result: DividendsData = endpoint.query(&client).unwrap(); + + assert_eq!(eod_result.pagination.limit, 5); + assert_eq!(eod_result.pagination.offset, 0); + + assert_eq!(eod_result.data.len(), 5); +} + +#[tokio::test] +#[ignore] +async fn test_async_dividends() { + let api_key = setup::setup_key(); + let client = AsyncMarketstack::new_insecure("api.marketstack.com", api_key) + .await + .unwrap(); + + let endpoint = dividends::Dividends::builder() + .limit(3) + .unwrap() + .symbol("AAPL") + .build() + .unwrap(); + let eod_result: DividendsData = endpoint.query_async(&client).await.unwrap(); + + assert_eq!(eod_result.pagination.limit, 3); + assert_eq!(eod_result.pagination.offset, 0); + + assert_eq!(eod_result.data.len(), 3); +} + +#[test] +#[ignore] +fn test_dividends_date() { + let api_key = setup::setup_key(); + let client = Marketstack::new_insecure("api.marketstack.com", api_key).unwrap(); + + let endpoint = dividends::Dividends::builder() + .symbol("AAPL") + .date_from(NaiveDate::from_ymd_opt(2023, 8, 10).unwrap()) + .date_to(NaiveDate::from_ymd_opt(2023, 8, 12).unwrap()) + .build() + .unwrap(); + let dividends_result: DividendsData = endpoint.query(&client).unwrap(); + + assert_eq!(dividends_result.data.len(), 1); + assert_eq!(dividends_result.data[0].dividend, 0.24); +}