brokapi_tbank/cid.rs
1//! T-Bank client order id codec
2//!
3//! `PostOrderRequest.order_id` is the idempotency key: "в формате UID, максимальная длина 36 символов"
4//! (orders.proto). A hyphenated UUID is exactly 36 characters, so the cid fits without loss.
5//! The same value comes back as `order_request_id` in `PostOrderResponse`, `OrderState` and
6//! `OrderStateStream`, and `CancelOrder` / `GetOrderState` accept it with `ORDER_ID_TYPE_REQUEST`.
7
8use brokapi_core::{Charset, CidCaps, CidCodec, CidError, ClientOrderId, NativeCid};
9use uuid::Uuid;
10
11/// The client order id codec of T-Bank: the cid is sent as a hyphenated UUID.
12///
13/// A hyphenated UUID is exactly 36 characters, which is the limit of `PostOrderRequest.order_id`, so
14/// the cid fits without loss and comes back unchanged in `order_request_id`. The connectors use this
15/// codec for you. Use it yourself when you read broker messages outside the connector (for example
16/// through [`TbankRaw`](crate::TbankRaw)) and want to know whether an order is yours.
17///
18/// # Examples
19///
20/// ```
21/// use brokapi_core::{CidCodec, ClientOrderId, NativeCid};
22/// use brokapi_tbank::TbankCidCodec;
23///
24/// let codec = TbankCidCodec;
25/// assert_eq!(codec.caps().max_len, 36);
26/// assert!(codec.caps().lossless);
27///
28/// let cid = ClientOrderId::new_v4();
29/// let native = codec.encode(cid).expect("a UUID always fits");
30/// assert_eq!(native.as_str().len(), 36);
31/// assert_eq!(codec.decode(&native), Some(cid));
32///
33/// // An id that is not a UUID was not placed by this library.
34/// assert_eq!(codec.decode(&NativeCid::new("placed-by-hand")), None);
35/// ```
36#[derive(Debug, Clone, Copy, Default)]
37pub struct TbankCidCodec;
38
39impl CidCodec for TbankCidCodec {
40 fn caps(&self) -> CidCaps {
41 CidCaps {
42 max_len: 36,
43 charset: Charset::Ascii,
44 native_bits: 128,
45 lossless: true,
46 }
47 }
48
49 fn encode(&self, cid: ClientOrderId) -> Result<NativeCid, CidError> {
50 let s = cid.hyphenated().to_string();
51 let max = self.caps().max_len;
52 if s.len() > max {
53 return Err(CidError::TooLong { len: s.len(), max });
54 }
55 Ok(NativeCid::new(s))
56 }
57
58 fn decode(&self, native: &NativeCid) -> Option<ClientOrderId> {
59 Uuid::parse_str(native.as_str()).ok()
60 }
61}
62
63/// `order_request_id` of a broker message -> our cid, if it is one.
64///
65/// Returns `None` for an empty string or anything that is not a UUID: the order was placed
66/// elsewhere (phone, another program) and carries no cid of ours.
67///
68/// # Examples
69///
70/// ```
71/// use brokapi_tbank::cid::parse_request_id;
72///
73/// let cid = parse_request_id("67e55044-10b1-426f-9247-bb680e5fe0c8");
74/// assert!(cid.is_some());
75/// assert!(parse_request_id("").is_none());
76/// ```
77pub fn parse_request_id(raw: &str) -> Option<ClientOrderId> {
78 TbankCidCodec.decode(&NativeCid::new(raw))
79}
80
81/// Stable stand-in cid for an order that was not placed by us (phone, another robot): the broker gives
82/// no `order_request_id`, but `ReconcileReport::foreign` needs a non-nil cid. Derived from the exchange
83/// order id (FNV-1a), never produced by `Uuid::new_v4`.
84///
85/// The same exchange id always gives the same value, so repeated reports of one foreign order line
86/// up. The value is not a key you can send back to the broker: the broker does not know it.
87///
88/// # Examples
89///
90/// ```
91/// use brokapi_tbank::cid::synthetic_cid;
92///
93/// assert_eq!(synthetic_cid("EX1"), synthetic_cid("EX1"));
94/// assert_ne!(synthetic_cid("EX1"), synthetic_cid("EX2"));
95/// ```
96pub fn synthetic_cid(exchange_order_id: &str) -> ClientOrderId {
97 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
98 for b in exchange_order_id.bytes() {
99 h ^= u64::from(b);
100 h = h.wrapping_mul(0x0000_0100_0000_01b3);
101 }
102 Uuid::from_u64_pair(0x7462_616e_6b2d_666f, h)
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use proptest::prelude::*;
109
110 proptest! {
111 #[test]
112 fn tbank_cid_roundtrip(bytes in any::<[u8; 16]>()) {
113 let cid = ClientOrderId::from_bytes(bytes);
114 let codec = TbankCidCodec;
115 let native = codec.encode(cid).expect("encode");
116 prop_assert!(native.as_str().len() <= 36);
117 prop_assert_eq!(codec.decode(&native), Some(cid));
118 }
119 }
120
121 #[test]
122 fn synthetic_is_stable_and_distinct() {
123 assert_eq!(synthetic_cid("123"), synthetic_cid("123"));
124 assert_ne!(synthetic_cid("123"), synthetic_cid("124"));
125 assert!(!synthetic_cid("123").is_nil());
126 }
127
128 #[test]
129 fn foreign_request_id_is_not_a_cid() {
130 assert_eq!(parse_request_id(""), None);
131 assert_eq!(parse_request_id("not-a-uuid"), None);
132 }
133}