Skip to main content

rust_okx/model/
mod.rs

1//! Shared data types: the [`NumberString`] wrapper, the response envelope, and
2//! the common string enums used across the API modules.
3
4use std::fmt;
5use std::str::FromStr;
6
7use serde::{Deserialize, Serialize};
8
9#[derive(Serialize)]
10pub(crate) struct EmptyRequest {}
11
12/// Deserialize an OKX array field that may be encoded as an empty string when
13/// no entries are available.
14///
15/// A few OKX REST endpoints document these fields as arrays but return `""`
16/// for accounts/currencies without data. Treat only the empty string and
17/// `null` as an empty vector; non-empty strings remain decode errors.
18pub(crate) fn deserialize_vec_or_empty_string<'de, D, T>(
19    deserializer: D,
20) -> Result<Vec<T>, D::Error>
21where
22    D: serde::Deserializer<'de>,
23    T: Deserialize<'de>,
24{
25    #[derive(Deserialize)]
26    #[serde(untagged)]
27    enum WireValue<T> {
28        Sequence(Vec<T>),
29        String(String),
30        Null(()),
31    }
32
33    match WireValue::<T>::deserialize(deserializer)? {
34        WireValue::Sequence(values) => Ok(values),
35        WireValue::String(value) if value.is_empty() => Ok(Vec::new()),
36        WireValue::String(value) => Err(serde::de::Error::custom(format!(
37            "expected an array or empty string, got {value:?}"
38        ))),
39        WireValue::Null(()) => Ok(Vec::new()),
40    }
41}
42
43/// The OKX response envelope: `{ "code": "...", "msg": "...", "data": [...] }`.
44///
45/// Internal — the client unwraps it and returns `data` (or an [`Error::Api`]).
46///
47/// [`Error::Api`]: crate::Error::Api
48#[derive(Debug, Clone, Deserialize)]
49pub(crate) struct OkxResponse<D> {
50    pub code: String,
51    pub msg: String,
52    pub data: D,
53}
54
55/// A numeric value returned by OKX as a JSON string.
56///
57/// OKX encodes all prices, sizes, and balances as strings to avoid floating
58/// point precision loss. `NumberString` preserves the exact wire representation
59/// and lets the caller decide how to interpret it:
60///
61/// ```
62/// use rust_okx::NumberString;
63/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
64/// let px = NumberString::from("42000.1");
65/// assert_eq!(px.as_str(), "42000.1");
66/// let as_f64: f64 = px.parse()?;
67/// assert_eq!(as_f64, 42000.1);
68/// # Ok(())
69/// # }
70/// ```
71#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
72#[serde(transparent)]
73pub struct NumberString(String);
74
75impl NumberString {
76    /// Borrow the raw string value.
77    pub fn as_str(&self) -> &str {
78        &self.0
79    }
80
81    /// Returns `true` if the value is the empty string (OKX uses `""` for
82    /// "not applicable" fields).
83    pub fn is_empty(&self) -> bool {
84        self.0.is_empty()
85    }
86
87    /// Parse the value into any [`FromStr`] type, e.g. `f64`, `i64`, or
88    /// `rust_decimal::Decimal`.
89    pub fn parse<T: FromStr>(&self) -> Result<T, T::Err> {
90        self.0.parse()
91    }
92
93    /// Consume the wrapper and return the inner [`String`].
94    pub fn into_string(self) -> String {
95        self.0
96    }
97
98    /// Parse the value as a [`rust_decimal::Decimal`].
99    #[cfg(feature = "rust-decimal")]
100    pub fn to_decimal(&self) -> Result<rust_decimal::Decimal, rust_decimal::Error> {
101        self.0.parse()
102    }
103}
104
105impl From<String> for NumberString {
106    fn from(s: String) -> Self {
107        NumberString(s)
108    }
109}
110
111impl From<&str> for NumberString {
112    fn from(s: &str) -> Self {
113        NumberString(s.to_owned())
114    }
115}
116
117impl AsRef<str> for NumberString {
118    fn as_ref(&self) -> &str {
119        &self.0
120    }
121}
122
123impl fmt::Display for NumberString {
124    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125        f.write_str(&self.0)
126    }
127}
128
129/// Defines a string-backed enum that round-trips through the OKX wire format and
130/// tolerates unknown values via an `Unknown(String)` fallback variant. This
131/// keeps response deserialization non-breaking when OKX adds new values.
132macro_rules! string_enum {
133    (
134        $(#[$meta:meta])*
135        $vis:vis enum $name:ident {
136            $( $(#[$vmeta:meta])* $variant:ident = $wire:literal ),* $(,)?
137        }
138    ) => {
139        $(#[$meta])*
140        #[derive(Debug, Clone, PartialEq, Eq, Hash)]
141        #[non_exhaustive]
142        $vis enum $name {
143            $( $(#[$vmeta])* $variant, )*
144            /// A value not modeled by this version of the crate; the raw string
145            /// is preserved.
146            Unknown(String),
147        }
148
149        impl $name {
150            /// The OKX wire representation of this value.
151            pub fn as_str(&self) -> &str {
152                match self {
153                    $( $name::$variant => $wire, )*
154                    $name::Unknown(s) => s.as_str(),
155                }
156            }
157        }
158
159        impl ::core::convert::From<&str> for $name {
160            fn from(s: &str) -> Self {
161                match s {
162                    $( $wire => $name::$variant, )*
163                    other => $name::Unknown(other.to_owned()),
164                }
165            }
166        }
167
168        impl ::core::fmt::Display for $name {
169            fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
170                f.write_str(self.as_str())
171            }
172        }
173
174        impl ::serde::Serialize for $name {
175            fn serialize<S: ::serde::Serializer>(&self, ser: S) -> ::core::result::Result<S::Ok, S::Error> {
176                ser.serialize_str(self.as_str())
177            }
178        }
179
180        impl<'de> ::serde::Deserialize<'de> for $name {
181            fn deserialize<D: ::serde::Deserializer<'de>>(de: D) -> ::core::result::Result<Self, D::Error> {
182                let s = <::std::string::String as ::serde::Deserialize>::deserialize(de)?;
183                ::core::result::Result::Ok($name::from(s.as_str()))
184            }
185        }
186    };
187}
188
189string_enum! {
190    /// Instrument type.
191    pub enum InstType {
192        /// Spot.
193        Spot = "SPOT",
194        /// Margin.
195        Margin = "MARGIN",
196        /// Perpetual Futures.
197        Swap = "SWAP",
198        /// Expiry Futures.
199        Futures = "FUTURES",
200        /// Option.
201        Option = "OPTION",
202        /// Event Contracts
203        Events = "EVENTS",
204    }
205}
206
207string_enum! {
208    /// Order side.
209    pub enum OrderSide {
210        /// Buy.
211        Buy = "buy",
212        /// Sell.
213        Sell = "sell",
214    }
215}
216
217string_enum! {
218    /// Order type.
219    pub enum OrderType {
220        /// Market order.
221        Market = "market",
222        /// Limit order.
223        Limit = "limit",
224        /// Post-only order.
225        PostOnly = "post_only",
226        /// Fill-or-kill order.
227        Fok = "fok",
228        /// Immediate-or-cancel order.
229        Ioc = "ioc",
230        /// Market order with immediate-or-cancel (futures/swap).
231        OptimalLimitIoc = "optimal_limit_ioc",
232    }
233}
234
235string_enum! {
236    /// Trade (margin) mode.
237    pub enum TradeMode {
238        /// Non-margin (cash).
239        Cash = "cash",
240        /// Cross margin.
241        Cross = "cross",
242        /// Isolated margin.
243        Isolated = "isolated",
244        /// Spot isolated margin mode.
245        SpotIsolated = "spot_isolated",
246    }
247}
248
249string_enum! {
250    /// Position side.
251    pub enum PositionSide {
252        /// Long position.
253        Long = "long",
254        /// Short position.
255        Short = "short",
256        /// Net position.
257        Net = "net",
258    }
259}
260
261string_enum! {
262    /// Order lifecycle state.
263    pub enum OrderState {
264        /// Resting on the book.
265        Live = "live",
266        /// Partially filled.
267        PartiallyFilled = "partially_filled",
268        /// Fully filled.
269        Filled = "filled",
270        /// Canceled.
271        Canceled = "canceled",
272        /// Canceled by market maker protection.
273        MmpCanceled = "mmp_canceled",
274    }
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280
281    #[test]
282    fn number_string_parses_and_preserves() {
283        let n = NumberString::from("1.005");
284        assert_eq!(n.as_str(), "1.005");
285        assert_eq!(n.parse::<f64>().unwrap(), 1.005);
286        assert_eq!(n.into_string(), "1.005");
287    }
288
289    #[test]
290    fn known_enum_value_round_trips() {
291        let v: InstType = serde_json::from_str("\"SWAP\"").unwrap();
292        assert_eq!(v, InstType::Swap);
293        assert_eq!(serde_json::to_string(&v).unwrap(), "\"SWAP\"");
294    }
295
296    #[test]
297    fn unknown_enum_value_is_preserved_not_an_error() {
298        let v: InstType = serde_json::from_str("\"FUTURE_THING\"").unwrap();
299        assert_eq!(v, InstType::Unknown("FUTURE_THING".to_owned()));
300        // And serializes back to the original wire value.
301        assert_eq!(serde_json::to_string(&v).unwrap(), "\"FUTURE_THING\"");
302    }
303}