use chrono::DateTime;
use chrono::Utc;
use serde::Deserialize;
use serde::Serialize;
use serde_urlencoded::to_string as to_query;
use crate::data::v2::Feed;
use crate::data::DATA_BASE_URL;
use crate::util::vec_from_str;
use crate::Str;
pub use super::last_quotes::Quote;
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[non_exhaustive]
pub struct Quotes {
#[serde(deserialize_with = "vec_from_str")]
pub quotes: Vec<Quote>,
pub symbol: String,
pub next_page_token: Option<String>,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct QuotesReqInit {
pub limit: Option<usize>,
pub feed: Option<Feed>,
pub page_token: Option<String>,
#[doc(hidden)]
pub _non_exhaustive: (),
}
impl QuotesReqInit {
#[inline]
pub fn init<S>(self, symbol: S, start: DateTime<Utc>, end: DateTime<Utc>) -> QuotesReq
where
S: Into<String>,
{
QuotesReq {
symbol: symbol.into(),
start,
end,
limit: self.limit,
feed: self.feed,
page_token: self.page_token,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct QuotesReq {
#[serde(skip)]
pub symbol: String,
#[serde(rename = "start")]
pub start: DateTime<Utc>,
#[serde(rename = "end")]
pub end: DateTime<Utc>,
#[serde(rename = "limit")]
pub limit: Option<usize>,
#[serde(rename = "feed")]
pub feed: Option<Feed>,
#[serde(rename = "page_token")]
pub page_token: Option<String>,
}
Endpoint! {
pub Get(QuotesReq),
Ok => Quotes, [
OK,
],
Err => GetError, [
UNPROCESSABLE_ENTITY => InvalidInput,
]
fn base_url() -> Option<Str> {
Some(DATA_BASE_URL.into())
}
#[inline]
fn path(input: &Self::Input) -> Str {
format!("/v2/stocks/{}/quotes", input.symbol).into()
}
fn query(input: &Self::Input) -> Result<Option<Str>, Self::ConversionError> {
Ok(Some(to_query(input)?.into()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr as _;
use num_decimal::Num;
use test_log::test;
use crate::api_info::ApiInfo;
use crate::Client;
use crate::RequestError;
#[test(tokio::test)]
async fn request_quotes() {
let api_info = ApiInfo::from_env().unwrap();
let client = Client::new(api_info);
let start = DateTime::from_str("2022-01-04T13:35:59Z").unwrap();
let end = DateTime::from_str("2022-01-04T13:36:00Z").unwrap();
let request = QuotesReqInit::default().init("SPY", start, end);
let quotes = client.issue::<Get>(&request).await.unwrap();
assert_eq!("es.symbol, "SPY");
for quote in quotes.quotes {
assert!(quote.time >= start, "{}", quote.time);
assert!(quote.time <= end, "{}", quote.time);
assert_ne!(quote.ask_price, Num::from(0));
assert_ne!(quote.bid_price, Num::from(0));
assert_ne!(quote.ask_size, 0);
assert_ne!(quote.bid_size, 0);
}
}
#[test(tokio::test)]
async fn invalid_symbol() {
let api_info = ApiInfo::from_env().unwrap();
let client = Client::new(api_info);
let start = DateTime::from_str("2022-01-04T13:35:59Z").unwrap();
let end = DateTime::from_str("2022-01-04T13:36:00Z").unwrap();
let request = QuotesReqInit::default().init("ABC123", start, end);
let err = client.issue::<Get>(&request).await.unwrap_err();
match err {
RequestError::Endpoint(GetError::InvalidInput(Ok(message))) if message.code == 42210000 => (),
_ => panic!("Received unexpected error: {err:?}"),
};
}
#[test(tokio::test)]
async fn invalid_page_token() {
let api_info = ApiInfo::from_env().unwrap();
let client = Client::new(api_info);
let start = DateTime::from_str("2022-01-04T13:35:59Z").unwrap();
let end = DateTime::from_str("2022-01-04T13:36:00Z").unwrap();
let request = QuotesReqInit {
page_token: Some("123456789abcdefghi".to_string()),
..Default::default()
}
.init("SPY", start, end);
let err = client.issue::<Get>(&request).await.unwrap_err();
match err {
RequestError::Endpoint(GetError::InvalidInput(_)) => (),
_ => panic!("Received unexpected error: {err:?}"),
};
}
#[test(tokio::test)]
async fn page_quotes() {
let api_info = ApiInfo::from_env().unwrap();
let client = Client::new(api_info);
let start = DateTime::from_str("2022-01-04T13:35:00Z").unwrap();
let end = DateTime::from_str("2022-01-04T13:36:00Z").unwrap();
let mut request = QuotesReqInit {
limit: Some(2),
..Default::default()
}
.init("SPY", start, end);
let mut last_quotes = None;
for _ in 0..3 {
let quotes = client.issue::<Get>(&request).await.unwrap();
assert_ne!(Some(quotes.clone()), last_quotes);
request.page_token = quotes.next_page_token.clone();
last_quotes = Some(quotes);
}
}
}