nautilus_hyperliquid/common/
parse.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use std::str::FromStr;
17
18use rust_decimal::Decimal;
19use serde::{Deserialize, Deserializer, Serializer};
20
21/// Serializes decimal as string (lossless, no scientific notation).
22pub fn serialize_decimal_as_str<S>(decimal: &Decimal, serializer: S) -> Result<S::Ok, S::Error>
23where
24    S: Serializer,
25{
26    serializer.serialize_str(&decimal.normalize().to_string())
27}
28
29/// Deserializes decimal from string only (reject numbers to avoid precision loss).
30pub fn deserialize_decimal_from_str<'de, D>(deserializer: D) -> Result<Decimal, D::Error>
31where
32    D: Deserializer<'de>,
33{
34    let s = String::deserialize(deserializer)?;
35    Decimal::from_str(&s).map_err(serde::de::Error::custom)
36}
37
38/// Serialize optional decimal as string
39pub fn serialize_optional_decimal_as_str<S>(
40    decimal: &Option<Decimal>,
41    serializer: S,
42) -> Result<S::Ok, S::Error>
43where
44    S: Serializer,
45{
46    match decimal {
47        Some(d) => serializer.serialize_str(&d.normalize().to_string()),
48        None => serializer.serialize_none(),
49    }
50}
51
52/// Deserialize optional decimal from string
53pub fn deserialize_optional_decimal_from_str<'de, D>(
54    deserializer: D,
55) -> Result<Option<Decimal>, D::Error>
56where
57    D: Deserializer<'de>,
58{
59    let opt = Option::<String>::deserialize(deserializer)?;
60    match opt {
61        Some(s) => {
62            let decimal = Decimal::from_str(&s).map_err(serde::de::Error::custom)?;
63            Ok(Some(decimal))
64        }
65        None => Ok(None),
66    }
67}
68
69////////////////////////////////////////////////////////////////////////////////
70// Tests
71////////////////////////////////////////////////////////////////////////////////
72
73#[cfg(test)]
74mod tests {
75    use rstest::rstest;
76    use serde::{Deserialize, Serialize};
77
78    use super::*;
79
80    #[derive(Serialize, Deserialize)]
81    struct TestStruct {
82        #[serde(
83            serialize_with = "serialize_decimal_as_str",
84            deserialize_with = "deserialize_decimal_from_str"
85        )]
86        value: Decimal,
87        #[serde(
88            serialize_with = "serialize_optional_decimal_as_str",
89            deserialize_with = "deserialize_optional_decimal_from_str"
90        )]
91        optional_value: Option<Decimal>,
92    }
93
94    #[rstest]
95    fn test_decimal_serialization_roundtrip() {
96        let original = TestStruct {
97            value: Decimal::from_str("123.456789012345678901234567890").unwrap(),
98            optional_value: Some(Decimal::from_str("0.000000001").unwrap()),
99        };
100
101        let json = serde_json::to_string(&original).unwrap();
102        println!("Serialized: {}", json);
103
104        // Check that it's serialized as strings (rust_decimal may normalize precision)
105        assert!(json.contains("\"123.45678901234567890123456789\""));
106        assert!(json.contains("\"0.000000001\""));
107
108        let deserialized: TestStruct = serde_json::from_str(&json).unwrap();
109        assert_eq!(original.value, deserialized.value);
110        assert_eq!(original.optional_value, deserialized.optional_value);
111    }
112
113    #[rstest]
114    fn test_decimal_precision_preservation() {
115        let test_cases = [
116            "0",
117            "1",
118            "0.1",
119            "0.01",
120            "0.001",
121            "123.456789012345678901234567890",
122            "999999999999999999.999999999999999999",
123        ];
124
125        for case in test_cases {
126            let decimal = Decimal::from_str(case).unwrap();
127            let test_struct = TestStruct {
128                value: decimal,
129                optional_value: Some(decimal),
130            };
131
132            let json = serde_json::to_string(&test_struct).unwrap();
133            let parsed: TestStruct = serde_json::from_str(&json).unwrap();
134
135            assert_eq!(decimal, parsed.value, "Failed for case: {}", case);
136            assert_eq!(
137                Some(decimal),
138                parsed.optional_value,
139                "Failed for case: {}",
140                case
141            );
142        }
143    }
144
145    #[rstest]
146    fn test_optional_none_handling() {
147        let test_struct = TestStruct {
148            value: Decimal::from_str("42.0").unwrap(),
149            optional_value: None,
150        };
151
152        let json = serde_json::to_string(&test_struct).unwrap();
153        assert!(json.contains("null"));
154
155        let parsed: TestStruct = serde_json::from_str(&json).unwrap();
156        assert_eq!(test_struct.value, parsed.value);
157        assert_eq!(None, parsed.optional_value);
158    }
159}