nautilus_model/identifiers/
trade_id.rs1use std::{
19 ffi::CStr,
20 fmt::{Debug, Display, Formatter},
21 hash::Hash,
22};
23
24use nautilus_core::correctness::{
25 FAILED, check_predicate_false, check_predicate_true, check_slice_not_empty,
26};
27use serde::{Deserialize, Deserializer, Serialize};
28
29pub const TRADE_ID_LEN: usize = 37;
31
32#[repr(C)]
41#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
42#[cfg_attr(
43 feature = "python",
44 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
45)]
46pub struct TradeId {
47 pub(crate) value: [u8; TRADE_ID_LEN],
49}
50
51impl TradeId {
52 pub fn new_checked<T: AsRef<str>>(value: T) -> anyhow::Result<Self> {
66 Self::from_bytes(value.as_ref().as_bytes())
67 }
68
69 pub fn new<T: AsRef<str>>(value: T) -> Self {
79 Self::new_checked(value).expect(FAILED)
80 }
81
82 pub fn from_bytes(value: &[u8]) -> anyhow::Result<Self> {
98 check_slice_not_empty(value, "value")?;
99
100 let mut last_byte = 0;
102 let all_ascii = value
103 .iter()
104 .inspect(|&&b| last_byte = b)
105 .all(|&b| b.is_ascii());
106
107 check_predicate_true(all_ascii, "'value' contains non-ASCII characters")?;
108 check_predicate_false(
109 value.len() == 1 && last_byte == 0,
110 "'value' was single null byte",
111 )?;
112 check_predicate_true(
113 value.len() <= 36 || (value.len() == 37 && last_byte == 0),
114 "'value' exceeds max length or invalid format",
115 )?;
116
117 let mut buf = [0; TRADE_ID_LEN];
118 buf[..value.len()].copy_from_slice(value);
119
120 Ok(Self { value: buf })
121 }
122
123 #[must_use]
129 pub fn as_cstr(&self) -> &CStr {
130 CStr::from_bytes_until_nul(&self.value).unwrap()
133 }
134}
135
136impl Debug for TradeId {
137 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
138 write!(f, "{}('{}')", stringify!(TradeId), self)
139 }
140}
141
142impl Display for TradeId {
143 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
144 write!(f, "{}", self.as_cstr().to_str().unwrap())
145 }
146}
147
148impl Serialize for TradeId {
149 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
150 where
151 S: serde::Serializer,
152 {
153 serializer.serialize_str(&self.to_string())
154 }
155}
156
157impl<'de> Deserialize<'de> for TradeId {
158 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
159 where
160 D: Deserializer<'de>,
161 {
162 let value_str = String::deserialize(deserializer)?;
163 Ok(Self::new(&value_str))
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use rstest::rstest;
170
171 use crate::identifiers::{stubs::*, trade_id::TradeId};
172
173 #[rstest]
174 fn test_trade_id_new_valid() {
175 let trade_id = TradeId::new("TRADE12345");
176 assert_eq!(trade_id.to_string(), "TRADE12345");
177 }
178
179 #[rstest]
180 #[should_panic(expected = "Condition failed: 'value' exceeds max length or invalid format")]
181 fn test_trade_id_new_invalid_length() {
182 let _ = TradeId::new("A".repeat(37).as_str());
183 }
184
185 #[rstest]
186 #[case(b"1234567890", "1234567890")]
187 #[case(
188 b"ABCDEFGHIJKLMNOPQRSTUVWXYZ123456",
189 "ABCDEFGHIJKLMNOPQRSTUVWXYZ123456"
190 )]
191 #[case(b"1234567890\0", "1234567890")]
192 #[case(
193 b"ABCDEFGHIJKLMNOPQRSTUVWXYZ123456\0",
194 "ABCDEFGHIJKLMNOPQRSTUVWXYZ123456"
195 )]
196 fn test_trade_id_from_valid_bytes(#[case] input: &[u8], #[case] expected: &str) {
197 let trade_id = TradeId::from_bytes(input).unwrap();
198 assert_eq!(trade_id.to_string(), expected);
199 }
200
201 #[rstest]
202 #[should_panic(expected = "the 'value' slice `&[u8]` was empty")]
203 fn test_trade_id_from_bytes_empty() {
204 TradeId::from_bytes(&[] as &[u8]).unwrap();
205 }
206
207 #[rstest]
208 #[should_panic(expected = "'value' was single null byte")]
209 fn test_trade_id_single_null_byte() {
210 TradeId::from_bytes(&[0u8] as &[u8]).unwrap();
211 }
212
213 #[rstest]
214 #[case(b"ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789012")] #[case(b"ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789012\0")] #[should_panic(expected = "'value' exceeds max length or invalid format")]
217 fn test_trade_id_exceeds_max_length(#[case] input: &[u8]) {
218 TradeId::from_bytes(input).unwrap();
219 }
220
221 #[rstest]
222 fn test_trade_id_with_null_terminator_at_max_length() {
223 let input = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ123456\0" as &[u8];
224 let trade_id = TradeId::from_bytes(input).unwrap();
225 assert_eq!(trade_id.to_string(), "ABCDEFGHIJKLMNOPQRSTUVWXYZ123456");
226 }
227
228 #[rstest]
229 fn test_trade_id_as_cstr() {
230 let trade_id = TradeId::new("TRADE12345");
231 assert_eq!(trade_id.as_cstr().to_str().unwrap(), "TRADE12345");
232 }
233
234 #[rstest]
235 fn test_trade_id_equality() {
236 let trade_id1 = TradeId::new("TRADE12345");
237 let trade_id2 = TradeId::new("TRADE12345");
238 assert_eq!(trade_id1, trade_id2);
239 }
240
241 #[rstest]
242 fn test_string_reprs(trade_id: TradeId) {
243 assert_eq!(trade_id.to_string(), "1234567890");
244 assert_eq!(format!("{trade_id}"), "1234567890");
245 assert_eq!(format!("{trade_id:?}"), "TradeId('1234567890')");
246 }
247
248 #[rstest]
249 fn test_trade_id_ordering() {
250 let trade_id1 = TradeId::new("TRADE12345");
251 let trade_id2 = TradeId::new("TRADE12346");
252 assert!(trade_id1 < trade_id2);
253 }
254
255 #[rstest]
256 fn test_trade_id_serialization() {
257 let trade_id = TradeId::new("TRADE12345");
258 let json = serde_json::to_string(&trade_id).unwrap();
259 assert_eq!(json, "\"TRADE12345\"");
260
261 let deserialized: TradeId = serde_json::from_str(&json).unwrap();
262 assert_eq!(trade_id, deserialized);
263 }
264}