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)]
171mod tests {
172 use rstest::rstest;
173
174 use crate::identifiers::{stubs::*, trade_id::TradeId};
175
176 #[rstest]
177 fn test_trade_id_new_valid() {
178 let trade_id = TradeId::new("TRADE12345");
179 assert_eq!(trade_id.to_string(), "TRADE12345");
180 }
181
182 #[rstest]
183 #[should_panic(expected = "Condition failed: 'value' exceeds max length or invalid format")]
184 fn test_trade_id_new_invalid_length() {
185 let _ = TradeId::new("A".repeat(37).as_str());
186 }
187
188 #[rstest]
189 #[case(b"1234567890", "1234567890")]
190 #[case(
191 b"ABCDEFGHIJKLMNOPQRSTUVWXYZ123456",
192 "ABCDEFGHIJKLMNOPQRSTUVWXYZ123456"
193 )]
194 #[case(b"1234567890\0", "1234567890")]
195 #[case(
196 b"ABCDEFGHIJKLMNOPQRSTUVWXYZ123456\0",
197 "ABCDEFGHIJKLMNOPQRSTUVWXYZ123456"
198 )]
199 fn test_trade_id_from_valid_bytes(#[case] input: &[u8], #[case] expected: &str) {
200 let trade_id = TradeId::from_bytes(input).unwrap();
201 assert_eq!(trade_id.to_string(), expected);
202 }
203
204 #[rstest]
205 #[should_panic(expected = "the 'value' slice `&[u8]` was empty")]
206 fn test_trade_id_from_bytes_empty() {
207 TradeId::from_bytes(&[] as &[u8]).unwrap();
208 }
209
210 #[rstest]
211 #[should_panic(expected = "'value' was single null byte")]
212 fn test_trade_id_single_null_byte() {
213 TradeId::from_bytes(&[0u8] as &[u8]).unwrap();
214 }
215
216 #[rstest]
217 #[case(b"ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789012")] #[case(b"ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789012\0")] #[should_panic(expected = "'value' exceeds max length or invalid format")]
220 fn test_trade_id_exceeds_max_length(#[case] input: &[u8]) {
221 TradeId::from_bytes(input).unwrap();
222 }
223
224 #[rstest]
225 fn test_trade_id_with_null_terminator_at_max_length() {
226 let input = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ123456\0" as &[u8];
227 let trade_id = TradeId::from_bytes(input).unwrap();
228 assert_eq!(trade_id.to_string(), "ABCDEFGHIJKLMNOPQRSTUVWXYZ123456");
229 }
230
231 #[rstest]
232 fn test_trade_id_as_cstr() {
233 let trade_id = TradeId::new("TRADE12345");
234 assert_eq!(trade_id.as_cstr().to_str().unwrap(), "TRADE12345");
235 }
236
237 #[rstest]
238 fn test_trade_id_equality() {
239 let trade_id1 = TradeId::new("TRADE12345");
240 let trade_id2 = TradeId::new("TRADE12345");
241 assert_eq!(trade_id1, trade_id2);
242 }
243
244 #[rstest]
245 fn test_string_reprs(trade_id: TradeId) {
246 assert_eq!(trade_id.to_string(), "1234567890");
247 assert_eq!(format!("{trade_id}"), "1234567890");
248 assert_eq!(format!("{trade_id:?}"), "TradeId('1234567890')");
249 }
250
251 #[rstest]
252 fn test_trade_id_ordering() {
253 let trade_id1 = TradeId::new("TRADE12345");
254 let trade_id2 = TradeId::new("TRADE12346");
255 assert!(trade_id1 < trade_id2);
256 }
257
258 #[rstest]
259 fn test_trade_id_serialization() {
260 let trade_id = TradeId::new("TRADE12345");
261 let json = serde_json::to_string(&trade_id).unwrap();
262 assert_eq!(json, "\"TRADE12345\"");
263
264 let deserialized: TradeId = serde_json::from_str(&json).unwrap();
265 assert_eq!(trade_id, deserialized);
266 }
267}