Skip to main content

brokapi_core/
cid.rs

1//! The client order id codec: the mapping between the library's [`ClientOrderId`] and the form a broker accepts.
2//!
3//! You generate a [`ClientOrderId`] (a UUID v4) for every order. A broker may take a different form
4//! in its own field: a shorter string, other characters. A [`CidCodec`] is the pure function pair
5//! that converts one to the other, so an order seen in a broker's answer or stream can be tied back
6//! to your cid. A connector owns its codec. Most callers never use this module directly. It is public
7//! for connector authors and for tools that read a broker's raw data.
8
9use thiserror::Error;
10use uuid::Uuid;
11
12use crate::types::ClientOrderId;
13
14/// The form of a client order id as the broker sees it: a string in the broker's own field.
15///
16/// Produced by [`CidCodec::encode`] and read back by [`CidCodec::decode`]. The content depends on
17/// the broker and may carry a prefix or a changed alphabet, so treat it as opaque.
18///
19/// # Examples
20///
21/// ```
22/// use brokapi_core::NativeCid;
23///
24/// let n = NativeCid::new("abc");
25/// assert_eq!(n.as_str(), "abc");
26/// ```
27#[derive(Debug, Clone, PartialEq, Eq, Hash)]
28pub struct NativeCid(pub String);
29
30impl NativeCid {
31    /// Wraps a string. Nothing is checked here: [`CidCodec::encode`] makes the checks.
32    pub fn new(value: impl Into<String>) -> Self {
33        Self(value.into())
34    }
35
36    /// The string as the broker sees it.
37    pub fn as_str(&self) -> &str {
38        &self.0
39    }
40}
41
42/// The characters a broker accepts in its client order id field.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum Charset {
45    /// Digits `0-9` only.
46    Numeric,
47    /// Digits and capital letters `A-Z`.
48    AlnumUpper,
49    /// Any printable ASCII.
50    Ascii,
51    /// Digits and lowercase `a-f`.
52    HexLower,
53}
54
55/// What the broker's client order id field can hold, declared by a [`CidCodec`].
56///
57/// The connector checks it at start. If the field cannot carry a UUID without loss, that is a
58/// configuration error at start ([`Error::Config`](crate::Error::Config)), never a degradation at run
59/// time: an order whose cid cannot be read back cannot be tied to your ledger.
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub struct CidCaps {
62    /// Longest native string the broker's field accepts, in characters.
63    pub max_len: usize,
64    /// Characters the field accepts.
65    pub charset: Charset,
66    /// How many bits of the UUID the broker field can keep. 128 means the whole UUID.
67    pub native_bits: u32,
68    /// `true` when [`CidCodec::decode`] always gives back what [`CidCodec::encode`] was given.
69    pub lossless: bool,
70}
71
72/// Why a [`CidCodec`] cannot encode or accept a cid.
73///
74/// These are setup problems of a codec, found by the connector at start, so they are reported as
75/// [`Error::Config`](crate::Error::Config).
76#[derive(Error, Debug, Clone, PartialEq, Eq)]
77pub enum CidError {
78    /// The encoded string is longer than the broker's field.
79    #[error("native cid too long: {len} > {max}")]
80    TooLong {
81        /// Length of the encoded string.
82        len: usize,
83        /// Longest string the field takes, see [`CidCaps::max_len`].
84        max: usize,
85    },
86    /// The encoded string has a character the broker's field does not accept.
87    #[error("invalid character {ch:?} for charset {charset:?}")]
88    InvalidChar {
89        /// The offending character.
90        ch: char,
91        /// The set the field accepts.
92        charset: Charset,
93    },
94    /// The codec cannot give the UUID back from the native form, but the connector needs that.
95    #[error("lossless decode impossible for this codec")]
96    NotLossless,
97}
98
99/// A pure, stateless mapping between [`ClientOrderId`] and the broker's [`NativeCid`].
100///
101/// Rules for an implementation:
102/// - `encode` is a pure function: the same cid always gives the same string, with no state and no
103///   network.
104/// - `decode(encode(cid))` gives `Some(cid)` for every cid (lossless). A broker whose field cannot
105///   keep a UUID needs another carrier field, not a lossy codec.
106/// - `decode` gives `None` for a string that this codec did not make (for example the cid of an
107///   order placed from the broker's app).
108///
109/// # Examples
110///
111/// ```
112/// use brokapi_core::{CidCodec, ClientOrderId, HexUuidCodec};
113///
114/// let codec = HexUuidCodec;
115/// let cid = ClientOrderId::new_v4();
116/// let native = codec.encode(cid).unwrap();
117/// assert_eq!(native.as_str().len(), 32);
118/// assert_eq!(codec.decode(&native), Some(cid));
119/// ```
120pub trait CidCodec: Send + Sync {
121    /// What the broker's field can hold. Checked by the connector at start.
122    fn caps(&self) -> CidCaps;
123
124    /// The broker form of `cid`.
125    ///
126    /// # Errors
127    ///
128    /// [`CidError::TooLong`] or [`CidError::InvalidChar`] when the result does not fit the field
129    /// described by [`caps`](Self::caps). A correct codec never returns them for a UUID.
130    fn encode(&self, cid: ClientOrderId) -> Result<NativeCid, CidError>;
131
132    /// The cid behind a native string, or `None` if this codec did not make that string.
133    fn decode(&self, native: &NativeCid) -> Option<ClientOrderId>;
134}
135
136/// A lossless codec: the UUID as 32 lowercase hex digits, no hyphens.
137///
138/// Meant for tests and for brokers whose field holds at least 128 bits. [`decode`](CidCodec::decode)
139/// is more tolerant than `encode`: it also reads the hyphenated form and capital letters.
140///
141/// # Examples
142///
143/// ```
144/// use brokapi_core::{CidCodec, Charset, HexUuidCodec};
145///
146/// let caps = HexUuidCodec.caps();
147/// assert!(caps.lossless);
148/// assert_eq!(caps.max_len, 32);
149/// assert_eq!(caps.charset, Charset::HexLower);
150/// ```
151#[derive(Debug, Clone, Copy, Default)]
152pub struct HexUuidCodec;
153
154impl CidCodec for HexUuidCodec {
155    fn caps(&self) -> CidCaps {
156        CidCaps {
157            max_len: 32,
158            charset: Charset::HexLower,
159            native_bits: 128,
160            lossless: true,
161        }
162    }
163
164    fn encode(&self, cid: ClientOrderId) -> Result<NativeCid, CidError> {
165        Ok(NativeCid(cid.as_simple().to_string()))
166    }
167
168    fn decode(&self, native: &NativeCid) -> Option<ClientOrderId> {
169        Uuid::parse_str(native.as_str()).ok()
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use proptest::prelude::*;
176
177    use super::*;
178
179    prop_compose! {
180        fn arb_client_order_id()(bytes in any::<[u8; 16]>()) -> ClientOrderId {
181            ClientOrderId::from_bytes(bytes)
182        }
183    }
184
185    proptest! {
186        #[test]
187        fn hex_uuid_roundtrip(cid in arb_client_order_id()) {
188            let codec = HexUuidCodec;
189            let native = codec.encode(cid).expect("encode");
190            let decoded = codec.decode(&native).expect("decode");
191            prop_assert_eq!(decoded, cid);
192        }
193    }
194}