From 0a9b033f90f44ba6bf2b23cfbb7608716291ea8f Mon Sep 17 00:00:00 2001 From: Jonathan Strong Date: Fri, 17 Apr 2020 00:04:36 -0400 Subject: [PATCH] redo Deserialize impls for Currency, Exchange, and Ticker uses Visitor structs to provide borrow- and byte-friendly deserialization routines. --- Cargo.toml | 5 +- src/crypto.rs | 495 ++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 466 insertions(+), 34 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f924931..7790f49 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "markets" -version = "0.3.1" +version = "0.4.0" authors = ["Jonathan Strong "] edition = "2018" description = "kind of like the http crate, except about tradeable markets" @@ -20,5 +20,8 @@ hashbrown = { version = "0.6.3", default_features = false, features = ["serde", #decimal = { git = "https://git.mmcxi.com/mm/decimal", branch = "v2.3.x" } #decimal-macros = { git = "https://git.mmcxi.com/mm/decimal-macros", rev = "v0.2.1", optional = true } +[dev-dependencies] +serde_json = "1" + [features] unstable = [] diff --git a/src/crypto.rs b/src/crypto.rs index 5a811a6..4838c32 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -6,7 +6,7 @@ use std::cmp::{PartialEq, Eq}; use std::convert::TryFrom; use serde::{Serialize, Deserialize}; -use serde::de::{self, Deserializer}; +use serde::de::{self, Deserializer, Visitor}; use serde::Serializer; //use decimal::d128; //use chrono::{DateTime, Utc, TimeZone}; @@ -139,7 +139,9 @@ impl FromStr for Side { macro_rules! make_currency { - ($(|$ticker:ident, $name:expr, $code:expr, $upper:expr),*) => { + //(@as_ident $name:ident) => { $name }; + + ($(|$ticker:ident, $name:expr, $code:expr, $upper:ident),*) => { #[derive(Debug, PartialEq, Clone, Hash, Eq, Copy, PartialOrd, Ord, Serialize)] #[allow(non_camel_case_types)] #[repr(u8)] @@ -172,6 +174,35 @@ macro_rules! make_currency { } } + #[allow(non_upper_case_globals)] + pub fn from_bytes<'a>(bytes: &'a [u8]) -> Result> { + // this declares for each currency the equivalent of + // const BTC : &[u8] = "btc".as_bytes(); + $( + const $upper: &[u8] = stringify!($ticker).as_bytes(); + )* + + // this declares for each currency the equivalent of + // const btc : &[u8] = "BTC".as_bytes(); + $( + const $ticker: &[u8] = stringify!($upper).as_bytes(); + )* + + match bytes { + // first try lowercase (identified by uppercase consts) + $( + $upper => { return Ok(Currency::$ticker) } + )* + + // then try uppercase (identified by lowercase consts) + $( + $ticker => { return Ok(Currency::$ticker) } + )* + + other => Err(UnlistedCurrency(std::str::from_utf8(other).unwrap_or(""))) + } + } + /// Optimized for expecting lowercase. Does not attempt to search beyond /// every currency symbol in lowercase. /// @@ -205,6 +236,14 @@ macro_rules! make_currency { $( c!($ticker) ),* ] } + + pub fn variant_str_slice() -> &'static [&'static str] { + &[ + $( + stringify!($ticker) + ),* + ] + } } impl TryFrom for Currency { @@ -256,6 +295,44 @@ macro_rules! make_currency { assert_eq!(c.as_str().to_uppercase(), c.to_str_uppercase().to_string()); } } + + #[test] + fn checks_from_bytes_for_lower_and_upper_tickers() { + $( + assert_eq!( + Currency::from_bytes(Currency::$ticker.as_str().as_bytes()).unwrap(), + Currency::$ticker + ); + assert_eq!( + Currency::from_bytes(Currency::$ticker.to_str_uppercase().as_bytes()).unwrap(), + Currency::$ticker + ); + )* + } + + #[test] + fn check_serde_json_deser() { + #[derive(Serialize, Deserialize, PartialEq, Debug)] + struct A { + pub denom: Currency, + } + + $( + { + let a = A { denom: Currency::$ticker }; + let to_json = serde_json::to_string(&a).unwrap(); + let manual_json = format!("{{\"denom\":\"{}\"}}", stringify!($ticker)); + let manual_json_uppercase = format!("{{\"denom\":\"{}\"}}", stringify!($upper)); + + assert_eq!(a, serde_json::from_str(&to_json).unwrap()); + assert_eq!(a, serde_json::from_str(&manual_json).unwrap()); + assert_eq!(a, serde_json::from_str(&manual_json_uppercase).unwrap()); + assert_eq!(a, serde_json::from_slice(manual_json.as_bytes()).unwrap()); + assert_eq!(a, serde_json::from_slice(manual_json_uppercase.as_bytes()).unwrap()); + + } + )* + } } } @@ -393,13 +470,223 @@ fn check_generated_currency_fns() { assert_eq!(i16::from(c!(usd)), 100_i16); } +struct CurrencyVisitor; +struct ExchangeVisitor; +struct TickerVisitor; + +impl<'de> Visitor<'de> for CurrencyVisitor { + type Value = Currency; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a currency symbol or ticker, like those used by exchanges (e.g. btc, usd)") + } + + fn visit_str(self, v: &str) -> Result where + E: de::Error + { + Currency::from_str_lowercase(v) + .or_else(|_| Currency::from_str_uppercase(v)) + .map_err(|UnlistedCurrency(unknown)| E::unknown_variant(unknown, Currency::variant_str_slice())) + } + + fn visit_borrowed_str(self, v: &'de str) -> Result where + E: de::Error + { + Currency::from_str_lowercase(v) + .or_else(|_| Currency::from_str_uppercase(v)) + .map_err(|UnlistedCurrency(unknown)| E::unknown_variant(unknown, Currency::variant_str_slice())) + } + + fn visit_bytes(self, v: &[u8]) -> Result where + E: de::Error + { + Currency::from_bytes(v) + .map_err(|UnlistedCurrency(unknown)| E::unknown_variant(unknown, Currency::variant_str_slice())) + } + + fn visit_borrowed_bytes(self, v: &'de [u8]) -> Result where + E: de::Error + { + Currency::from_bytes(v) + .map_err(|UnlistedCurrency(unknown)| E::unknown_variant(unknown, Currency::variant_str_slice())) + } +} + +impl<'de> Visitor<'de> for ExchangeVisitor { + type Value = Exchange; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a four-character exchange symbol (e.g. gdax, bmex)") + } + + fn visit_str(self, v: &str) -> Result where + E: de::Error + { + Exchange::from_str_lowercase(v) + .or_else(|_| Exchange::from_str_uppercase(v)) + .map_err(|UnlistedExchange(unknown)| E::unknown_variant(unknown, Exchange::variant_str_slice())) + } + + fn visit_borrowed_str(self, v: &'de str) -> Result where + E: de::Error + { + Exchange::from_str_lowercase(v) + .or_else(|_| Exchange::from_str_uppercase(v)) + .map_err(|UnlistedExchange(unknown)| E::unknown_variant(unknown, Exchange::variant_str_slice())) + } + + fn visit_bytes(self, v: &[u8]) -> Result where + E: de::Error + { + Exchange::from_bytes(v) + .map_err(|UnlistedExchange(unknown)| E::unknown_variant(unknown, Exchange::variant_str_slice())) + } + + fn visit_borrowed_bytes(self, v: &'de [u8]) -> Result where + E: de::Error + { + Exchange::from_bytes(v) + .map_err(|UnlistedExchange(unknown)| E::unknown_variant(unknown, Exchange::variant_str_slice())) + } +} + +impl<'de> Visitor<'de> for TickerVisitor { + type Value = Ticker; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a currency rate symbol or ticker, like those used by exchanges (e.g. btc_usd)") + } + + fn visit_str(self, v: &str) -> Result where + E: de::Error + { + let mut parts = v.split(|c| c == '_' || c == '-' || c == '/'); + + match parts.next() { + Some(base_str) => { + match parts.next() { + Some(quote_str) => { + let base = Currency::from_str_lowercase(base_str) + .or_else(|_| Currency::from_str_uppercase(base_str)) + .map_err(|UnlistedCurrency(unknown)| E::unknown_variant(unknown, Currency::variant_str_slice()))?; + + let quote = Currency::from_str_lowercase(quote_str) + .or_else(|_| Currency::from_str_uppercase(quote_str)) + .map_err(|UnlistedCurrency(unknown)| E::unknown_variant(unknown, Currency::variant_str_slice()))?; + + Ok(Ticker { base, quote }) + } + + None => Err(E::missing_field("quote")) + } + } + + None => Err(E::missing_field("base")) + } + } + + fn visit_borrowed_str(self, v: &'de str) -> Result where + E: de::Error + { + let mut parts = v.split(|c| c == '_' || c == '-' || c == '/'); + + match parts.next() { + Some(base_str) => { + match parts.next() { + Some(quote_str) => { + let base = Currency::from_str_lowercase(base_str) + .or_else(|_| Currency::from_str_uppercase(base_str)) + .map_err(|UnlistedCurrency(unknown)| E::unknown_variant(unknown, Currency::variant_str_slice()))?; + + let quote = Currency::from_str_lowercase(quote_str) + .or_else(|_| Currency::from_str_uppercase(quote_str)) + .map_err(|UnlistedCurrency(unknown)| E::unknown_variant(unknown, Currency::variant_str_slice()))?; + + Ok(Ticker { base, quote }) + } + + None => Err(E::missing_field("quote")) + } + } + + None => Err(E::missing_field("base")) + } + } + + fn visit_bytes(self, v: &[u8]) -> Result where + E: de::Error + { + let mut parts = v.split(|&c| c == b'_' || c == b'-' || c == b'/'); + + match parts.next() { + Some(base_str) => { + match parts.next() { + Some(quote_str) => { + let base = Currency::from_bytes(base_str) + .map_err(|UnlistedCurrency(unknown)| E::unknown_variant(unknown, Currency::variant_str_slice()))?; + + let quote = Currency::from_bytes(quote_str) + .map_err(|UnlistedCurrency(unknown)| E::unknown_variant(unknown, Currency::variant_str_slice()))?; + + Ok(Ticker { base, quote }) + } + + None => Err(E::missing_field("quote")) + } + } + + None => Err(E::missing_field("base")) + } + } + + fn visit_borrowed_bytes(self, v: &'de [u8]) -> Result where + E: de::Error + { + let mut parts = v.split(|&c| c == b'_' || c == b'-' || c == b'/'); + + match parts.next() { + Some(base_str) => { + match parts.next() { + Some(quote_str) => { + let base = Currency::from_bytes(base_str) + .map_err(|UnlistedCurrency(unknown)| E::unknown_variant(unknown, Currency::variant_str_slice()))?; + + let quote = Currency::from_bytes(quote_str) + .map_err(|UnlistedCurrency(unknown)| E::unknown_variant(unknown, Currency::variant_str_slice()))?; + + Ok(Ticker { base, quote }) + } + + None => Err(E::missing_field("quote")) + } + } + + None => Err(E::missing_field("base")) + } + } +} + impl <'de> Deserialize<'de> for Currency { fn deserialize(deserializer: D) -> std::result::Result - where D: Deserializer<'de> { + where D: Deserializer<'de> + { + deserializer.deserialize_bytes(CurrencyVisitor) + } +} - use serde::de::Error; - let c = String::deserialize(deserializer)?; - Currency::from_str(&c).map_err(|err| D::Error::custom(format!("{:?}", err))) +impl <'de> Deserialize<'de> for Exchange { + fn deserialize(deserializer: D) -> std::result::Result + where D: Deserializer<'de> + { + deserializer.deserialize_bytes(ExchangeVisitor) + } +} + +impl <'de> Deserialize<'de> for Ticker { + fn deserialize(deserializer: D) -> std::result::Result + where D: Deserializer<'de> + { + deserializer.deserialize_bytes(TickerVisitor) } } @@ -410,9 +697,9 @@ impl Display for Currency { } macro_rules! make_exchange { - ($(|$ticker:ident, $name:expr, $code:expr),*) => { + ($(|$ticker:ident, $name:expr, $code:expr, $upper:ident),*) => { #[derive(Debug, PartialEq, Clone, Hash, Eq, Copy, PartialOrd, Ord, - Serialize, Deserialize)] + Serialize)] #[allow(non_camel_case_types)] #[repr(u8)] pub enum Exchange { @@ -436,11 +723,84 @@ macro_rules! make_exchange { } } + pub fn to_str_uppercase(&self) -> &'static str { + match *self { + $( + e!($ticker) => { stringify!($upper) } + ),* + } + } + pub fn all() -> Vec { vec![ $( e!($ticker) ),* ] } + + pub fn variant_str_slice() -> &'static [&'static str] { + &[ + $( + stringify!($ticker) + ),* + ] + } + + #[allow(non_upper_case_globals)] + pub fn from_bytes<'a>(bytes: &'a [u8]) -> Result> { + // this declares for each currency the equivalent of + // const BTC : &[u8] = "btc".as_bytes(); + $( + const $upper: &[u8] = stringify!($ticker).as_bytes(); + )* + + // this declares for each currency the equivalent of + // const btc : &[u8] = "BTC".as_bytes(); + $( + const $ticker: &[u8] = stringify!($upper).as_bytes(); + )* + + match bytes { + // first try lowercase (identified by uppercase consts) + $( + $upper => { return Ok(Exchange::$ticker) } + )* + + // then try uppercase (identified by lowercase consts) + $( + $ticker => { return Ok(Exchange::$ticker) } + )* + + other => Err(UnlistedExchange(std::str::from_utf8(other).unwrap_or(""))) + } + } + + /// Optimized for expecting lowercase. Does not attempt to search beyond + /// every currency symbol in lowercase. + /// + pub fn from_str_lowercase<'a>(s: &'a str) -> Result> { + match s { + $( + stringify!($ticker) => { Ok(Exchange::$ticker) } + + )* + + other => Err(UnlistedExchange(other)) + } + } + + /// Optimized for expecting uppercase. Does not attempt to search beyond + /// every currency symbol in uppercase. + /// + pub fn from_str_uppercase<'a>(s: &'a str) -> Result> { + match s { + $( + stringify!($upper) => { Ok(Exchange::$ticker) } + + )* + + other => Err(UnlistedExchange(other)) + } + } } impl TryFrom for Exchange { @@ -554,20 +914,65 @@ macro_rules! make_exchange { } } } + + #[test] + fn it_verifies_exch_lower_and_upper_consistency() { + for x in Exchange::all() { + assert_eq!(x.as_str().to_uppercase(), x.to_str_uppercase().to_string()); + } + } + + #[test] + fn checks_from_bytes_for_lower_and_upper_tickers_exch() { + $( + assert_eq!( + Exchange::from_bytes(Exchange::$ticker.as_str().as_bytes()).unwrap(), + Exchange::$ticker + ); + assert_eq!( + Exchange::from_bytes(Exchange::$ticker.to_str_uppercase().as_bytes()).unwrap(), + Exchange::$ticker + ); + )* + } + + #[test] + fn check_serde_json_deser_exch() { + #[derive(Serialize, Deserialize, PartialEq, Debug)] + struct A { + pub exch: Exchange, + } + + $( + { + let a = A { exch: Exchange::$ticker }; + let to_json = serde_json::to_string(&a).unwrap(); + let manual_json = format!("{{\"exch\":\"{}\"}}", stringify!($ticker)); + let manual_json_uppercase = format!("{{\"exch\":\"{}\"}}", stringify!($upper)); + + assert_eq!(a, serde_json::from_str(&to_json).unwrap()); + assert_eq!(a, serde_json::from_str(&manual_json).unwrap()); + assert_eq!(a, serde_json::from_str(&manual_json_uppercase).unwrap()); + assert_eq!(a, serde_json::from_slice(manual_json.as_bytes()).unwrap()); + assert_eq!(a, serde_json::from_slice(manual_json_uppercase.as_bytes()).unwrap()); + + } + )* + } } } make_exchange!( - | plnx, "Poloniex", 1, - | krkn, "Kraken", 2, - | gdax, "GDAX", 3, - | exmo, "Exmo", 4, - | bits, "Bitstamp", 5, - | bmex, "Bitmex", 6, - | btfx, "Bitfinex", 7, - | bnce, "Binance", 8, - | okex, "OKEx", 9, - | drbt, "Deribit", 10 + | plnx, "Poloniex", 1, PLNX, + | krkn, "Kraken", 2, KRKN, + | gdax, "GDAX", 3, GDAX, + | exmo, "Exmo", 4, EXMO, + | bits, "Bitstamp", 5, BITS, + | bmex, "Bitmex", 6, BMEX, + | btfx, "Bitfinex", 7, BTFX, + | bnce, "Binance", 8, BNCE, + | okex, "OKEx", 9, OKEX, + | drbt, "Deribit", 10, DRBT ); #[test] @@ -776,6 +1181,32 @@ macro_rules! ticker_to_u8 { assert_eq!(t.as_str().to_uppercase(), t.to_str_uppercase().to_string()); } } + + #[test] + fn check_serde_json_deser_ticker() { + #[derive(Serialize, Deserialize, PartialEq, Debug)] + struct A { + pub ticker: Ticker, + } + + $( + { + let a = A { ticker: Ticker { base: Currency::$base, quote: Currency::$quote } }; + let to_json = serde_json::to_string(&a).unwrap(); + assert_eq!(a, serde_json::from_str(&to_json).unwrap()); + + for sep in ["_", "-", "/"].iter() { + let manual_json = format!("{{\"ticker\":\"{}{}{}\"}}", stringify!($base), sep, stringify!($quote)); + let manual_json_uppercase = format!("{{\"ticker\":\"{}{}{}\"}}", stringify!($u_base), sep, stringify!($u_quote)); + + assert_eq!(a, serde_json::from_str(&manual_json).unwrap()); + assert_eq!(a, serde_json::from_str(&manual_json_uppercase).unwrap()); + assert_eq!(a, serde_json::from_slice(manual_json.as_bytes()).unwrap()); + assert_eq!(a, serde_json::from_slice(manual_json_uppercase.as_bytes()).unwrap()); + } + } + )* + } } } @@ -871,20 +1302,9 @@ impl From<(Currency, Currency)> for Ticker { impl Serialize for Ticker { fn serialize(&self, serializer: S) -> ::std::result::Result - where S: Serializer { - - let s: String = format!("{}_{}", self.base, self.quote); - serializer.serialize_str(&s) - } -} - -impl <'de> Deserialize<'de> for Ticker { - fn deserialize(deserializer: D) -> std::result::Result - where D: Deserializer<'de> { - - let s = String::deserialize(deserializer)?; - Ticker::from_str(s.as_str()) - .map_err(|_| de::Error::custom(format!("invalid Ticker '{}'", s))) + where S: Serializer + { + serializer.serialize_str(&format!("{}_{}", self.base, self.quote)) } } @@ -915,7 +1335,7 @@ impl ::std::error::Error for Error { } #[derive(Debug)] -pub struct UnlistedTicker<'a>(&'a str); +pub struct UnlistedTicker<'a>(pub &'a str); impl<'a> Display for UnlistedTicker<'a> { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { @@ -932,6 +1352,15 @@ impl<'a> Display for UnlistedCurrency<'a> { } } +#[derive(Debug)] +pub struct UnlistedExchange<'a>(&'a str); + +impl<'a> Display for UnlistedExchange<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "UnlistedExchange({})", self.0) + } +} + #[allow(unused)] #[cfg(test)] mod tests {