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