Skip to content

Commit

Permalink
Dividends Endpoint (#17)
Browse files Browse the repository at this point in the history
* add types for dividends data

* implement and unit test dividends endpoint

* add integration tests for dividends endpoint
  • Loading branch information
reubenwong97 authored Oct 20, 2023
1 parent 07db997 commit 401a7d9
Show file tree
Hide file tree
Showing 7 changed files with 375 additions and 4 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
1 change: 1 addition & 0 deletions src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
235 changes: 235 additions & 0 deletions src/api/dividends.rs
Original file line number Diff line number Diff line change
@@ -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<Cow<'a, str>>,
/// The sort order for the return results.
#[builder(default)]
sort: Option<SortOrder>,
/// Date to query EOD data from.
#[builder(default)]
date_from: Option<NaiveDate>,
/// Date to query EOD date to.
#[builder(default)]
date_to: Option<NaiveDate>,
/// Pagination limit for API request.
#[builder(setter(name = "_limit"), default)]
limit: Option<PageLimit>,
/// Pagination offset value for API request.
#[builder(default)]
offset: Option<u64>,
}

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<I, V>(&mut self, iter: I) -> &mut Self
where
I: Iterator<Item = V>,
V: Into<Cow<'a, str>>,
{
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<PaginationError>> {
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();
}
}
4 changes: 2 additions & 2 deletions src/api/splits.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! Implemented endpoints for `splits`
//! Implementation of the `splits` API endpoint.

use std::collections::BTreeSet;

Expand Down Expand Up @@ -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()
}
Expand Down
2 changes: 2 additions & 0 deletions src/marketstack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
69 changes: 68 additions & 1 deletion src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,31 @@ pub struct SplitsData {
pub data: Vec<SplitsDataItem>,
}

/// 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<DividendsDataItem>,
}

#[cfg(test)]
mod tests {
use chrono::NaiveDate;

use super::{EodData, SplitsData};
use crate::{DividendsData, EodData, SplitsData};

#[test]
fn test_deserialize_eod() {
Expand Down Expand Up @@ -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()
);
}
}
Loading

0 comments on commit 401a7d9

Please sign in to comment.