Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dividends Endpoint #17

Merged
merged 3 commits into from
Oct 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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