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