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}