Skip to content

Commit

Permalink
feat(api): implement exchanges endpoint (#36)
Browse files Browse the repository at this point in the history
* feat(exchanges): add basic types and deserialization for exchanges

* feat(api): implement exchanges eod endpoint

* tests(api): add integration tests for exchanges eod
  • Loading branch information
reubenwong97 authored Nov 1, 2023
1 parent 9ab3af5 commit a092b59
Show file tree
Hide file tree
Showing 5 changed files with 527 additions and 2 deletions.
1 change: 1 addition & 0 deletions src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ pub mod common;
pub mod currencies;
pub mod dividends;
pub mod eod;
pub mod exchanges;
pub mod paged;
pub mod splits;
pub mod tickers;
Expand Down
221 changes: 221 additions & 0 deletions src/api/exchanges.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
//! Implemented for `exchanges` and associated endpoints.

use std::borrow::Cow;

use derive_builder::Builder;

use crate::api::eod::Eod;
use crate::api::paged::PaginationError;
use crate::api::{endpoint_prelude::*, ApiError};

/// Base for `exchanges`.
#[derive(Debug, Builder, Clone)]
#[builder(setter(strip_option), build_fn(validate = "Self::validate"))]
pub struct Exchanges<'a> {
/// Obtain information about a specific stock exchange by attaching its MIC
/// identification to your API request URL, e.g. `/exchanges/XNAS`.
#[builder(setter(into), default)]
mic: Option<Cow<'a, str>>,
/// Obtain all available tickers for a specific exchange by attaching the
/// exchange MIC as well as `/tickers`, e.g. `/exchanges/XNAS/tickers`.
#[builder(setter(into), default)]
tickers: Option<Cow<'a, str>>,
/// `Eod` struct being built, and held by the `Exchanges` struct.
/// Results in the `/exchanges/[mic]/eod` endpoint.
#[builder(setter(into), default)]
eod: Option<Eod<'a>>,
/// Search stock exchanges by name or MIC.
#[builder(setter(into), default)]
search: Option<Cow<'a, str>>,
/// 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> Exchanges<'a> {
/// Create a builder for the endpoint.
pub fn builder() -> ExchangesBuilder<'a> {
ExchangesBuilder::default()
}
}

impl<'a> Endpoint for Exchanges<'a> {
fn method(&self) -> Method {
Method::GET
}

fn endpoint(&self) -> Cow<'static, str> {
let mut endpoint = "exchanges".to_owned();
if let Some(mic) = &self.mic {
endpoint.push_str(&format!("/{}", mic));

// NOTE: validator will ensure only one can be active.
if let Some(tickers) = &self.tickers {
endpoint.push_str(&format!("/{}", tickers));
}
if let Some(eod) = &self.eod {
endpoint.push_str(&format!("/{}", eod.endpoint().as_ref()));
}
}

endpoint.into()
}

fn parameters(&self) -> QueryParams {
let mut params = QueryParams::default();

// NOTE: Not the most ergonomic way I want to go about this, but its okay for now since
// only one "extension" endpoint like `eod` or `splits` can be active per `tickers`
// endpoint query to Marketstack.
if let Some(eod) = &self.eod {
params = eod.parameters().clone();
}

params
.push_opt("search", self.search.as_ref())
.push_opt("limit", self.limit.clone())
.push_opt("offset", self.offset);

params
}
}

impl<'a> ExchangesBuilder<'a> {
/// 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)
}

/// Check that `Exchanges` contains valid endpoint combinations
fn validate(&self) -> Result<(), String> {
let active_fields = [self.tickers.is_some(), self.eod.is_some()];
let count = active_fields.iter().filter(|x| **x).count();

if count > 1 {
Err("Invalid combinations of `eod`, `tickers` or `intraday`".into())
} else {
Ok(())
}
}
}

#[cfg(test)]
mod tests {

use chrono::NaiveDate;

use crate::api::eod::Eod;
use crate::api::exchanges::Exchanges;
use crate::api::{self, Query};
use crate::test::client::{ExpectedUrl, SingleTestClient};

#[test]
fn exchanges_defaults_are_sufficient() {
Exchanges::builder().build().unwrap();
}

#[test]
fn exchanges() {
let endpoint = ExpectedUrl::builder()
.endpoint("exchanges")
.build()
.unwrap();
let client = SingleTestClient::new_raw(endpoint, "");

let endpoint = Exchanges::builder().build().unwrap();
api::ignore(endpoint).query(&client).unwrap();
}

#[test]
fn exchanges_mic() {
let endpoint = ExpectedUrl::builder()
.endpoint("exchanges/XNAS")
.build()
.unwrap();
let client = SingleTestClient::new_raw(endpoint, "");

let endpoint = Exchanges::builder().mic("XNAS").build().unwrap();
api::ignore(endpoint).query(&client).unwrap();
}

#[test]
fn exchanges_mic_tickers() {
let endpoint = ExpectedUrl::builder()
.endpoint("exchanges/XNAS/tickers")
.build()
.unwrap();
let client = SingleTestClient::new_raw(endpoint, "");

let endpoint = Exchanges::builder()
.mic("XNAS")
.tickers("tickers")
.build()
.unwrap();
api::ignore(endpoint).query(&client).unwrap();
}

#[test]
fn exchanges_mic_eod() {
let endpoint = ExpectedUrl::builder()
.endpoint("exchanges/XNAS/eod")
.add_query_params(&[("limit", "5")])
.build()
.unwrap();
let client = SingleTestClient::new_raw(endpoint, "");

let endpoint = Exchanges::builder()
.mic("XNAS")
.eod(Eod::builder().limit(5).unwrap().build().unwrap())
.build()
.unwrap();
api::ignore(endpoint).query(&client).unwrap();
}

#[test]
fn exchanges_mic_eod_latest() {
let endpoint = ExpectedUrl::builder()
.endpoint("exchanges/XNAS/eod/latest")
.add_query_params(&[("limit", "5")])
.build()
.unwrap();
let client = SingleTestClient::new_raw(endpoint, "");

let endpoint = Exchanges::builder()
.mic("XNAS")
.eod(Eod::builder().latest(true).build().unwrap())
.limit(5)
.unwrap()
.build()
.unwrap();
api::ignore(endpoint).query(&client).unwrap();
}

#[test]
fn exchanges_mic_eod_date() {
let endpoint = ExpectedUrl::builder()
.endpoint("exchanges/XNAS/eod/2023-05-05")
.add_query_params(&[("limit", "5")])
.build()
.unwrap();
let client = SingleTestClient::new_raw(endpoint, "");

let endpoint = Exchanges::builder()
.mic("XNAS")
.eod(
Eod::builder()
.date(NaiveDate::from_ymd_opt(2023, 5, 5).unwrap())
.build()
.unwrap(),
)
.limit(5)
.unwrap()
.build()
.unwrap();
api::ignore(endpoint).query(&client).unwrap();
}
}
2 changes: 1 addition & 1 deletion src/api/tickers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ impl<'a> TickersBuilder<'a> {
Ok(new)
}

//// Check that `Tickers` contains valid endpoint combinations.
/// Check that `Tickers` contains valid endpoint combinations.
fn validate(&self) -> Result<(), String> {
let active_fields = [
self.eod.is_some(),
Expand Down
Loading

0 comments on commit a092b59

Please sign in to comment.