Browse Source

redo Deserialize impls for Currency, Exchange, and Ticker

uses Visitor structs to provide borrow- and byte-friendly deserialization routines.
tags/v0.4.0
Jonathan Strong 4 years ago
parent
commit
0a9b033f90
2 changed files with 466 additions and 34 deletions
  1. +4
    -1
      Cargo.toml
  2. +462
    -33
      src/crypto.rs

+ 4
- 1
Cargo.toml View File

@@ -1,6 +1,6 @@
[package]
name = "markets"
version = "0.3.1"
version = "0.4.0"
authors = ["Jonathan Strong <jonathan.strong@gmail.com>"]
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 = []

+ 462
- 33
src/crypto.rs View File

@@ -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<Self, UnlistedCurrency<'a>> {
// 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("<utf8 error>")))
}
}

/// 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<u8> 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<E>(self, v: &str) -> Result<Self::Value, E> 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<E>(self, v: &'de str) -> Result<Self::Value, E> 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<E>(self, v: &[u8]) -> Result<Self::Value, E> where
E: de::Error
{
Currency::from_bytes(v)
.map_err(|UnlistedCurrency(unknown)| E::unknown_variant(unknown, Currency::variant_str_slice()))
}

fn visit_borrowed_bytes<E>(self, v: &'de [u8]) -> Result<Self::Value, E> 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<E>(self, v: &str) -> Result<Self::Value, E> 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<E>(self, v: &'de str) -> Result<Self::Value, E> 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<E>(self, v: &[u8]) -> Result<Self::Value, E> where
E: de::Error
{
Exchange::from_bytes(v)
.map_err(|UnlistedExchange(unknown)| E::unknown_variant(unknown, Exchange::variant_str_slice()))
}

fn visit_borrowed_bytes<E>(self, v: &'de [u8]) -> Result<Self::Value, E> 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<E>(self, v: &str) -> Result<Self::Value, E> 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<E>(self, v: &'de str) -> Result<Self::Value, E> 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<E>(self, v: &[u8]) -> Result<Self::Value, E> 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<E>(self, v: &'de [u8]) -> Result<Self::Value, E> 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<D>(deserializer: D) -> std::result::Result<Self, D::Error>
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<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where D: Deserializer<'de>
{
deserializer.deserialize_bytes(ExchangeVisitor)
}
}

impl <'de> Deserialize<'de> for Ticker {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
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<Self> {
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<Self, UnlistedExchange<'a>> {
// 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("<utf8 error>")))
}
}

/// 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<Self, UnlistedExchange<'a>> {
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<Self, UnlistedExchange<'a>> {
match s {
$(
stringify!($upper) => { Ok(Exchange::$ticker) }

)*

other => Err(UnlistedExchange(other))
}
}
}

impl TryFrom<u8> 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<S>(&self, serializer: S) -> ::std::result::Result<S::Ok, S::Error>
where S: Serializer {

let s: String = format!("{}_{}", self.base, self.quote);
serializer.serialize_str(&s)
}
}

impl <'de> Deserialize<'de> for Ticker {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
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 {


Loading…
Cancel
Save