nautilus_model/identifiers/
trade_id.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Represents a valid trade match ID (assigned by a trading venue).
17
18use std::{
19    ffi::CStr,
20    fmt::{Debug, Display},
21    hash::Hash,
22};
23
24use nautilus_core::StackStr;
25use serde::{Deserialize, Deserializer, Serialize, Serializer};
26
27/// Represents a valid trade match ID (assigned by a trading venue).
28///
29/// The unique ID assigned to the trade entity once it is received or matched by
30/// the venue or central counterparty.
31///
32/// Can correspond to the `TradeID <1003> field` of the FIX protocol.
33///
34/// Maximum length is 36 characters.
35#[repr(C)]
36#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
37#[cfg_attr(
38    feature = "python",
39    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
40)]
41pub struct TradeId(StackStr);
42
43impl TradeId {
44    /// Creates a new [`TradeId`] instance with correctness checking.
45    ///
46    /// Maximum length is 36 characters.
47    ///
48    /// # Errors
49    ///
50    /// Returns an error if:
51    /// - `value` is an invalid string (e.g., is empty or contains non-ASCII characters).
52    /// - `value` length exceeds 36 characters.
53    ///
54    /// # Notes
55    ///
56    /// PyO3 requires a `Result` type for proper error handling and stacktrace printing in Python.
57    pub fn new_checked<T: AsRef<str>>(value: T) -> anyhow::Result<Self> {
58        Ok(Self(StackStr::new_checked(value.as_ref())?))
59    }
60
61    /// Creates a new [`TradeId`] instance.
62    ///
63    /// Maximum length is 36 characters.
64    ///
65    /// # Panics
66    ///
67    /// This function panics if:
68    /// - `value` is an invalid string (e.g., is empty or contains non-ASCII characters).
69    /// - `value` length exceeds 36 characters.
70    pub fn new<T: AsRef<str>>(value: T) -> Self {
71        Self(StackStr::new(value.as_ref()))
72    }
73
74    /// Creates a [`TradeId`] from a byte slice.
75    ///
76    /// # Errors
77    ///
78    /// Returns an error if `bytes` is empty, contains non-ASCII characters,
79    /// or exceeds 36 bytes (excluding trailing null terminator).
80    pub fn from_bytes(bytes: &[u8]) -> anyhow::Result<Self> {
81        Ok(Self(StackStr::from_bytes(bytes)?))
82    }
83
84    /// Returns the inner string value.
85    #[inline]
86    #[must_use]
87    pub fn as_str(&self) -> &str {
88        self.0.as_str()
89    }
90
91    /// Returns a C string slice from the trade ID value.
92    #[inline]
93    #[must_use]
94    pub fn as_cstr(&self) -> &CStr {
95        self.0.as_cstr()
96    }
97}
98
99impl Debug for TradeId {
100    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101        write!(f, "{}('{}')", stringify!(TradeId), self)
102    }
103}
104
105impl Display for TradeId {
106    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107        write!(f, "{}", self.0)
108    }
109}
110
111impl Serialize for TradeId {
112    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
113    where
114        S: Serializer,
115    {
116        self.0.serialize(serializer)
117    }
118}
119
120impl<'de> Deserialize<'de> for TradeId {
121    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
122    where
123        D: Deserializer<'de>,
124    {
125        let inner = StackStr::deserialize(deserializer)?;
126        Ok(Self(inner))
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use rstest::rstest;
133
134    use crate::identifiers::{TradeId, stubs::*};
135
136    #[rstest]
137    fn test_trade_id_new_valid() {
138        let trade_id = TradeId::new("TRADE12345");
139        assert_eq!(trade_id.to_string(), "TRADE12345");
140    }
141
142    #[rstest]
143    #[should_panic(expected = "exceeds maximum length")]
144    fn test_trade_id_new_invalid_length() {
145        let _ = TradeId::new("A".repeat(37).as_str());
146    }
147
148    #[rstest]
149    #[case(b"1234567890", "1234567890")]
150    #[case(b"ABCDEFGHIJKLMNOPQRSTUVWXYZ1234", "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234")] // 30 chars
151    #[case(b"1234567890\0", "1234567890")]
152    #[case(b"ABCDEFGHIJKLMNOPQRSTUVWXYZ1234\0", "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234")] // 30 chars with null
153    fn test_trade_id_from_valid_bytes(#[case] input: &[u8], #[case] expected: &str) {
154        let trade_id = TradeId::from_bytes(input).unwrap();
155        assert_eq!(trade_id.to_string(), expected);
156    }
157
158    #[rstest]
159    #[should_panic(expected = "String is empty")]
160    fn test_trade_id_from_bytes_empty() {
161        TradeId::from_bytes(&[] as &[u8]).unwrap();
162    }
163
164    #[rstest]
165    #[should_panic(expected = "String is empty")]
166    fn test_trade_id_single_null_byte() {
167        TradeId::from_bytes(&[0u8] as &[u8]).unwrap();
168    }
169
170    #[rstest]
171    #[case(b"ABCDEFGHIJKLMNOPQRSTUVWXYZ12345678901")] // 37 bytes, no null
172    #[case(b"ABCDEFGHIJKLMNOPQRSTUVWXYZ12345678901\0")] // 38 bytes, with null
173    #[should_panic(expected = "exceeds maximum length")]
174    fn test_trade_id_exceeds_max_length(#[case] input: &[u8]) {
175        TradeId::from_bytes(input).unwrap();
176    }
177
178    #[rstest]
179    fn test_trade_id_with_null_terminator_at_max_length() {
180        let input = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\0" as &[u8];
181        let trade_id = TradeId::from_bytes(input).unwrap();
182        assert_eq!(trade_id.to_string(), "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"); // 36 chars
183    }
184
185    #[rstest]
186    fn test_trade_id_as_cstr() {
187        let trade_id = TradeId::new("TRADE12345");
188        assert_eq!(trade_id.as_cstr().to_str().unwrap(), "TRADE12345");
189    }
190
191    #[rstest]
192    fn test_trade_id_as_str() {
193        let trade_id = TradeId::new("TRADE12345");
194        assert_eq!(trade_id.as_str(), "TRADE12345");
195    }
196
197    #[rstest]
198    fn test_trade_id_equality() {
199        let trade_id1 = TradeId::new("TRADE12345");
200        let trade_id2 = TradeId::new("TRADE12345");
201        assert_eq!(trade_id1, trade_id2);
202    }
203
204    #[rstest]
205    fn test_string_reprs(trade_id: TradeId) {
206        assert_eq!(trade_id.to_string(), "1234567890");
207        assert_eq!(format!("{trade_id}"), "1234567890");
208        assert_eq!(format!("{trade_id:?}"), "TradeId('1234567890')");
209    }
210
211    #[rstest]
212    fn test_trade_id_ordering() {
213        let trade_id1 = TradeId::new("TRADE12345");
214        let trade_id2 = TradeId::new("TRADE12346");
215        assert!(trade_id1 < trade_id2);
216    }
217
218    #[rstest]
219    fn test_trade_id_serialization() {
220        let trade_id = TradeId::new("TRADE12345");
221        let json = serde_json::to_string(&trade_id).unwrap();
222        assert_eq!(json, "\"TRADE12345\"");
223
224        let deserialized: TradeId = serde_json::from_str(&json).unwrap();
225        assert_eq!(trade_id, deserialized);
226    }
227}