use std::fmt;
use std::str::FromStr;
use rust_decimal::Decimal;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use super::serde_util::decimal_from_str_or_number;
macro_rules! string_newtype {
($(#[$meta:meta])* $name:ident) => {
$(#[$meta])*
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct $name(pub String);
impl $name {
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl From<String> for $name {
fn from(value: String) -> Self {
$name(value)
}
}
impl From<&str> for $name {
fn from(value: &str) -> Self {
$name(value.to_owned())
}
}
impl FromStr for $name {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok($name(s.to_owned()))
}
}
};
}
string_newtype! {
AccountId
}
string_newtype! {
TransactionId
}
string_newtype! {
OrderId
}
string_newtype! {
TradeId
}
string_newtype! {
ClientId
}
string_newtype! {
ClientTag
}
string_newtype! {
ClientComment
}
string_newtype! {
RequestId
}
string_newtype! {
Currency
}
string_newtype! {
OrderSpecifier
}
string_newtype! {
TradeSpecifier
}
impl OrderSpecifier {
pub fn from_client_id(id: impl AsRef<str>) -> Self {
OrderSpecifier(format!("@{}", id.as_ref()))
}
}
impl From<&OrderId> for OrderSpecifier {
fn from(id: &OrderId) -> Self {
OrderSpecifier(id.0.clone())
}
}
impl From<OrderId> for OrderSpecifier {
fn from(id: OrderId) -> Self {
OrderSpecifier(id.0)
}
}
impl TradeSpecifier {
pub fn from_client_id(id: impl AsRef<str>) -> Self {
TradeSpecifier(format!("@{}", id.as_ref()))
}
}
impl From<&TradeId> for TradeSpecifier {
fn from(id: &TradeId) -> Self {
TradeSpecifier(id.0.clone())
}
}
impl From<TradeId> for TradeSpecifier {
fn from(id: TradeId) -> Self {
TradeSpecifier(id.0)
}
}
macro_rules! decimal_newtype {
($(#[$meta:meta])* $name:ident) => {
$(#[$meta])*
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
pub struct $name(pub Decimal);
impl $name {
pub fn value(&self) -> Decimal {
self.0
}
}
impl Serialize for $name {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_str(&self.0)
}
}
impl<'de> Deserialize<'de> for $name {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
decimal_from_str_or_number(deserializer).map($name)
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}
impl From<Decimal> for $name {
fn from(value: Decimal) -> Self {
$name(value)
}
}
impl From<$name> for Decimal {
fn from(value: $name) -> Self {
value.0
}
}
impl From<i64> for $name {
fn from(value: i64) -> Self {
$name(Decimal::from(value))
}
}
impl From<i32> for $name {
fn from(value: i32) -> Self {
$name(Decimal::from(value))
}
}
impl From<u32> for $name {
fn from(value: u32) -> Self {
$name(Decimal::from(value))
}
}
impl FromStr for $name {
type Err = rust_decimal::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
s.parse::<Decimal>()
.or_else(|_| Decimal::from_scientific(s))
.map($name)
}
}
impl TryFrom<f64> for $name {
type Error = rust_decimal::Error;
fn try_from(value: f64) -> Result<Self, Self::Error> {
Decimal::try_from(value).map($name)
}
}
};
}
decimal_newtype! {
DecimalNumber
}
decimal_newtype! {
AccountUnits
}
decimal_newtype! {
PriceValue
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct DateTime(pub String);
impl DateTime {
pub fn as_str(&self) -> &str {
&self.0
}
pub fn to_utc(&self) -> Option<chrono::DateTime<chrono::Utc>> {
let s = self.0.as_str();
if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(s) {
return Some(dt.with_timezone(&chrono::Utc));
}
let (secs, frac) = match s.split_once('.') {
Some((secs, frac)) => (secs, frac),
None => (s, ""),
};
let secs: i64 = secs.parse().ok()?;
let nanos: u32 = if frac.is_empty() {
0
} else if frac.len() <= 9 && frac.chars().all(|c| c.is_ascii_digit()) {
frac.parse::<u32>().ok()? * 10u32.pow(9 - frac.len() as u32)
} else {
return None;
};
chrono::DateTime::from_timestamp(secs, nanos)
}
}
impl fmt::Display for DateTime {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl From<String> for DateTime {
fn from(value: String) -> Self {
DateTime(value)
}
}
impl From<&str> for DateTime {
fn from(value: &str) -> Self {
DateTime(value.to_owned())
}
}
impl From<chrono::DateTime<chrono::Utc>> for DateTime {
fn from(value: chrono::DateTime<chrono::Utc>) -> Self {
DateTime(value.to_rfc3339_opts(chrono::SecondsFormat::Micros, true))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
pub enum AcceptDatetimeFormat {
#[serde(rename = "UNIX")]
Unix,
#[default]
#[serde(rename = "RFC3339")]
Rfc3339,
}
impl AcceptDatetimeFormat {
pub fn as_header_value(self) -> &'static str {
match self {
AcceptDatetimeFormat::Unix => "UNIX",
AcceptDatetimeFormat::Rfc3339 => "RFC3339",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decimal_newtype_serializes_as_string() {
let v = DecimalNumber::from_str("1.2345").unwrap();
assert_eq!(serde_json::to_string(&v).unwrap(), r#""1.2345""#);
}
#[test]
fn decimal_newtype_deserializes_from_number() {
let v: PriceValue = serde_json::from_str("1.1050").unwrap();
assert_eq!(v.to_string(), "1.105");
}
#[test]
fn decimal_newtype_roundtrips_string() {
let v: AccountUnits = serde_json::from_str(r#""-43.2100""#).unwrap();
assert_eq!(serde_json::to_string(&v).unwrap(), r#""-43.2100""#);
}
#[test]
fn datetime_parses_rfc3339() {
let dt = DateTime::from("2024-06-14T12:01:32.123456Z");
let parsed = dt.to_utc().unwrap();
assert_eq!(parsed.timestamp(), 1718366492);
assert_eq!(parsed.timestamp_subsec_micros(), 123456);
}
#[test]
fn datetime_parses_unix_with_fraction() {
let dt = DateTime::from("1718366492.123456000");
let parsed = dt.to_utc().unwrap();
assert_eq!(parsed.timestamp(), 1718366492);
assert_eq!(parsed.timestamp_subsec_micros(), 123456);
}
#[test]
fn datetime_parses_unix_without_fraction() {
let dt = DateTime::from("1718366492");
assert_eq!(dt.to_utc().unwrap().timestamp(), 1718366492);
}
#[test]
fn datetime_rejects_garbage() {
assert!(DateTime::from("not a date").to_utc().is_none());
}
#[test]
fn order_specifier_from_client_id() {
assert_eq!(OrderSpecifier::from_client_id("my-id").as_str(), "@my-id");
assert_eq!(OrderSpecifier::from(OrderId::from("42")).as_str(), "42");
}
}