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::ser::{SerializeMap, SerializeSeq};
8use serde::{Deserialize, Serialize};
9
10/// Deserialize an OKX array field that may be encoded as an empty string when
11/// no entries are available.
12///
13/// A few OKX REST endpoints document these fields as arrays but return `""`
14/// for accounts/currencies without data. Treat only the empty string and
15/// `null` as an empty vector; non-empty strings remain decode errors.
16pub(crate) fn deserialize_vec_or_empty_string<'de, D, T>(
17    deserializer: D,
18) -> Result<Vec<T>, D::Error>
19where
20    D: serde::Deserializer<'de>,
21    T: Deserialize<'de>,
22{
23    #[derive(Deserialize)]
24    #[serde(untagged)]
25    enum WireValue<T> {
26        Sequence(Vec<T>),
27        String(String),
28        Null(()),
29    }
30
31    match WireValue::<T>::deserialize(deserializer)? {
32        WireValue::Sequence(values) => Ok(values),
33        WireValue::String(value) if value.is_empty() => Ok(Vec::new()),
34        WireValue::String(value) => Err(serde::de::Error::custom(format!(
35            "expected an array or empty string, got {value:?}"
36        ))),
37        WireValue::Null(()) => Ok(Vec::new()),
38    }
39}
40
41mod validation;
42
43pub use validation::{RequestValidationError, ValidateRequest};
44pub(crate) use validation::{
45    at_least_one, collection_length, decimal_string_range, exactly_one, length_range, max_length,
46    non_empty, non_empty_items, non_negative_decimal_string, one_of, optional_non_empty,
47    optional_one_of, optional_positive_decimal_string, optional_unsigned_integer_string,
48    positive_decimal_string, positive_unsigned_integer_string, range_u64, reject_when_present,
49    require_when, validate_client_request_id, validate_side,
50};
51
52/// The OKX response envelope: `{ "code": "...", "msg": "...", "data": [...] }`.
53///
54/// Internal — the client unwraps it and returns `data` (or an [`Error::Api`]).
55///
56/// [`Error::Api`]: crate::Error::Api
57#[derive(Debug, Clone, Deserialize)]
58pub(crate) struct OkxResponse<D> {
59    pub code: String,
60    pub msg: String,
61    pub data: D,
62}
63
64/// A numeric value returned by OKX as a JSON string.
65///
66/// OKX encodes all prices, sizes, and balances as strings to avoid floating
67/// point precision loss. `NumberString` preserves the exact wire representation
68/// and lets the caller decide how to interpret it:
69///
70/// ```
71/// use rust_okx::NumberString;
72/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
73/// let px = NumberString::from("42000.1");
74/// assert_eq!(px.as_str(), "42000.1");
75/// let as_f64: f64 = px.parse()?;
76/// assert_eq!(as_f64, 42000.1);
77/// # Ok(())
78/// # }
79/// ```
80#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
81#[serde(transparent)]
82pub struct NumberString(String);
83
84impl NumberString {
85    /// Borrow the raw string value.
86    pub fn as_str(&self) -> &str {
87        &self.0
88    }
89
90    /// Returns `true` if the value is the empty string (OKX uses `""` for
91    /// "not applicable" fields).
92    pub fn is_empty(&self) -> bool {
93        self.0.is_empty()
94    }
95
96    /// Parse the value into any [`FromStr`] type, e.g. `f64`, `i64`, or
97    /// `rust_decimal::Decimal`.
98    pub fn parse<T: FromStr>(&self) -> Result<T, T::Err> {
99        self.0.parse()
100    }
101
102    /// Consume the wrapper and return the inner [`String`].
103    pub fn into_string(self) -> String {
104        self.0
105    }
106
107    /// Parse the value as a [`rust_decimal::Decimal`].
108    #[cfg(feature = "rust-decimal")]
109    pub fn to_decimal(&self) -> Result<rust_decimal::Decimal, rust_decimal::Error> {
110        self.0.parse()
111    }
112}
113
114impl From<String> for NumberString {
115    fn from(s: String) -> Self {
116        NumberString(s)
117    }
118}
119
120impl From<&str> for NumberString {
121    fn from(s: &str) -> Self {
122        NumberString(s.to_owned())
123    }
124}
125
126impl AsRef<str> for NumberString {
127    fn as_ref(&self) -> &str {
128        &self.0
129    }
130}
131
132impl fmt::Display for NumberString {
133    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134        f.write_str(&self.0)
135    }
136}
137
138/// A flexible, untyped request-parameter builder for unsupported or newly
139/// introduced OKX fields.
140///
141/// Prefer endpoint-specific request types whenever one exists. Adding the same
142/// key more than once replaces its previous value while preserving insertion
143/// order, preventing duplicate JSON object keys.
144#[derive(Debug, Clone, Default)]
145pub struct RawRequestParams {
146    fields: Vec<(String, ParamValue)>,
147}
148
149impl RawRequestParams {
150    /// Create an empty parameter set.
151    pub fn new() -> Self {
152        Self::default()
153    }
154
155    /// Add or replace a string parameter.
156    pub fn param(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
157        self.set(key.into(), ParamValue::String(value.into()));
158        self
159    }
160
161    /// Add or replace a boolean parameter.
162    pub fn bool_param(mut self, key: impl Into<String>, value: bool) -> Self {
163        self.set(key.into(), ParamValue::Bool(value));
164        self
165    }
166
167    /// Add or replace an array-of-strings parameter.
168    pub fn string_list<I, S>(mut self, key: impl Into<String>, values: I) -> Self
169    where
170        I: IntoIterator<Item = S>,
171        S: Into<String>,
172    {
173        self.set(
174            key.into(),
175            ParamValue::StringList(values.into_iter().map(Into::into).collect()),
176        );
177        self
178    }
179
180    /// Return true when no parameters are set.
181    pub fn is_empty(&self) -> bool {
182        self.fields.is_empty()
183    }
184
185    fn set(&mut self, key: String, value: ParamValue) {
186        if let Some((_, existing)) = self.fields.iter_mut().find(|(name, _)| name == &key) {
187            *existing = value;
188        } else {
189            self.fields.push((key, value));
190        }
191    }
192}
193
194impl Serialize for RawRequestParams {
195    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
196        let mut map = serializer.serialize_map(Some(self.fields.len()))?;
197        for (key, value) in &self.fields {
198            map.serialize_entry(key, value)?;
199        }
200        map.end()
201    }
202}
203
204/// Backward-compatible name for [`RawRequestParams`].
205///
206/// New endpoint implementations should expose typed request structs instead of
207/// accepting this alias directly.
208pub type RequestParams = RawRequestParams;
209
210#[derive(Debug, Clone)]
211enum ParamValue {
212    String(String),
213    Bool(bool),
214    StringList(Vec<String>),
215}
216
217impl Serialize for ParamValue {
218    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
219        match self {
220            Self::String(value) => serializer.serialize_str(value),
221            Self::Bool(value) => serializer.serialize_bool(*value),
222            Self::StringList(values) => {
223                let mut seq = serializer.serialize_seq(Some(values.len()))?;
224                for value in values {
225                    seq.serialize_element(value)?;
226                }
227                seq.end()
228            }
229        }
230    }
231}
232
233/// A broad OKX response row for low-frequency endpoints.
234///
235/// OKX edge endpoints often return sparse, feature-dependent objects. Fields
236/// default when absent so deserialization remains forward-compatible.
237#[derive(Debug, Clone, Default, Deserialize)]
238#[serde(rename_all = "camelCase")]
239#[non_exhaustive]
240pub struct RestRow {
241    /// Instrument type.
242    #[serde(default, rename = "instType")]
243    pub inst_type: String,
244    /// Instrument ID.
245    #[serde(default, rename = "instId")]
246    pub inst_id: String,
247    /// Instrument family.
248    #[serde(default, rename = "instFamily")]
249    pub inst_family: String,
250    /// Currency code.
251    #[serde(default)]
252    pub ccy: String,
253    /// Order ID.
254    #[serde(default, rename = "ordId")]
255    pub ord_id: String,
256    /// Client order ID.
257    #[serde(default, rename = "clOrdId")]
258    pub cl_ord_id: String,
259    /// Algo order ID.
260    #[serde(default, rename = "algoId")]
261    pub algo_id: String,
262    /// Client algo order ID.
263    #[serde(default, rename = "algoClOrdId")]
264    pub algo_cl_ord_id: String,
265    /// Quote ID.
266    #[serde(default, rename = "quoteId")]
267    pub quote_id: String,
268    /// Request ID.
269    #[serde(default, rename = "reqId")]
270    pub req_id: String,
271    /// Product ID.
272    #[serde(default, rename = "productId")]
273    pub product_id: String,
274    /// Operation type.
275    #[serde(default, rename = "type")]
276    pub row_type: String,
277    /// State or status.
278    #[serde(default)]
279    pub state: String,
280    /// Status.
281    #[serde(default)]
282    pub status: String,
283    /// Side.
284    #[serde(default)]
285    pub side: String,
286    /// Amount.
287    #[serde(default)]
288    pub amt: NumberString,
289    /// Size.
290    #[serde(default)]
291    pub sz: NumberString,
292    /// Price.
293    #[serde(default)]
294    pub px: NumberString,
295    /// Rate.
296    #[serde(default)]
297    pub rate: NumberString,
298    /// Balance.
299    #[serde(default)]
300    pub bal: NumberString,
301    /// Available balance.
302    #[serde(default)]
303    pub avail_bal: NumberString,
304    /// Timestamp.
305    #[serde(default)]
306    pub ts: NumberString,
307    /// Success code.
308    #[serde(default, rename = "sCode")]
309    pub s_code: String,
310    /// Success message.
311    #[serde(default, rename = "sMsg")]
312    pub s_msg: String,
313}
314
315/// Defines a string-backed enum that round-trips through the OKX wire format and
316/// tolerates unknown values via an `Unknown(String)` fallback variant. This
317/// keeps response deserialization non-breaking when OKX adds new values.
318macro_rules! string_enum {
319    (
320        $(#[$meta:meta])*
321        $vis:vis enum $name:ident {
322            $( $(#[$vmeta:meta])* $variant:ident = $wire:literal ),* $(,)?
323        }
324    ) => {
325        $(#[$meta])*
326        #[derive(Debug, Clone, PartialEq, Eq, Hash)]
327        #[non_exhaustive]
328        $vis enum $name {
329            $( $(#[$vmeta])* $variant, )*
330            /// A value not modeled by this version of the crate; the raw string
331            /// is preserved.
332            Unknown(String),
333        }
334
335        impl $name {
336            /// The OKX wire representation of this value.
337            pub fn as_str(&self) -> &str {
338                match self {
339                    $( $name::$variant => $wire, )*
340                    $name::Unknown(s) => s.as_str(),
341                }
342            }
343        }
344
345        impl ::core::convert::From<&str> for $name {
346            fn from(s: &str) -> Self {
347                match s {
348                    $( $wire => $name::$variant, )*
349                    other => $name::Unknown(other.to_owned()),
350                }
351            }
352        }
353
354        impl ::core::fmt::Display for $name {
355            fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
356                f.write_str(self.as_str())
357            }
358        }
359
360        impl ::serde::Serialize for $name {
361            fn serialize<S: ::serde::Serializer>(&self, ser: S) -> ::core::result::Result<S::Ok, S::Error> {
362                ser.serialize_str(self.as_str())
363            }
364        }
365
366        impl<'de> ::serde::Deserialize<'de> for $name {
367            fn deserialize<D: ::serde::Deserializer<'de>>(de: D) -> ::core::result::Result<Self, D::Error> {
368                let s = <::std::string::String as ::serde::Deserialize>::deserialize(de)?;
369                ::core::result::Result::Ok($name::from(s.as_str()))
370            }
371        }
372    };
373}
374
375string_enum! {
376    /// Instrument type.
377    pub enum InstType {
378        /// Spot.
379        Spot = "SPOT",
380        /// Margin.
381        Margin = "MARGIN",
382        /// Perpetual Futures.
383        Swap = "SWAP",
384        /// Expiry Futures.
385        Futures = "FUTURES",
386        /// Option.
387        Option = "OPTION",
388        /// Event Contracts
389        Events = "EVENTS",
390    }
391}
392
393string_enum! {
394    /// Order side.
395    pub enum OrderSide {
396        /// Buy.
397        Buy = "buy",
398        /// Sell.
399        Sell = "sell",
400    }
401}
402
403string_enum! {
404    /// Order type.
405    pub enum OrderType {
406        /// Market order.
407        Market = "market",
408        /// Limit order.
409        Limit = "limit",
410        /// Post-only order.
411        PostOnly = "post_only",
412        /// Fill-or-kill order.
413        Fok = "fok",
414        /// Immediate-or-cancel order.
415        Ioc = "ioc",
416        /// Market order with immediate-or-cancel (futures/swap).
417        OptimalLimitIoc = "optimal_limit_ioc",
418    }
419}
420
421string_enum! {
422    /// Trade (margin) mode.
423    pub enum TradeMode {
424        /// Non-margin (cash).
425        Cash = "cash",
426        /// Cross margin.
427        Cross = "cross",
428        /// Isolated margin.
429        Isolated = "isolated",
430        /// Spot isolated margin mode.
431        SpotIsolated = "spot_isolated",
432    }
433}
434
435string_enum! {
436    /// Position side.
437    pub enum PositionSide {
438        /// Long position.
439        Long = "long",
440        /// Short position.
441        Short = "short",
442        /// Net position.
443        Net = "net",
444    }
445}
446
447string_enum! {
448    /// Order lifecycle state.
449    pub enum OrderState {
450        /// Resting on the book.
451        Live = "live",
452        /// Partially filled.
453        PartiallyFilled = "partially_filled",
454        /// Fully filled.
455        Filled = "filled",
456        /// Canceled.
457        Canceled = "canceled",
458        /// Canceled by market maker protection.
459        MmpCanceled = "mmp_canceled",
460    }
461}
462
463#[cfg(test)]
464mod tests {
465    use super::*;
466
467    #[test]
468    fn number_string_parses_and_preserves() {
469        let n = NumberString::from("1.005");
470        assert_eq!(n.as_str(), "1.005");
471        assert_eq!(n.parse::<f64>().unwrap(), 1.005);
472        assert_eq!(n.into_string(), "1.005");
473    }
474
475    #[test]
476    fn known_enum_value_round_trips() {
477        let v: InstType = serde_json::from_str("\"SWAP\"").unwrap();
478        assert_eq!(v, InstType::Swap);
479        assert_eq!(serde_json::to_string(&v).unwrap(), "\"SWAP\"");
480    }
481
482    #[test]
483    fn unknown_enum_value_is_preserved_not_an_error() {
484        let v: InstType = serde_json::from_str("\"FUTURE_THING\"").unwrap();
485        assert_eq!(v, InstType::Unknown("FUTURE_THING".to_owned()));
486        // And serializes back to the original wire value.
487        assert_eq!(serde_json::to_string(&v).unwrap(), "\"FUTURE_THING\"");
488    }
489
490    #[test]
491    fn raw_request_params_replace_duplicate_keys() {
492        let params = RawRequestParams::new()
493            .param("ccy", "BTC")
494            .param("ccy", "ETH");
495
496        assert_eq!(
497            serde_json::to_value(params).unwrap(),
498            serde_json::json!({"ccy": "ETH"})
499        );
500    }
501}