use std::fmt;
use rust_decimal::Decimal;
use rust_decimal::prelude::FromPrimitive as _;
use serde::de::{self, Unexpected, Visitor};
use serde::{Deserializer, Serializer};
struct DecimalVisitor;
impl Visitor<'_> for DecimalVisitor {
type Value = Decimal;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a decimal as a string or number")
}
fn visit_str<E: de::Error>(self, value: &str) -> Result<Self::Value, E> {
value
.trim()
.parse::<Decimal>()
.map_err(|_| E::invalid_value(Unexpected::Str(value), &self))
}
fn visit_f64<E: de::Error>(self, value: f64) -> Result<Self::Value, E> {
Decimal::from_f64(value).ok_or_else(|| E::invalid_value(Unexpected::Float(value), &self))
}
fn visit_i64<E: de::Error>(self, value: i64) -> Result<Self::Value, E> {
Ok(Decimal::from(value))
}
fn visit_u64<E: de::Error>(self, value: u64) -> Result<Self::Value, E> {
Ok(Decimal::from(value))
}
fn visit_i128<E: de::Error>(self, value: i128) -> Result<Self::Value, E> {
Decimal::from_i128(value).ok_or_else(|| E::custom("i128 out of range for a decimal"))
}
fn visit_u128<E: de::Error>(self, value: u128) -> Result<Self::Value, E> {
Decimal::from_u128(value).ok_or_else(|| E::custom("u128 out of range for a decimal"))
}
}
pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Decimal, D::Error> {
deserializer.deserialize_any(DecimalVisitor)
}
pub fn serialize<S: Serializer>(value: &Decimal, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&value.to_string())
}
pub mod option {
use super::{Decimal, DecimalVisitor, Deserializer, Serializer, Visitor, de, fmt};
struct OptionVisitor;
impl<'de> Visitor<'de> for OptionVisitor {
type Value = Option<Decimal>;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a decimal as a string or number, or null")
}
fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_some<D: Deserializer<'de>>(
self,
deserializer: D,
) -> Result<Self::Value, D::Error> {
struct MaybeEmpty;
impl Visitor<'_> for MaybeEmpty {
type Value = Option<Decimal>;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a decimal as a string or number, or an empty string")
}
fn visit_str<E: de::Error>(self, value: &str) -> Result<Self::Value, E> {
if value.trim().is_empty() {
return Ok(None);
}
DecimalVisitor.visit_str(value).map(Some)
}
fn visit_f64<E: de::Error>(self, value: f64) -> Result<Self::Value, E> {
DecimalVisitor.visit_f64(value).map(Some)
}
fn visit_i64<E: de::Error>(self, value: i64) -> Result<Self::Value, E> {
DecimalVisitor.visit_i64(value).map(Some)
}
fn visit_u64<E: de::Error>(self, value: u64) -> Result<Self::Value, E> {
DecimalVisitor.visit_u64(value).map(Some)
}
fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
}
deserializer.deserialize_any(MaybeEmpty)
}
}
pub fn deserialize<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Option<Decimal>, D::Error> {
deserializer.deserialize_option(OptionVisitor)
}
pub fn serialize<S: Serializer>(
value: &Option<Decimal>,
serializer: S,
) -> Result<S::Ok, S::Error> {
match value {
Some(decimal) => serializer.serialize_str(&decimal.to_string()),
None => serializer.serialize_none(),
}
}
}
#[cfg(test)]
mod tests {
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
struct Order {
#[serde(with = "crate::types::decimal")]
qty: Decimal,
#[serde(with = "crate::types::option_decimal", default)]
limit_price: Option<Decimal>,
}
fn parse(json: &str) -> Order {
serde_json::from_str(json).unwrap()
}
#[test]
fn accepts_a_string_which_is_what_alpaca_sends() {
let order = parse(r#"{"qty":"1.5","limit_price":"183.42"}"#);
assert_eq!(order.qty, Decimal::new(15, 1));
assert_eq!(order.limit_price, Some(Decimal::new(18_342, 2)));
}
#[test]
fn accepts_a_number_because_some_endpoints_send_one() {
let order = parse(r#"{"qty":3,"limit_price":183.42}"#);
assert_eq!(order.qty, Decimal::from(3));
assert_eq!(order.limit_price, Some(Decimal::new(18_342, 2)));
}
#[test]
fn null_and_empty_string_both_mean_absent() {
assert_eq!(parse(r#"{"qty":"1","limit_price":null}"#).limit_price, None);
assert_eq!(parse(r#"{"qty":"1","limit_price":""}"#).limit_price, None);
assert_eq!(parse(r#"{"qty":"1"}"#).limit_price, None);
}
#[test]
fn serializes_as_a_string_to_preserve_precision() {
let json = serde_json::to_string(&Order {
qty: Decimal::new(15, 1),
limit_price: Some(Decimal::new(18_342, 2)),
})
.unwrap();
assert_eq!(json, r#"{"qty":"1.5","limit_price":"183.42"}"#);
}
#[test]
fn absent_optional_serializes_as_null() {
let json = serde_json::to_string(&Order {
qty: Decimal::from(1),
limit_price: None,
})
.unwrap();
assert_eq!(json, r#"{"qty":"1","limit_price":null}"#);
}
#[test]
fn fractional_shares_keep_all_nine_decimal_places() {
let order = parse(r#"{"qty":"0.123456789"}"#);
assert_eq!(order.qty.to_string(), "0.123456789");
assert_eq!(
serde_json::to_string(&order).unwrap(),
r#"{"qty":"0.123456789","limit_price":null}"#
);
}
#[test]
fn a_json_number_does_not_pick_up_float_noise() {
let order = parse(r#"{"qty":"1","limit_price":183.42}"#);
assert_eq!(order.limit_price.unwrap().to_string(), "183.42");
}
#[test]
fn large_notional_values_survive() {
let order = parse(r#"{"qty":"123456789.987654321"}"#);
assert_eq!(order.qty.to_string(), "123456789.987654321");
}
#[test]
fn round_trips_through_msgpack() {
let original = Order {
qty: Decimal::new(15, 1),
limit_price: None,
};
let encoded = rmp_serde::to_vec_named(&original).unwrap();
let decoded: Order = rmp_serde::from_slice(&encoded).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn rejects_a_value_that_is_not_a_number() {
let err = serde_json::from_str::<Order>(r#"{"qty":"abc"}"#).unwrap_err();
assert!(err.to_string().contains("abc"), "{err}");
}
}