nautilus_model/identifiers/
trade_id.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
// -------------------------------------------------------------------------------------------------
//  Copyright (C) 2015-2024 Nautech Systems Pty Ltd. All rights reserved.
//  https://nautechsystems.io
//
//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
//  You may not use this file except in compliance with the License.
//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
// -------------------------------------------------------------------------------------------------

//! Represents a valid trade match ID (assigned by a trading venue).

use std::{
    ffi::{CStr, CString},
    fmt::{Debug, Display, Formatter},
    hash::Hash,
};

use nautilus_core::correctness::{check_in_range_inclusive_usize, check_valid_string, FAILED};
use serde::{Deserialize, Deserializer, Serialize};

/// The maximum length of ASCII characters for a `TradeId` string value (including null terminator).
pub const TRADE_ID_LEN: usize = 37;

/// Represents a valid trade match ID (assigned by a trading venue).
///
/// Can correspond to the `TradeID <1003> field` of the FIX protocol.
#[repr(C)]
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(
    feature = "python",
    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
)]
pub struct TradeId {
    /// The trade match ID value as a fixed-length C string byte array (includes null terminator).
    pub(crate) value: [u8; TRADE_ID_LEN],
}

impl TradeId {
    /// Creates a new [`TradeId`] instance with correctness checking.
    ///
    /// Maximum length is 36 characters.
    ///
    /// The unique ID assigned to the trade entity once it is received or matched by
    /// the exchange or central counterparty.
    ///
    /// # Errors
    ///
    /// This function returns an error:
    /// - If `value` is not a valid string.
    /// - If `value` length is greater than 36.
    pub fn new_checked(value: &str) -> anyhow::Result<Self> {
        // check that string is non-empty and within the expected length
        check_in_range_inclusive_usize(value.len(), 1, TRADE_ID_LEN, stringify!(value))?;
        Ok(Self::from_valid_bytes(value.as_bytes()))
    }

    /// Creates a new [`TradeId`] instance.
    ///
    /// Maximum length is 36 characters.
    ///
    /// The unique ID assigned to the trade entity once it is received or matched by
    /// the exchange or central counterparty.
    ///
    /// # Panics
    ///
    /// This function panics:
    /// - If `value` is not a valid string, or value length is greater than 36.
    pub fn new(value: &str) -> Self {
        Self::new_checked(value).expect(FAILED)
    }

    pub fn from_cstr(cstr: CString) -> Self {
        let cstr_str = cstr
            .to_str()
            .expect("TradeId expected valid string as `CString`");
        check_valid_string(cstr_str, stringify!(cstr)).expect(FAILED);
        let bytes = cstr.as_bytes_with_nul();
        // check that string is non-empty excluding '\0' and within the expected length
        check_in_range_inclusive_usize(bytes.len(), 2, TRADE_ID_LEN, stringify!(cstr))
            .expect(FAILED);
        Self::from_valid_bytes(bytes)
    }

    fn from_valid_bytes(bytes: &[u8]) -> Self {
        let mut value = [0; TRADE_ID_LEN];
        value[..bytes.len()].copy_from_slice(bytes);
        Self { value }
    }

    #[must_use]
    pub fn to_cstr(&self) -> &CStr {
        // SAFETY: Unwrap safe as we always store valid C strings
        // We use until nul because the values array may be padded with nul bytes
        CStr::from_bytes_until_nul(&self.value).unwrap()
    }
}

impl Display for TradeId {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.to_cstr().to_str().unwrap())
    }
}

impl Serialize for TradeId {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        serializer.serialize_str(&self.to_string())
    }
}

impl<'de> Deserialize<'de> for TradeId {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let value_str = String::deserialize(deserializer)?;
        Ok(Self::new(&value_str))
    }
}

////////////////////////////////////////////////////////////////////////////////
// Tests
////////////////////////////////////////////////////////////////////////////////
#[cfg(test)]
mod tests {
    use rstest::rstest;

    use crate::identifiers::{stubs::*, trade_id::TradeId};

    #[rstest]
    fn test_string_reprs(trade_id: TradeId) {
        assert_eq!(trade_id.to_string(), "1234567890");
        assert_eq!(format!("{trade_id}"), "1234567890");
    }
}