diff --git a/Cargo.toml b/Cargo.toml index f7018ec..d82d24c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "marketstack" -version = "0.0.2" +version = "0.0.3" edition = "2021" license = "MIT" description = "Rust bindings for Marketstack REST API" diff --git a/src/api.rs b/src/api.rs index c20fc2b..a349a1a 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,7 +1,48 @@ -// Licensed under the MIT license -// . -// This file may not be copied, modified, or distributed -// except according to those terms. +#![warn(missing_docs)] + +//! API endpoint structures. +//! +//! The types in this module are meant to aid in constructing the appropriate calls using type-safe +//! Rust idioms. +//! +//! All endpoints use the builder pattern and have their members as private so that there are no +//! API implications of adding new members for additional query parameters in future GitLab +//! releases. +//! +//! # Example +//! +//! ```rust,no_run +//! use serde::{Deserialize, Serialize}; +//! use marketstack::Marketstack; +//! use marketstack::api::{self, Query}; +//! use marketstack::api::eod; +//! use marketstack::{PaginationInfo, EodDataItem}; +//! +//! // The return type of an `EodData`. Note that Marketstack may contain more information, but you can +//! // define your structure to only fetch what is needed. +//! #[derive(Serialize, Deserialize, Debug, Clone)] +//! pub struct EodData { +//! pub pagination: PaginationInfo, +//! pub data: Vec, +//! } +//! +//! // Create the client. +//! let client = Marketstack::new("api.marketstack.com", "private-token").unwrap(); +//! +//! // OR create an insecure token (if on the Free plan). +//! let client = Marketstack::new_insecure("api.marketstack.com", "private-token").unwrap(); +//! +//! // Create a simple endpoint. This one gets the "eod" for the AAPL symbol. +//! let endpoint = eod::Eod::builder().symbol("AAPL").build().unwrap(); +//! // Call the endpoint. The return type decides how to represent the value. +//! let eod_date: EodData = endpoint.query(&client).unwrap(); +//! +//! // Some endpoints support pagination. Since Marketstack does pagination through query +//! // params, we simply specify them in the endpoint builder. +//! // Note that there are limits defined, and therefore, limit(5) is fallible and returns +//! // a Result. +//! let pageable_endpoint = eod::Eod::builder().symbol("AAPL").limit(5).unwrap().build().unwrap(); +//! ``` mod basic; mod client; diff --git a/src/api/client.rs b/src/api/client.rs index d55a079..d4680f6 100644 --- a/src/api/client.rs +++ b/src/api/client.rs @@ -1,8 +1,3 @@ -// Licensed under the MIT license -// . -// This file may not be copied, modified, or distributed -// except according to those terms. - use std::error::Error; use async_trait::async_trait; diff --git a/src/api/common.rs b/src/api/common.rs index f0e1d21..dabc9c7 100644 --- a/src/api/common.rs +++ b/src/api/common.rs @@ -1,8 +1,3 @@ -// Licensed under the MIT license -// . -// This file may not be copied, modified, or distributed -// except according to those terms. - //! API types common to many endpoints. //! //! Usually these are enumerations or other simple wrappers around structures @@ -29,6 +24,7 @@ impl Default for SortOrder { } } impl SortOrder { + /// The string representation of the sort order. pub fn as_str(self) -> &'static str { match self { SortOrder::Ascending => "ASC", diff --git a/src/api/endpoint.rs b/src/api/endpoint.rs index 6fd84c0..650cb3a 100644 --- a/src/api/endpoint.rs +++ b/src/api/endpoint.rs @@ -6,6 +6,7 @@ use serde::de::DeserializeOwned; use crate::api::{query, ApiError, AsyncClient, AsyncQuery, BodyError, Client, Query, QueryParams}; +/// A trait for providing the necessary information for a single REST API endpoint. pub trait Endpoint { /// The HTTP method to use for the endpoint. fn method(&self) -> Method; diff --git a/src/api/endpoint_prelude.rs b/src/api/endpoint_prelude.rs index ee6db20..c832a3c 100644 --- a/src/api/endpoint_prelude.rs +++ b/src/api/endpoint_prelude.rs @@ -1,8 +1,3 @@ -// Licensed under the MIT license -// . -// This file may not be copied, modified, or distributed -// except according to those terms. - //! Endpoint prelude //! //! This module re-exports all of the types needed for endpoints to implement the diff --git a/src/api/eod.rs b/src/api/eod.rs index 97e0cc4..ae0a1c2 100644 --- a/src/api/eod.rs +++ b/src/api/eod.rs @@ -1,12 +1,654 @@ -// Licensed under the MIT license -// . -// This file may not be copied, modified, or distributed -// except according to those terms. +//! Implemented endpoints for eod, eod/latest and eod/[date]. -#![allow(clippy::module_inception)] +use std::collections::BTreeSet; -mod eod; +use chrono::NaiveDate; +use derive_builder::Builder; -pub use eod::Eod; -pub use eod::EodBuilder; -pub use eod::EodBuilderError; +use crate::api::common::SortOrder; +use crate::api::paged::PaginationError; +use crate::api::{endpoint_prelude::*, ApiError}; + +/// Query for eod. +#[derive(Debug, Builder, Clone)] +#[builder(setter(strip_option))] +pub struct Eod<'a> { + /// Search for eod for a symbol. + #[builder(setter(name = "_symbols"), default)] + symbols: BTreeSet>, + /// Exchange to filer symbol by. + #[builder(setter(into), default)] + exchange: Option>, + /// 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> Eod<'a> { + /// Create a builder for the endpoint. + pub fn builder() -> EodBuilder<'a> { + EodBuilder::default() + } +} + +impl<'a> EodBuilder<'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 Eod<'a> { + fn method(&self) -> Method { + Method::GET + } + + fn endpoint(&self) -> Cow<'static, str> { + // TODO: The other endpoints should be refactored such that whether + // it is /latest or /[date] can be checked at based on what + // options were passed into the EodBuilder. + "eod".into() + } + + fn parameters(&self) -> QueryParams { + let mut params = QueryParams::default(); + + params + .extend(self.symbols.iter().map(|value| ("symbols", value))) + .push_opt("exchange", self.exchange.as_ref()) + .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 + } +} + +/// Query for eod/latest. +#[derive(Debug, Builder, Clone)] +#[builder(setter(strip_option))] +pub struct EodLatest<'a> { + /// Search for eod for a symbol. + #[builder(setter(name = "_symbols"), default)] + symbols: BTreeSet>, + /// Exchange to filer symbol by. + #[builder(setter(into), default)] + exchange: Option>, + /// The sort order for the return results. + #[builder(default)] + sort: 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> EodLatest<'a> { + /// Create a builder for the endpoint. + pub fn builder() -> EodLatestBuilder<'a> { + EodLatestBuilder::default() + } +} + +impl<'a> EodLatestBuilder<'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 EodLatest<'a> { + fn method(&self) -> Method { + Method::GET + } + + fn endpoint(&self) -> Cow<'static, str> { + "eod/latest".into() + } + + fn parameters(&self) -> QueryParams { + let mut params = QueryParams::default(); + + params + .extend(self.symbols.iter().map(|value| ("symbols", value))) + .push_opt("exchange", self.exchange.as_ref()) + .push_opt("sort", self.sort) + .push_opt("limit", self.limit.clone()) + .push_opt("offset", self.offset); + + params + } +} + +/// Query for eod/[date]. +#[derive(Debug, Builder, Clone)] +#[builder(setter(strip_option))] +pub struct EodDate<'a> { + /// Search for eod for a symbol. + #[builder(setter(name = "_symbols"), default)] + symbols: BTreeSet>, + /// Date to query EOD data for. + date: NaiveDate, + /// Exchange to filer symbol by. + #[builder(setter(into), default)] + exchange: Option>, + /// The sort order for the return results. + #[builder(default)] + sort: 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> EodDate<'a> { + /// Create a builder for the endpoint. + pub fn builder() -> EodDateBuilder<'a> { + EodDateBuilder::default() + } +} + +impl<'a> EodDateBuilder<'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 EodDate<'a> { + fn method(&self) -> Method { + Method::GET + } + + fn endpoint(&self) -> Cow<'static, str> { + // NaiveDate.to_string() would be e.g. 2022-01-01 + format!("eod/{}", self.date).into() + } + + fn parameters(&self) -> QueryParams { + let mut params = QueryParams::default(); + + params + .extend(self.symbols.iter().map(|value| ("symbols", value))) + .push_opt("exchange", self.exchange.as_ref()) + .push_opt("sort", self.sort) + .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::eod::{Eod, EodDate, EodLatest}; + use crate::api::{self, Query}; + use crate::test::client::{ExpectedUrl, SingleTestClient}; + + #[test] + fn eod_defaults_are_sufficient() { + Eod::builder().build().unwrap(); + } + + #[test] + fn eod_endpoint() { + let endpoint = ExpectedUrl::builder().endpoint("eod").build().unwrap(); + let client = SingleTestClient::new_raw(endpoint, ""); + + let endpoint = Eod::builder().build().unwrap(); + api::ignore(endpoint).query(&client).unwrap(); + } + + #[test] + fn eod_symbol() { + let endpoint = ExpectedUrl::builder() + .endpoint("eod") + .add_query_params(&[("symbols", "AAPL")]) + .build() + .unwrap(); + let client = SingleTestClient::new_raw(endpoint, ""); + + let endpoint = Eod::builder().symbol("AAPL").build().unwrap(); + api::ignore(endpoint).query(&client).unwrap(); + } + + #[test] + fn eod_symbols() { + let endpoint = ExpectedUrl::builder() + .endpoint("eod") + .add_query_params(&[("symbols", "AAPL"), ("symbols", "GOOG")]) + .build() + .unwrap(); + let client = SingleTestClient::new_raw(endpoint, ""); + + let endpoint = Eod::builder() + .symbol("AAPL") + .symbols(["AAPL", "GOOG"].iter().copied()) + .build() + .unwrap(); + api::ignore(endpoint).query(&client).unwrap(); + } + + #[test] + fn eod_exchange() { + let endpoint = ExpectedUrl::builder() + .endpoint("eod") + .add_query_params(&[("exchange", "NYSE")]) + .build() + .unwrap(); + let client = SingleTestClient::new_raw(endpoint, ""); + + let endpoint = Eod::builder().exchange("NYSE").build().unwrap(); + api::ignore(endpoint).query(&client).unwrap(); + } + + #[test] + fn eod_sort() { + let endpoint = ExpectedUrl::builder() + .endpoint("eod") + .add_query_params(&[("sort", "ASC")]) + .build() + .unwrap(); + let client = SingleTestClient::new_raw(endpoint, ""); + + let endpoint = Eod::builder().sort(SortOrder::Ascending).build().unwrap(); + api::ignore(endpoint).query(&client).unwrap(); + } + + #[test] + fn eod_date_from() { + let endpoint = ExpectedUrl::builder() + .endpoint("eod") + .add_query_params(&[("date_from", "2020-01-01")]) + .build() + .unwrap(); + let client = SingleTestClient::new_raw(endpoint, ""); + + let endpoint = Eod::builder() + .date_from(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap()) + .build() + .unwrap(); + api::ignore(endpoint).query(&client).unwrap(); + } + + #[test] + fn eod_date_to() { + let endpoint = ExpectedUrl::builder() + .endpoint("eod") + .add_query_params(&[("date_to", "2020-01-01")]) + .build() + .unwrap(); + let client = SingleTestClient::new_raw(endpoint, ""); + + let endpoint = Eod::builder() + .date_to(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap()) + .build() + .unwrap(); + api::ignore(endpoint).query(&client).unwrap(); + } + + #[test] + fn eod_limit() { + let endpoint = ExpectedUrl::builder() + .endpoint("eod") + .add_query_params(&[("limit", "50")]) + .build() + .unwrap(); + let client = SingleTestClient::new_raw(endpoint, ""); + + let endpoint = Eod::builder().limit(50).unwrap().build().unwrap(); + api::ignore(endpoint).query(&client).unwrap(); + } + + #[test] + fn eod_over_limit() { + assert!(Eod::builder().limit(9999).is_err()); + } + + #[test] + fn eod_offset() { + let endpoint = ExpectedUrl::builder() + .endpoint("eod") + .add_query_params(&[("offset", "2")]) + .build() + .unwrap(); + let client = SingleTestClient::new_raw(endpoint, ""); + + let endpoint = Eod::builder().offset(2).build().unwrap(); + api::ignore(endpoint).query(&client).unwrap(); + } + + #[test] + fn eod_latest_defaults_are_sufficient() { + EodLatest::builder().build().unwrap(); + } + + #[test] + fn eod_latest_endpoint() { + let endpoint = ExpectedUrl::builder() + .endpoint("eod/latest") + .build() + .unwrap(); + let client = SingleTestClient::new_raw(endpoint, ""); + + let endpoint = EodLatest::builder().build().unwrap(); + api::ignore(endpoint).query(&client).unwrap(); + } + + #[test] + fn eod_latest_symbol() { + let endpoint = ExpectedUrl::builder() + .endpoint("eod/latest") + .add_query_params(&[("symbols", "AAPL")]) + .build() + .unwrap(); + let client = SingleTestClient::new_raw(endpoint, ""); + + let endpoint = EodLatest::builder().symbol("AAPL").build().unwrap(); + api::ignore(endpoint).query(&client).unwrap(); + } + + #[test] + fn eod_latest_symbols() { + let endpoint = ExpectedUrl::builder() + .endpoint("eod/latest") + .add_query_params(&[("symbols", "AAPL"), ("symbols", "GOOG")]) + .build() + .unwrap(); + let client = SingleTestClient::new_raw(endpoint, ""); + + let endpoint = EodLatest::builder() + .symbol("AAPL") + .symbols(["AAPL", "GOOG"].iter().copied()) + .build() + .unwrap(); + api::ignore(endpoint).query(&client).unwrap(); + } + + #[test] + fn eod_latest_exchange() { + let endpoint = ExpectedUrl::builder() + .endpoint("eod/latest") + .add_query_params(&[("exchange", "NYSE")]) + .build() + .unwrap(); + let client = SingleTestClient::new_raw(endpoint, ""); + + let endpoint = EodLatest::builder().exchange("NYSE").build().unwrap(); + api::ignore(endpoint).query(&client).unwrap(); + } + + #[test] + fn eod_latest_sort() { + let endpoint = ExpectedUrl::builder() + .endpoint("eod/latest") + .add_query_params(&[("sort", "ASC")]) + .build() + .unwrap(); + let client = SingleTestClient::new_raw(endpoint, ""); + + let endpoint = EodLatest::builder() + .sort(SortOrder::Ascending) + .build() + .unwrap(); + api::ignore(endpoint).query(&client).unwrap(); + } + + #[test] + fn eod_latest_limit() { + let endpoint = ExpectedUrl::builder() + .endpoint("eod/latest") + .add_query_params(&[("limit", "50")]) + .build() + .unwrap(); + let client = SingleTestClient::new_raw(endpoint, ""); + + let endpoint = EodLatest::builder().limit(50).unwrap().build().unwrap(); + api::ignore(endpoint).query(&client).unwrap(); + } + + #[test] + fn eod_latest_over_limit() { + assert!(EodLatest::builder().limit(9999).is_err()); + } + + #[test] + fn eod_latest_offset() { + let endpoint = ExpectedUrl::builder() + .endpoint("eod/latest") + .add_query_params(&[("offset", "2")]) + .build() + .unwrap(); + let client = SingleTestClient::new_raw(endpoint, ""); + + let endpoint = EodLatest::builder().offset(2).build().unwrap(); + api::ignore(endpoint).query(&client).unwrap(); + } + + #[test] + fn eod_date_endpoint() { + let endpoint = ExpectedUrl::builder() + .endpoint("eod/2022-01-01") + .build() + .unwrap(); + let client = SingleTestClient::new_raw(endpoint, ""); + + let endpoint = EodDate::builder() + .date(NaiveDate::from_ymd_opt(2022, 1, 1).unwrap()) + .build() + .unwrap(); + api::ignore(endpoint).query(&client).unwrap(); + } + + #[test] + fn eod_date_symbol() { + let endpoint = ExpectedUrl::builder() + .endpoint("eod/2022-01-01") + .add_query_params(&[("symbols", "AAPL")]) + .build() + .unwrap(); + let client = SingleTestClient::new_raw(endpoint, ""); + + let endpoint = EodDate::builder() + .symbol("AAPL") + .date(NaiveDate::from_ymd_opt(2022, 1, 1).unwrap()) + .build() + .unwrap(); + api::ignore(endpoint).query(&client).unwrap(); + } + + #[test] + fn eod_date_symbols() { + let endpoint = ExpectedUrl::builder() + .endpoint("eod/2022-01-01") + .add_query_params(&[("symbols", "AAPL"), ("symbols", "GOOG")]) + .build() + .unwrap(); + let client = SingleTestClient::new_raw(endpoint, ""); + + let endpoint = EodDate::builder() + .symbol("AAPL") + .date(NaiveDate::from_ymd_opt(2022, 1, 1).unwrap()) + .symbols(["AAPL", "GOOG"].iter().copied()) + .build() + .unwrap(); + api::ignore(endpoint).query(&client).unwrap(); + } + + #[test] + fn eod_date_exchange() { + let endpoint = ExpectedUrl::builder() + .endpoint("eod/2022-01-01") + .add_query_params(&[("exchange", "NYSE")]) + .build() + .unwrap(); + let client = SingleTestClient::new_raw(endpoint, ""); + + let endpoint = EodDate::builder() + .exchange("NYSE") + .date(NaiveDate::from_ymd_opt(2022, 1, 1).unwrap()) + .build() + .unwrap(); + api::ignore(endpoint).query(&client).unwrap(); + } + + #[test] + fn eod_date_sort() { + let endpoint = ExpectedUrl::builder() + .endpoint("eod/2022-01-01") + .add_query_params(&[("sort", "ASC")]) + .build() + .unwrap(); + let client = SingleTestClient::new_raw(endpoint, ""); + + let endpoint = EodDate::builder() + .sort(SortOrder::Ascending) + .date(NaiveDate::from_ymd_opt(2022, 1, 1).unwrap()) + .build() + .unwrap(); + api::ignore(endpoint).query(&client).unwrap(); + } + + #[test] + fn eod_date_limit() { + let endpoint = ExpectedUrl::builder() + .endpoint("eod/2022-01-01") + .add_query_params(&[("limit", "50")]) + .build() + .unwrap(); + let client = SingleTestClient::new_raw(endpoint, ""); + + let endpoint = EodDate::builder() + .date(NaiveDate::from_ymd_opt(2022, 1, 1).unwrap()) + .limit(50) + .unwrap() + .build() + .unwrap(); + api::ignore(endpoint).query(&client).unwrap(); + } + + #[test] + fn eod_date_over_limit() { + assert!(EodDate::builder() + .date(NaiveDate::from_ymd_opt(2022, 1, 1).unwrap()) + .limit(9999) + .is_err()); + } + + #[test] + fn eod_date_offset() { + let endpoint = ExpectedUrl::builder() + .endpoint("eod/2022-01-01") + .add_query_params(&[("offset", "2")]) + .build() + .unwrap(); + let client = SingleTestClient::new_raw(endpoint, ""); + + let endpoint = EodDate::builder() + .date(NaiveDate::from_ymd_opt(2022, 1, 1).unwrap()) + .offset(2) + .build() + .unwrap(); + api::ignore(endpoint).query(&client).unwrap(); + } +} diff --git a/src/api/eod/eod.rs b/src/api/eod/eod.rs deleted file mode 100644 index e0d4bac..0000000 --- a/src/api/eod/eod.rs +++ /dev/null @@ -1,247 +0,0 @@ -// Licensed under the MIT license -// . -// This file may not be copied, modified, or distributed -// except according to those terms. - -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 eod. -#[derive(Debug, Builder, Clone)] -#[builder(setter(strip_option))] -pub struct Eod<'a> { - /// Search for eod for a symbol. - #[builder(setter(name = "_symbols"), default)] - symbols: BTreeSet>, - /// Exchange to filer symbol by. - #[builder(setter(into), default)] - exchange: Option>, - /// 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> Eod<'a> { - pub fn builder() -> EodBuilder<'a> { - EodBuilder::default() - } -} - -impl<'a> EodBuilder<'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 - } - - 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 Eod<'a> { - fn method(&self) -> Method { - Method::GET - } - - fn endpoint(&self) -> Cow<'static, str> { - "eod".into() - } - - fn parameters(&self) -> QueryParams { - let mut params = QueryParams::default(); - - params - .extend(self.symbols.iter().map(|value| ("symbols", value))) - .push_opt("exchange", self.exchange.as_ref()) - .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::eod::Eod; - use crate::api::{self, Query}; - use crate::test::client::{ExpectedUrl, SingleTestClient}; - - #[test] - fn eod_defaults_are_sufficient() { - Eod::builder().build().unwrap(); - } - - #[test] - fn eod_endpoint() { - let endpoint = ExpectedUrl::builder().endpoint("eod").build().unwrap(); - let client = SingleTestClient::new_raw(endpoint, ""); - - let endpoint = Eod::builder().build().unwrap(); - api::ignore(endpoint).query(&client).unwrap(); - } - - #[test] - fn eod_symbol() { - let endpoint = ExpectedUrl::builder() - .endpoint("eod") - .add_query_params(&[("symbols", "AAPL")]) - .build() - .unwrap(); - let client = SingleTestClient::new_raw(endpoint, ""); - - let endpoint = Eod::builder().symbol("AAPL").build().unwrap(); - api::ignore(endpoint).query(&client).unwrap(); - } - - #[test] - fn eod_symbols() { - let endpoint = ExpectedUrl::builder() - .endpoint("eod") - .add_query_params(&[("symbols", "AAPL"), ("symbols", "GOOG")]) - .build() - .unwrap(); - let client = SingleTestClient::new_raw(endpoint, ""); - - let endpoint = Eod::builder() - .symbol("AAPL") - .symbols(["AAPL", "GOOG"].iter().copied()) - .build() - .unwrap(); - api::ignore(endpoint).query(&client).unwrap(); - } - - #[test] - fn eod_exchange() { - let endpoint = ExpectedUrl::builder() - .endpoint("eod") - .add_query_params(&[("exchange", "NYSE")]) - .build() - .unwrap(); - let client = SingleTestClient::new_raw(endpoint, ""); - - let endpoint = Eod::builder().exchange("NYSE").build().unwrap(); - api::ignore(endpoint).query(&client).unwrap(); - } - - #[test] - fn eod_sort() { - let endpoint = ExpectedUrl::builder() - .endpoint("eod") - .add_query_params(&[("sort", "ASC")]) - .build() - .unwrap(); - let client = SingleTestClient::new_raw(endpoint, ""); - - let endpoint = Eod::builder().sort(SortOrder::Ascending).build().unwrap(); - api::ignore(endpoint).query(&client).unwrap(); - } - - #[test] - fn eod_date_from() { - let endpoint = ExpectedUrl::builder() - .endpoint("eod") - .add_query_params(&[("date_from", "2020-01-01")]) - .build() - .unwrap(); - let client = SingleTestClient::new_raw(endpoint, ""); - - let endpoint = Eod::builder() - .date_from(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap()) - .build() - .unwrap(); - api::ignore(endpoint).query(&client).unwrap(); - } - - #[test] - fn eod_date_to() { - let endpoint = ExpectedUrl::builder() - .endpoint("eod") - .add_query_params(&[("date_to", "2020-01-01")]) - .build() - .unwrap(); - let client = SingleTestClient::new_raw(endpoint, ""); - - let endpoint = Eod::builder() - .date_to(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap()) - .build() - .unwrap(); - api::ignore(endpoint).query(&client).unwrap(); - } - - #[test] - fn eod_limit() { - let endpoint = ExpectedUrl::builder() - .endpoint("eod") - .add_query_params(&[("limit", "50")]) - .build() - .unwrap(); - let client = SingleTestClient::new_raw(endpoint, ""); - - let endpoint = Eod::builder().limit(50).unwrap().build().unwrap(); - api::ignore(endpoint).query(&client).unwrap(); - } - - #[test] - fn eod_over_limit() { - assert!(Eod::builder().limit(9999).is_err()); - } - - #[test] - fn eod_offset() { - let endpoint = ExpectedUrl::builder() - .endpoint("eod") - .add_query_params(&[("offset", "2")]) - .build() - .unwrap(); - let client = SingleTestClient::new_raw(endpoint, ""); - - let endpoint = Eod::builder().offset(2).build().unwrap(); - api::ignore(endpoint).query(&client).unwrap(); - } -} diff --git a/src/api/error.rs b/src/api/error.rs index dac68ca..050489f 100644 --- a/src/api/error.rs +++ b/src/api/error.rs @@ -1,8 +1,3 @@ -// Licensed under the MIT license -// . -// This file may not be copied, modified, or distributed -// except according to those terms. - use std::any; use std::error::Error; diff --git a/src/api/ignore.rs b/src/api/ignore.rs index a9ff173..19c59b7 100644 --- a/src/api/ignore.rs +++ b/src/api/ignore.rs @@ -1,8 +1,3 @@ -// Licensed under the MIT license -// . -// This file may not be copied, modified, or distributed -// except according to those terms. - use async_trait::async_trait; use http::{header, Request}; diff --git a/src/api/paged.rs b/src/api/paged.rs index 1a03cfd..4a6e33a 100644 --- a/src/api/paged.rs +++ b/src/api/paged.rs @@ -1,3 +1,8 @@ +//! Pagination related types and functions. +//! +//! Pagination is done simply for Marketstack, but this allows setting +//! page limits to have safety guarantees provided by the new-type pattern. + use thiserror::Error; use crate::api::ApiError; @@ -19,9 +24,11 @@ impl PageLimit { } } +/// Errors which may occur with pagination. #[derive(Debug, Error)] #[non_exhaustive] pub enum PaginationError { + /// Pagination exceeds the limit allowed by Marketstack. #[error("pagination exceeds limit error")] ExceedLimit, } diff --git a/src/api/params.rs b/src/api/params.rs index a72265d..7109564 100644 --- a/src/api/params.rs +++ b/src/api/params.rs @@ -1,8 +1,3 @@ -// Licensed under the MIT license -// . -// This file may not be copied, modified, or distributed -// except according to those terms. - use std::borrow::Cow; use chrono::{DateTime, NaiveDate, Utc}; diff --git a/src/api/query.rs b/src/api/query.rs index 6447918..56cf0af 100644 --- a/src/api/query.rs +++ b/src/api/query.rs @@ -1,8 +1,3 @@ -// Licensed under the MIT license -// . -// This file may not be copied, modified, or distributed -// except according to those terms. - use async_trait::async_trait; use http::Uri; use url::Url; diff --git a/src/api/raw.rs b/src/api/raw.rs index 7ef5941..d63b86c 100644 --- a/src/api/raw.rs +++ b/src/api/raw.rs @@ -1,8 +1,3 @@ -// Licensed under the MIT license -// . -// This file may not be copied, modified, or distributed -// except according to those terms. - use async_trait::async_trait; use http::{header, Request}; diff --git a/src/auth.rs b/src/auth.rs index 3c16707..5f7492c 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,7 +1,3 @@ -// Licensed under the MIT license -// . -// This file may not be copied, modified, or distributed -// except according to those terms. use thiserror::Error; use crate::api::{self, Query}; diff --git a/src/test/client.rs b/src/test/client.rs index 25fe11f..e570bba 100644 --- a/src/test/client.rs +++ b/src/test/client.rs @@ -1,8 +1,3 @@ -// Licensed under the MIT license -// . -// This file may not be copied, modified, or distributed -// except according to those terms. - use std::borrow::Cow; use std::cmp; use std::collections::HashMap; diff --git a/tests/eod_test.rs b/tests/eod.rs similarity index 83% rename from tests/eod_test.rs rename to tests/eod.rs index 6cb21be..d2bdcb9 100644 --- a/tests/eod_test.rs +++ b/tests/eod.rs @@ -1,3 +1,4 @@ +use marketstack::api::common::SortOrder; use marketstack::api::{eod, AsyncQuery, Query}; use marketstack::{AsyncMarketstack, EodData, Marketstack}; @@ -36,6 +37,21 @@ fn test_eod_paged() { assert_eq!(eod_result.data.len(), 5); } +#[test] +#[ignore] +fn test_eod_sorting() { + let api_key = setup::setup_key(); + let client = Marketstack::new_insecure("api.marketstack.com", api_key).unwrap(); + + let endpoint = eod::Eod::builder() + .symbol("AAPL") + .sort(SortOrder::Ascending) + .build() + .unwrap(); + + let _: EodData = endpoint.query(&client).unwrap(); +} + #[tokio::test] #[ignore] async fn test_async_eod() { diff --git a/tests/eod_date.rs b/tests/eod_date.rs new file mode 100644 index 0000000..399f72c --- /dev/null +++ b/tests/eod_date.rs @@ -0,0 +1,103 @@ +use chrono::NaiveDate; +use marketstack::api::common::SortOrder; +use marketstack::api::eod::EodDate; +use marketstack::api::{AsyncQuery, Query}; +use marketstack::{AsyncMarketstack, EodData, Marketstack}; + +mod setup; + +#[test] +#[ignore] +fn test_eod_date() { + let api_key = setup::setup_key(); + let client = Marketstack::new_insecure("api.marketstack.com", api_key).unwrap(); + + let endpoint = EodDate::builder() + .date(NaiveDate::from_ymd_opt(2023, 9, 29).unwrap()) + .symbol("AAPL") + .build() + .unwrap(); + let eod_result: EodData = endpoint.query(&client).unwrap(); + + assert_eq!(eod_result.pagination.limit, 100); + assert_eq!(eod_result.pagination.offset, 0); + + assert_eq!(eod_result.data.len(), 1); +} + +#[test] +#[ignore] +fn test_eod_date_paged() { + let api_key = setup::setup_key(); + let client = Marketstack::new_insecure("api.marketstack.com", api_key).unwrap(); + + let endpoint = EodDate::builder() + .date(NaiveDate::from_ymd_opt(2023, 9, 29).unwrap()) + .symbol("AAPL") + .limit(5) + .unwrap() + .build() + .unwrap(); + let eod_result: EodData = endpoint.query(&client).unwrap(); + + assert_eq!(eod_result.pagination.limit, 5); + assert_eq!(eod_result.data.len(), 1); +} + +#[test] +#[ignore] +fn test_eod_date_sorting() { + let api_key = setup::setup_key(); + let client = Marketstack::new_insecure("api.marketstack.com", api_key).unwrap(); + + let endpoint = EodDate::builder() + .symbol("AAPL") + .date(NaiveDate::from_ymd_opt(2023, 9, 29).unwrap()) + .sort(SortOrder::Ascending) + .build() + .unwrap(); + + let _: EodData = endpoint.query(&client).unwrap(); +} + +#[tokio::test] +#[ignore] +async fn test_async_eod_date() { + let api_key = setup::setup_key(); + let client = AsyncMarketstack::new_insecure("api.marketstack.com", api_key) + .await + .unwrap(); + + let endpoint = EodDate::builder() + .date(NaiveDate::from_ymd_opt(2023, 9, 29).unwrap()) + .symbol("AAPL") + .build() + .unwrap(); + let eod_result: EodData = endpoint.query_async(&client).await.unwrap(); + + assert_eq!(eod_result.pagination.limit, 100); + assert_eq!(eod_result.pagination.offset, 0); + + assert_eq!(eod_result.data.len(), 1); +} + +#[tokio::test] +#[ignore] +async fn test_async_eod_date_paged() { + let api_key = setup::setup_key(); + let client = AsyncMarketstack::new_insecure("api.marketstack.com", api_key) + .await + .unwrap(); + + let endpoint = EodDate::builder() + .symbol("AAPL") + .date(NaiveDate::from_ymd_opt(2023, 9, 29).unwrap()) + .limit(5) + .unwrap() + .build() + .unwrap(); + let eod_result: EodData = endpoint.query_async(&client).await.unwrap(); + + assert_eq!(eod_result.pagination.limit, 5); + assert_eq!(eod_result.data.len(), 1); +} diff --git a/tests/eod_latest.rs b/tests/eod_latest.rs new file mode 100644 index 0000000..9b30f2f --- /dev/null +++ b/tests/eod_latest.rs @@ -0,0 +1,91 @@ +use marketstack::api::common::SortOrder; +use marketstack::api::eod; +use marketstack::api::{AsyncQuery, Query}; +use marketstack::{AsyncMarketstack, EodData, Marketstack}; + +mod setup; + +#[test] +#[ignore] +fn test_eod_latest() { + let api_key = setup::setup_key(); + let client = Marketstack::new_insecure("api.marketstack.com", api_key).unwrap(); + + let endpoint = eod::EodLatest::builder().symbol("AAPL").build().unwrap(); + let eod_result: EodData = endpoint.query(&client).unwrap(); + + assert_eq!(eod_result.pagination.limit, 100); + assert_eq!(eod_result.pagination.offset, 0); + + assert_eq!(eod_result.data.len(), 1); +} + +#[test] +#[ignore] +fn test_eod_latest_paged() { + let api_key = setup::setup_key(); + let client = Marketstack::new_insecure("api.marketstack.com", api_key).unwrap(); + + let endpoint = eod::EodLatest::builder() + .symbol("AAPL") + .limit(5) + .unwrap() + .build() + .unwrap(); + let eod_result: EodData = endpoint.query(&client).unwrap(); + + assert_eq!(eod_result.pagination.limit, 5); + assert_eq!(eod_result.data.len(), 1); +} + +#[test] +#[ignore] +fn test_eod_latest_sorting() { + let api_key = setup::setup_key(); + let client = Marketstack::new_insecure("api.marketstack.com", api_key).unwrap(); + + let endpoint = eod::EodLatest::builder() + .symbol("AAPL") + .sort(SortOrder::Ascending) + .build() + .unwrap(); + + let _: EodData = endpoint.query(&client).unwrap(); +} + +#[tokio::test] +#[ignore] +async fn test_async_eod_latest() { + let api_key = setup::setup_key(); + let client = AsyncMarketstack::new_insecure("api.marketstack.com", api_key) + .await + .unwrap(); + + let endpoint = eod::EodLatest::builder().symbol("AAPL").build().unwrap(); + let eod_result: EodData = endpoint.query_async(&client).await.unwrap(); + + assert_eq!(eod_result.pagination.limit, 100); + assert_eq!(eod_result.pagination.offset, 0); + + assert_eq!(eod_result.data.len(), 1); +} + +#[tokio::test] +#[ignore] +async fn test_async_eod_paged() { + let api_key = setup::setup_key(); + let client = AsyncMarketstack::new_insecure("api.marketstack.com", api_key) + .await + .unwrap(); + + let endpoint = eod::EodLatest::builder() + .symbol("AAPL") + .limit(5) + .unwrap() + .build() + .unwrap(); + let eod_result: EodData = endpoint.query_async(&client).await.unwrap(); + + assert_eq!(eod_result.pagination.limit, 5); + assert_eq!(eod_result.data.len(), 1); +}