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