nautilus_model/identifiers/
strategy_id.rs1use std::fmt::{Debug, Display};
19
20use nautilus_core::correctness::{
21 CorrectnessResult, CorrectnessResultExt, FAILED, check_predicate_false, check_string_contains,
22 check_valid_string_ascii,
23};
24use ustr::Ustr;
25
26const EXTERNAL_STRATEGY_ID: &str = "EXTERNAL";
28
29#[repr(C)]
31#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
32#[cfg_attr(
33 feature = "python",
34 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
35)]
36#[cfg_attr(
37 feature = "python",
38 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
39)]
40pub struct StrategyId(Ustr);
41
42impl StrategyId {
43 pub fn new_checked<T: AsRef<str>>(value: T) -> CorrectnessResult<Self> {
61 let value = value.as_ref();
62 check_valid_string_ascii(value, stringify!(value))?;
63 if value != EXTERNAL_STRATEGY_ID {
64 check_string_contains(value, "-", stringify!(value))?;
65
66 if let Some((name, tag)) = value.rsplit_once('-') {
67 check_predicate_false(
68 name.is_empty(),
69 "`value` name part (before '-') cannot be empty",
70 )?;
71 check_predicate_false(
72 tag.is_empty(),
73 "`value` tag part (after '-') cannot be empty",
74 )?;
75 }
76 }
77 Ok(Self(Ustr::from(value)))
78 }
79
80 pub fn new<T: AsRef<str>>(value: T) -> Self {
86 Self::new_checked(value).expect_display(FAILED)
87 }
88
89 #[cfg_attr(not(feature = "python"), allow(dead_code))]
91 pub(crate) fn set_inner(&mut self, value: &str) {
92 self.0 = Ustr::from(value);
93 }
94
95 #[must_use]
97 pub fn inner(&self) -> Ustr {
98 self.0
99 }
100
101 #[must_use]
103 pub fn as_str(&self) -> &str {
104 self.0.as_str()
105 }
106
107 #[must_use]
108 pub fn external() -> Self {
109 Self::new(EXTERNAL_STRATEGY_ID)
110 }
111
112 #[must_use]
113 pub fn is_external(&self) -> bool {
114 self.0 == EXTERNAL_STRATEGY_ID
115 }
116
117 #[must_use]
121 pub fn get_tag(&self) -> &str {
122 self.0
123 .rsplit_once('-')
124 .map_or(self.0.as_str(), |(_, tag)| tag)
125 }
126}
127
128impl Debug for StrategyId {
129 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130 write!(f, "\"{}\"", self.0)
131 }
132}
133
134impl Display for StrategyId {
135 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136 write!(f, "{}", self.0)
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use nautilus_core::correctness::CorrectnessError;
143 use rstest::rstest;
144
145 use super::StrategyId;
146 use crate::identifiers::stubs::*;
147
148 #[rstest]
149 fn test_string_reprs(strategy_id_ema_cross: StrategyId) {
150 assert_eq!(strategy_id_ema_cross.as_str(), "EMACross-001");
151 assert_eq!(format!("{strategy_id_ema_cross}"), "EMACross-001");
152 }
153
154 #[rstest]
155 fn test_get_external() {
156 assert_eq!(StrategyId::external().as_str(), "EXTERNAL");
157 }
158
159 #[rstest]
160 fn test_is_external() {
161 assert!(StrategyId::external().is_external());
162 }
163
164 #[rstest]
165 fn test_get_tag(strategy_id_ema_cross: StrategyId) {
166 assert_eq!(strategy_id_ema_cross.get_tag(), "001");
167 }
168
169 #[rstest]
170 fn test_get_tag_external() {
171 assert_eq!(StrategyId::external().get_tag(), "EXTERNAL");
172 }
173
174 #[rstest]
175 #[should_panic(expected = "name part (before '-') cannot be empty")]
176 fn test_new_with_empty_name_panics() {
177 let _ = StrategyId::new("-001");
178 }
179
180 #[rstest]
181 #[should_panic(expected = "tag part (after '-') cannot be empty")]
182 fn test_new_with_empty_tag_panics() {
183 let _ = StrategyId::new("EMACross-");
184 }
185
186 #[rstest]
187 fn test_new_checked_with_empty_name_returns_error() {
188 assert!(StrategyId::new_checked("-001").is_err());
189 }
190
191 #[rstest]
192 fn test_new_checked_with_empty_tag_returns_error() {
193 assert!(StrategyId::new_checked("EMACross-").is_err());
194 }
195
196 #[rstest]
197 fn test_new_checked_with_empty_name_returns_typed_error_with_stable_display() {
198 let error = StrategyId::new_checked("-001").unwrap_err();
199
200 match error {
201 CorrectnessError::PredicateViolation { ref message } => {
202 assert_eq!(message, "`value` name part (before '-') cannot be empty");
203 }
204 other => panic!("Expected typed predicate violation, was: {other:?}"),
205 }
206
207 assert_eq!(
208 error.to_string(),
209 "`value` name part (before '-') cannot be empty"
210 );
211 }
212
213 #[rstest]
214 fn test_new_checked_with_empty_tag_returns_typed_error_with_stable_display() {
215 let error = StrategyId::new_checked("EMACross-").unwrap_err();
216
217 match error {
218 CorrectnessError::PredicateViolation { ref message } => {
219 assert_eq!(message, "`value` tag part (after '-') cannot be empty");
220 }
221 other => panic!("Expected typed predicate violation, was: {other:?}"),
222 }
223
224 assert_eq!(
225 error.to_string(),
226 "`value` tag part (after '-') cannot be empty"
227 );
228 }
229}