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 check_predicate_false, check_predicate_true, check_slice_not_empty, FAILED,
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> {
94 check_slice_not_empty(value, "value")?;
95
96 let mut last_byte = 0;
98 let all_ascii = value
99 .iter()
100 .inspect(|&&b| last_byte = b)
101 .all(|&b| b.is_ascii());
102
103 check_predicate_true(all_ascii, "'value' contains non-ASCII characters")?;
104 check_predicate_false(
105 value.len() == 1 && last_byte == 0,
106 "'value' was single null byte",
107 )?;
108 check_predicate_true(
109 value.len() <= 36 || (value.len() == 37 && last_byte == 0),
110 "'value' exceeds max length or invalid format",
111 )?;
112
113 let mut buf = [0; TRADE_ID_LEN];
114 buf[..value.len()].copy_from_slice(value);
115
116 Ok(Self { value: buf })
117 }
118
119 #[must_use]
121 pub fn as_cstr(&self) -> &CStr {
122 CStr::from_bytes_until_nul(&self.value).unwrap()
125 }
126}
127
128impl Debug for TradeId {
129 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
130 write!(f, "{}('{}')", stringify!(TradeId), self)
131 }
132}
133
134impl Display for TradeId {
135 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
136 write!(f, "{}", self.as_cstr().to_str().unwrap())
137 }
138}
139
140impl Serialize for TradeId {
141 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
142 where
143 S: serde::Serializer,
144 {
145 serializer.serialize_str(&self.to_string())
146 }
147}
148
149impl<'de> Deserialize<'de> for TradeId {
150 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
151 where
152 D: Deserializer<'de>,
153 {
154 let value_str = String::deserialize(deserializer)?;
155 Ok(Self::new(&value_str))
156 }
157}
158
159#[cfg(test)]
163mod tests {
164 use rstest::rstest;
165
166 use crate::identifiers::{stubs::*, trade_id::TradeId};
167
168 #[rstest]
169 fn test_trade_id_new_valid() {
170 let trade_id = TradeId::new("TRADE12345");
171 assert_eq!(trade_id.to_string(), "TRADE12345");
172 }
173
174 #[rstest]
175 #[should_panic(expected = "Condition failed: 'value' exceeds max length or invalid format")]
176 fn test_trade_id_new_invalid_length() {
177 let _ = TradeId::new("A".repeat(37).as_str());
178 }
179
180 #[rstest]
181 #[case(b"1234567890", "1234567890")]
182 #[case(
183 b"ABCDEFGHIJKLMNOPQRSTUVWXYZ123456",
184 "ABCDEFGHIJKLMNOPQRSTUVWXYZ123456"
185 )]
186 #[case(b"1234567890\0", "1234567890")]
187 #[case(
188 b"ABCDEFGHIJKLMNOPQRSTUVWXYZ123456\0",
189 "ABCDEFGHIJKLMNOPQRSTUVWXYZ123456"
190 )]
191 fn test_trade_id_from_valid_bytes(#[case] input: &[u8], #[case] expected: &str) {
192 let trade_id = TradeId::from_bytes(input).unwrap();
193 assert_eq!(trade_id.to_string(), expected);
194 }
195
196 #[rstest]
197 #[should_panic(expected = "the 'value' slice `&[u8]` was empty")]
198 fn test_trade_id_from_bytes_empty() {
199 TradeId::from_bytes(&[] as &[u8]).unwrap();
200 }
201
202 #[rstest]
203 #[should_panic(expected = "'value' was single null byte")]
204 fn test_trade_id_single_null_byte() {
205 TradeId::from_bytes(&[0u8] as &[u8]).unwrap();
206 }
207
208 #[rstest]
209 #[case(b"ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789012")] #[case(b"ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789012\0")] #[should_panic(expected = "'value' exceeds max length or invalid format")]
212 fn test_trade_id_exceeds_max_length(#[case] input: &[u8]) {
213 TradeId::from_bytes(input).unwrap();
214 }
215
216 #[rstest]
217 fn test_trade_id_with_null_terminator_at_max_length() {
218 let input = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ123456\0" as &[u8];
219 let trade_id = TradeId::from_bytes(input).unwrap();
220 assert_eq!(trade_id.to_string(), "ABCDEFGHIJKLMNOPQRSTUVWXYZ123456");
221 }
222
223 #[rstest]
224 fn test_trade_id_as_cstr() {
225 let trade_id = TradeId::new("TRADE12345");
226 assert_eq!(trade_id.as_cstr().to_str().unwrap(), "TRADE12345");
227 }
228
229 #[rstest]
230 fn test_trade_id_equality() {
231 let trade_id1 = TradeId::new("TRADE12345");
232 let trade_id2 = TradeId::new("TRADE12345");
233 assert_eq!(trade_id1, trade_id2);
234 }
235
236 #[rstest]
237 fn test_string_reprs(trade_id: TradeId) {
238 assert_eq!(trade_id.to_string(), "1234567890");
239 assert_eq!(format!("{trade_id}"), "1234567890");
240 assert_eq!(format!("{trade_id:?}"), "TradeId('1234567890')");
241 }
242
243 #[rstest]
244 fn test_trade_id_ordering() {
245 let trade_id1 = TradeId::new("TRADE12345");
246 let trade_id2 = TradeId::new("TRADE12346");
247 assert!(trade_id1 < trade_id2);
248 }
249
250 #[rstest]
251 fn test_trade_id_serialization() {
252 let trade_id = TradeId::new("TRADE12345");
253 let json = serde_json::to_string(&trade_id).unwrap();
254 assert_eq!(json, "\"TRADE12345\"");
255
256 let deserialized: TradeId = serde_json::from_str(&json).unwrap();
257 assert_eq!(trade_id, deserialized);
258 }
259}