use std::fmt;
use rust_decimal::Decimal;
use rust_decimal::prelude::FromPrimitive as _;
use serde::de::{self, Unexpected, Visitor};
use serde::{Deserializer, Serializer};
fn parse_exact(value: &str) -> Option<Decimal> {
if value.contains('_') {
return None;
}
value.parse::<Decimal>().ok()
}
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> {
parse_exact(value.trim()).ok_or_else(|| 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 super::parse_exact;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
#[test]
fn a_digit_separator_is_not_a_number() {
assert_eq!("1_000".parse::<Decimal>().unwrap(), Decimal::from(1000));
assert_eq!(parse_exact("1_000"), None);
}
#[test]
fn precision_beyond_the_type_is_rounded_rather_than_refused() {
let captured = "49.6499849505605227113711001112";
let parsed = parse_exact(captured).expect("a real captured value must decode");
assert!(parsed > Decimal::new(496, 1) && parsed < Decimal::new(497, 1));
}
#[test]
fn the_exact_forms_still_parse() {
for value in [
"185.50",
"0.5",
"-0.5",
"+1.25",
"0",
"-0",
"1000",
"0001",
"9.999999999",
"79228162514264337593543950335",
] {
assert!(parse_exact(value).is_some(), "{value} should parse exactly");
}
assert_eq!(parse_exact("185.50").unwrap().to_string(), "185.50");
assert_eq!(parse_exact("+1.25").unwrap(), Decimal::new(125, 2));
}
#[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 a_number_beyond_u64_decodes_the_same_required_or_optional() {
let huge = "100000000000000000000";
let required = parse(&format!(r#"{{"qty":{huge}}}"#));
assert_eq!(required.qty.to_string(), huge);
let optional = parse(&format!(r#"{{"qty":"1","limit_price":{huge}}}"#));
assert_eq!(optional.limit_price.unwrap().to_string(), huge);
}
#[test]
fn a_number_beyond_decimals_range_is_an_error() {
let beyond = "1".to_owned() + &"0".repeat(40);
assert!(serde_json::from_str::<Order>(&format!(r#"{{"qty":{beyond}}}"#)).is_err());
}
#[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}");
}
}