use std::convert::TryFrom;
use std::fmt::Display;
use std::fmt::Formatter;
use std::fmt::Result as FmtResult;
use std::ops::Deref;
use std::str::FromStr;
use serde::Deserialize;
use serde::Serialize;
use serde::Serializer;
use uuid::Error as UuidError;
use uuid::Uuid;
use crate::Str;
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct Id(pub Uuid);
impl Deref for Id {
type Target = Uuid;
#[inline]
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[non_exhaustive]
pub enum Class {
#[serde(rename = "us_equity")]
UsEquity,
#[serde(rename = "crypto")]
Crypto,
#[serde(other, rename(serialize = "unknown"))]
Unknown,
}
impl AsRef<str> for Class {
#[inline]
fn as_ref(&self) -> &'static str {
match *self {
Class::UsEquity => "us_equity",
Class::Crypto => "crypto",
Class::Unknown => "unknown",
}
}
}
impl Default for Class {
#[inline]
fn default() -> Self {
Self::UsEquity
}
}
impl FromStr for Class {
type Err = ();
#[inline]
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s == Class::UsEquity.as_ref() {
Ok(Class::UsEquity)
} else if s == Class::Crypto.as_ref() {
Ok(Class::Crypto)
} else {
Err(())
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum Status {
#[serde(rename = "active")]
Active,
#[serde(rename = "inactive")]
Inactive,
}
impl AsRef<str> for Status {
#[inline]
fn as_ref(&self) -> &'static str {
match *self {
Status::Active => "active",
Status::Inactive => "inactive",
}
}
}
impl Default for Status {
#[inline]
fn default() -> Self {
Self::Active
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ParseSymbolError {
InvalidSymbol(char),
UnknownExchange,
UnknownClass,
InvalidId(UuidError),
InvalidFormat,
}
impl Display for ParseSymbolError {
fn fmt(&self, fmt: &mut Formatter<'_>) -> FmtResult {
match self {
Self::InvalidSymbol(c) => write!(fmt, "the symbol contains an invalid character ('{c}')"),
Self::UnknownExchange => fmt.write_str("the exchange is unknown"),
Self::UnknownClass => fmt.write_str("the asset class is unknown"),
Self::InvalidId(err) => write!(fmt, "failed to parse asset ID: {err}"),
Self::InvalidFormat => fmt.write_str("the symbol is of an invalid format"),
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[serde(try_from = "&str")]
pub enum Symbol {
Sym(String),
SymExchg(String, Exchange),
SymExchgCls(String, Exchange, Class),
Id(Id),
}
impl From<Id> for Symbol {
#[inline]
fn from(symbol: Id) -> Self {
Self::Id(symbol)
}
}
impl TryFrom<&str> for Symbol {
type Error = ParseSymbolError;
fn try_from(other: &str) -> Result<Self, Self::Error> {
Symbol::from_str(other)
}
}
impl FromStr for Symbol {
type Err = ParseSymbolError;
fn from_str(sym: &str) -> Result<Self, Self::Err> {
let sym = match sym.split(':').collect::<Vec<_>>().as_slice() {
[sym] => {
if let Ok(id) = Uuid::parse_str(sym) {
Self::Id(Id(id))
} else {
let invalid = sym.as_bytes().iter().try_fold((), |(), c| {
if !c.is_ascii_alphabetic() || !c.is_ascii_uppercase() {
Err(*c as char)
} else {
Ok(())
}
});
if let Err(c) = invalid {
return Err(ParseSymbolError::InvalidSymbol(c))
}
Self::Sym((*sym).to_string())
}
},
[sym, exchg] => {
let exchg = Exchange::from_str(exchg).map_err(|_| ParseSymbolError::UnknownExchange)?;
Self::SymExchg((*sym).to_string(), exchg)
},
[sym, exchg, cls] => {
let exchg = Exchange::from_str(exchg).map_err(|_| ParseSymbolError::UnknownExchange)?;
let cls = Class::from_str(cls).map_err(|_| ParseSymbolError::UnknownClass)?;
Self::SymExchgCls((*sym).to_string(), exchg, cls)
},
_ => return Err(ParseSymbolError::InvalidFormat),
};
Ok(sym)
}
}
impl Display for Symbol {
fn fmt(&self, fmt: &mut Formatter<'_>) -> FmtResult {
match self {
Self::Sym(sym) => fmt.write_str(sym),
Self::SymExchg(sym, exchg) => write!(fmt, "{}:{}", sym, exchg.as_ref()),
Self::SymExchgCls(sym, exchg, cls) => {
write!(fmt, "{}:{}:{}", sym, exchg.as_ref(), cls.as_ref())
},
Self::Id(id) => write!(fmt, "{}", id.as_hyphenated()),
}
}
}
impl Serialize for Symbol {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[non_exhaustive]
pub enum Exchange {
#[serde(rename = "AMEX")]
Amex,
#[serde(rename = "ARCA")]
Arca,
#[serde(rename = "BATS")]
Bats,
#[serde(rename = "NASDAQ")]
Nasdaq,
#[serde(rename = "NYSE")]
Nyse,
#[serde(rename = "NYSEARCA")]
Nysearca,
#[serde(rename = "OTC")]
Otc,
#[serde(other)]
Unknown,
}
impl AsRef<str> for Exchange {
fn as_ref(&self) -> &'static str {
match *self {
Exchange::Amex => "AMEX",
Exchange::Arca => "ARCA",
Exchange::Bats => "BATS",
Exchange::Nasdaq => "NASDAQ",
Exchange::Nyse => "NYSE",
Exchange::Nysearca => "NYSEARCA",
Exchange::Otc => "OTC",
Exchange::Unknown => "unknown",
}
}
}
impl FromStr for Exchange {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s == Exchange::Amex.as_ref() {
Ok(Exchange::Amex)
} else if s == Exchange::Arca.as_ref() {
Ok(Exchange::Arca)
} else if s == Exchange::Bats.as_ref() {
Ok(Exchange::Bats)
} else if s == Exchange::Nasdaq.as_ref() {
Ok(Exchange::Nasdaq)
} else if s == Exchange::Nyse.as_ref() {
Ok(Exchange::Nyse)
} else if s == Exchange::Nysearca.as_ref() {
Ok(Exchange::Nysearca)
} else {
Err(())
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[non_exhaustive]
pub struct Asset {
#[serde(rename = "id")]
pub id: Id,
#[serde(rename = "class")]
pub class: Class,
#[serde(rename = "exchange")]
pub exchange: Exchange,
#[serde(rename = "symbol")]
pub symbol: String,
#[serde(rename = "status")]
pub status: Status,
#[serde(rename = "tradable")]
pub tradable: bool,
#[serde(rename = "marginable")]
pub marginable: bool,
#[serde(rename = "shortable")]
pub shortable: bool,
#[serde(rename = "easy_to_borrow")]
pub easy_to_borrow: bool,
#[serde(rename = "fractionable")]
pub fractionable: bool,
}
Endpoint! {
pub Get(Symbol),
Ok => Asset, [
OK,
],
Err => GetError, [
NOT_FOUND => NotFound,
]
#[inline]
fn path(input: &Self::Input) -> Str {
format!("/v2/assets/{input}").into()
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::from_str as from_json;
use serde_json::to_string as to_json;
use test_log::test;
use uuid::Uuid;
use crate::api_info::ApiInfo;
use crate::Client;
#[test]
fn parse_symbol() {
let id = "b0b6dd9d-8b9b-48a9-ba46-b9d54906e415";
assert_eq!(
Symbol::from_str(id).unwrap(),
Symbol::Id(Id(Uuid::parse_str(id).unwrap())),
);
assert_eq!(Symbol::from_str("SPY").unwrap(), Symbol::Sym("SPY".into()));
assert_eq!(
Symbol::from_str("SPY:NYSE").unwrap(),
Symbol::SymExchg("SPY".into(), Exchange::Nyse),
);
assert_eq!(
Symbol::from_str("AAPL:NASDAQ:us_equity").unwrap(),
Symbol::SymExchgCls("AAPL".into(), Exchange::Nasdaq, Class::UsEquity),
);
assert_eq!(
Symbol::from_str("AAPL:HIHI"),
Err(ParseSymbolError::UnknownExchange),
);
assert_eq!(
Symbol::from_str("AAPL:NASDAQ:blah"),
Err(ParseSymbolError::UnknownClass),
);
assert_eq!(
Symbol::from_str("Z%&Y"),
Err(ParseSymbolError::InvalidSymbol('%')),
);
assert_eq!(
Symbol::from_str("A:B:C:"),
Err(ParseSymbolError::InvalidFormat),
);
}
#[test]
fn serialize_deserialize_symbol() {
let symbol = Symbol::Sym("AAPL".to_string());
let json = to_json(&symbol).unwrap();
assert_eq!(json, r#""AAPL""#);
assert_eq!(from_json::<Symbol>(&json).unwrap(), symbol);
let symbol = Symbol::SymExchg("AAPL".to_string(), Exchange::Nasdaq);
let json = to_json(&symbol).unwrap();
assert_eq!(json, r#""AAPL:NASDAQ""#);
assert_eq!(from_json::<Symbol>(&json).unwrap(), symbol);
let symbol = Symbol::SymExchgCls("AAPL".to_string(), Exchange::Nasdaq, Class::UsEquity);
let json = to_json(&symbol).unwrap();
assert_eq!(json, r#""AAPL:NASDAQ:us_equity""#);
assert_eq!(from_json::<Symbol>(&json).unwrap(), symbol);
let id = Id(Uuid::parse_str("b0b6dd9d-8b9b-48a9-ba46-b9d54906e415").unwrap());
let symbol = Symbol::Id(id);
let json = to_json(&symbol).unwrap();
assert_eq!(json, r#""b0b6dd9d-8b9b-48a9-ba46-b9d54906e415""#);
assert_eq!(from_json::<Symbol>(&json).unwrap(), symbol);
}
#[test]
fn parse_reference_asset() {
let response = r#"{
"id": "904837e3-3b76-47ec-b432-046db621571b",
"class": "us_equity",
"exchange": "NASDAQ",
"symbol": "AAPL",
"status": "active",
"tradable": true,
"marginable": true,
"shortable": true,
"easy_to_borrow": true,
"fractionable": true
}"#;
let id = Id(Uuid::parse_str("904837e3-3b76-47ec-b432-046db621571b").unwrap());
let asset = from_json::<Asset>(response).unwrap();
assert_eq!(asset.id, id);
assert_eq!(asset.class, Class::UsEquity);
assert_eq!(asset.exchange, Exchange::Nasdaq);
assert_eq!(asset.symbol, "AAPL");
assert_eq!(asset.status, Status::Active);
assert!(asset.tradable);
assert!(asset.marginable);
assert!(asset.shortable);
assert!(asset.easy_to_borrow);
}
#[test]
fn parse_with_unknown_exchange() {
let response = r#"{
"id": "904837e3-3b76-47ec-b432-046db621571b",
"class": "us_equity",
"exchange": "ABCDEF",
"symbol": "AAPL",
"status": "active",
"tradable": true,
"marginable": true,
"shortable": true,
"easy_to_borrow": true,
"fractionable": true
}"#;
let asset = from_json::<Asset>(response).unwrap();
assert_eq!(asset.exchange, Exchange::Unknown);
}
#[test(tokio::test)]
async fn serialize_deserialize_asset() {
let api_info = ApiInfo::from_env().unwrap();
let client = Client::new(api_info);
let asset = client
.issue::<Get>(&Symbol::try_from("SPY").unwrap())
.await
.unwrap();
let json = to_json(&asset).unwrap();
assert_eq!(from_json::<Asset>(&json).unwrap(), asset);
}
#[test(tokio::test)]
async fn retrieve_asset() {
async fn test(symbol: Symbol) {
let api_info = ApiInfo::from_env().unwrap();
let client = Client::new(api_info);
let asset = client.issue::<Get>(&symbol).await.unwrap();
let id = Id(Uuid::parse_str("b0b6dd9d-8b9b-48a9-ba46-b9d54906e415").unwrap());
assert_eq!(asset.id, id);
assert_eq!(asset.class, Class::UsEquity);
assert_eq!(asset.exchange, Exchange::Nasdaq);
assert_eq!(asset.symbol, "AAPL");
assert_eq!(asset.status, Status::Active);
assert!(asset.tradable);
}
let symbols = [
Symbol::Sym("AAPL".to_string()),
Symbol::SymExchg("AAPL".to_string(), Exchange::Nasdaq),
Symbol::SymExchgCls("AAPL".to_string(), Exchange::Nasdaq, Class::UsEquity),
Symbol::Id(Id(
Uuid::parse_str("b0b6dd9d-8b9b-48a9-ba46-b9d54906e415").unwrap(),
)),
];
for symbol in symbols.iter().cloned() {
test(symbol).await;
}
}
}