1use std::{fmt::Display, ops::Deref};
19
20use nautilus_core::correctness::{FAILED, check_valid_string_utf8};
21use serde::{Deserialize, Serialize};
22use ustr::Ustr;
23
24#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
26pub struct Pattern;
27
28#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
30pub struct Topic;
31
32#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
34pub struct Endpoint;
35
36#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
42#[serde(transparent)]
43pub struct MStr<T> {
44 value: Ustr,
45 #[serde(skip)]
46 _marker: std::marker::PhantomData<T>,
47}
48
49impl<T> MStr<T> {
50 #[inline(always)]
51 fn checked(value: Ustr, key: &str) -> anyhow::Result<Self> {
52 check_valid_string_utf8(value.as_str(), stringify!(value))?;
53
54 if value.as_bytes().iter().any(|&b| b == b'*' || b == b'?') {
55 anyhow::bail!("{key} `value` contained invalid characters, was {value}");
56 }
57
58 Ok(Self {
59 value,
60 _marker: std::marker::PhantomData,
61 })
62 }
63}
64
65impl<T> Display for MStr<T> {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 write!(f, "{}", self.value)
68 }
69}
70
71impl<T> Deref for MStr<T> {
72 type Target = Ustr;
73
74 fn deref(&self) -> &Self::Target {
75 &self.value
76 }
77}
78
79impl<T> AsRef<str> for MStr<T> {
80 fn as_ref(&self) -> &str {
81 self.value.as_str()
82 }
83}
84
85impl MStr<Pattern> {
86 pub fn pattern<T: AsRef<str>>(value: T) -> Self {
88 let value = Ustr::from(value.as_ref());
89
90 Self {
91 value,
92 _marker: std::marker::PhantomData,
93 }
94 }
95
96 pub fn pattern_checked<T: AsRef<str>>(value: T) -> anyhow::Result<Self> {
104 check_valid_string_utf8(value.as_ref(), stringify!(value))?;
105
106 Ok(Self::pattern(value))
107 }
108}
109
110impl From<&str> for MStr<Pattern> {
111 fn from(value: &str) -> Self {
112 Self::pattern(value)
113 }
114}
115
116impl From<String> for MStr<Pattern> {
117 fn from(value: String) -> Self {
118 value.as_str().into()
119 }
120}
121
122impl From<&String> for MStr<Pattern> {
123 fn from(value: &String) -> Self {
124 value.as_str().into()
125 }
126}
127
128impl From<MStr<Topic>> for MStr<Pattern> {
129 fn from(value: MStr<Topic>) -> Self {
130 Self {
131 value: value.value,
132 _marker: std::marker::PhantomData,
133 }
134 }
135}
136
137impl MStr<Topic> {
138 pub fn topic<T: AsRef<str>>(value: T) -> anyhow::Result<Self> {
144 Self::checked(Ustr::from(value.as_ref()), stringify!(Topic))
145 }
146
147 pub fn topic_from_ustr(value: Ustr) -> anyhow::Result<Self> {
153 Self::checked(value, stringify!(Topic))
154 }
155}
156
157impl From<&str> for MStr<Topic> {
158 fn from(value: &str) -> Self {
159 Self::topic(value).expect(FAILED)
160 }
161}
162
163impl From<String> for MStr<Topic> {
164 fn from(value: String) -> Self {
165 value.as_str().into()
166 }
167}
168
169impl From<&String> for MStr<Topic> {
170 fn from(value: &String) -> Self {
171 value.as_str().into()
172 }
173}
174
175impl From<Ustr> for MStr<Topic> {
176 fn from(value: Ustr) -> Self {
177 Self::topic_from_ustr(value).expect(FAILED)
178 }
179}
180
181impl From<&Ustr> for MStr<Topic> {
182 fn from(value: &Ustr) -> Self {
183 (*value).into()
184 }
185}
186
187impl MStr<Endpoint> {
188 pub fn endpoint<T: AsRef<str>>(value: T) -> anyhow::Result<Self> {
194 Self::checked(Ustr::from(value.as_ref()), stringify!(Endpoint))
195 }
196
197 pub fn endpoint_from_ustr(value: Ustr) -> anyhow::Result<Self> {
203 Self::checked(value, stringify!(Endpoint))
204 }
205}
206
207impl From<&str> for MStr<Endpoint> {
208 fn from(value: &str) -> Self {
209 Self::endpoint(value).expect(FAILED)
210 }
211}
212
213impl From<String> for MStr<Endpoint> {
214 fn from(value: String) -> Self {
215 value.as_str().into()
216 }
217}
218
219impl From<&String> for MStr<Endpoint> {
220 fn from(value: &String) -> Self {
221 value.as_str().into()
222 }
223}
224
225impl From<Ustr> for MStr<Endpoint> {
226 fn from(value: Ustr) -> Self {
227 Self::endpoint_from_ustr(value).expect(FAILED)
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use proptest::prelude::*;
234 use rstest::rstest;
235
236 use super::*;
237
238 #[rstest]
239 #[case("data.quotes.BINANCE.BTCUSDT")]
240 #[case("events.order.filled")]
241 #[case("a")]
242 #[case("a.b.c.d.e.f")]
243 fn test_topic_valid(#[case] input: &str) {
244 let topic = MStr::<Topic>::topic(input).unwrap();
245 assert_eq!(topic.as_ref(), input);
246 }
247
248 #[rstest]
249 #[case("data.*.BINANCE")]
250 #[case("events.order.*")]
251 #[case("*")]
252 #[case("data.quotes.?")]
253 #[case("a?b")]
254 fn test_topic_rejects_wildcards(#[case] input: &str) {
255 assert!(MStr::<Topic>::topic(input).is_err());
256 }
257
258 #[rstest]
259 #[case("", "invalid string for 'value', was empty")]
260 #[case(" ", "invalid string for 'value', was all whitespace")]
261 #[case("data.*", "Topic `value` contained invalid characters, was data.*")]
262 fn test_topic_constructors_return_exact_error(#[case] input: &str, #[case] expected: &str) {
263 let string_error = MStr::<Topic>::topic(input).unwrap_err();
264 let ustr_error = MStr::<Topic>::topic_from_ustr(Ustr::from(input)).unwrap_err();
265
266 assert_eq!(string_error.to_string(), expected);
267 assert_eq!(ustr_error.to_string(), expected);
268 }
269
270 #[rstest]
271 #[case("DataEngine.execute")]
272 #[case("RiskEngine.process")]
273 fn test_endpoint_valid(#[case] input: &str) {
274 let endpoint = MStr::<Endpoint>::endpoint(input).unwrap();
275 assert_eq!(endpoint.as_ref(), input);
276 }
277
278 #[rstest]
279 #[case("DataEngine.*")]
280 #[case("*.execute")]
281 #[case("Risk?Engine")]
282 fn test_endpoint_rejects_wildcards(#[case] input: &str) {
283 assert!(MStr::<Endpoint>::endpoint(input).is_err());
284 }
285
286 #[rstest]
287 #[case("", "invalid string for 'value', was empty")]
288 #[case(" ", "invalid string for 'value', was all whitespace")]
289 #[case(
290 "Risk?Engine",
291 "Endpoint `value` contained invalid characters, was Risk?Engine"
292 )]
293 fn test_endpoint_constructors_return_exact_error(#[case] input: &str, #[case] expected: &str) {
294 let string_error = MStr::<Endpoint>::endpoint(input).unwrap_err();
295 let ustr_error = MStr::<Endpoint>::endpoint_from_ustr(Ustr::from(input)).unwrap_err();
296
297 assert_eq!(string_error.to_string(), expected);
298 assert_eq!(ustr_error.to_string(), expected);
299 }
300
301 #[rstest]
302 #[case("data.*")]
303 #[case("*.quotes.*")]
304 #[case("data.?.BINANCE")]
305 #[case("*")]
306 #[case("exact.match.no.wildcards")]
307 fn test_pattern_accepts_all(#[case] input: &str) {
308 let pattern = MStr::<Pattern>::pattern(input);
309 assert_eq!(pattern.as_ref(), input);
310 }
311
312 #[rstest]
313 #[case("data.*")]
314 #[case("*.quotes.*")]
315 #[case("data.?.BINANCE")]
316 #[case("*")]
317 #[case("exact.match.no.wildcards")]
318 fn test_pattern_checked_accepts_matchable_patterns(#[case] input: &str) {
319 let pattern = MStr::<Pattern>::pattern_checked(input).unwrap();
320 assert_eq!(pattern.as_ref(), input);
321 }
322
323 #[rstest]
324 #[case("")]
325 #[case(" ")]
326 #[case("\t\n")]
327 fn test_pattern_checked_rejects_empty_whitespace(#[case] input: &str) {
328 assert!(MStr::<Pattern>::pattern_checked(input).is_err());
329 }
330
331 #[rstest]
332 fn test_topic_to_pattern_conversion() {
333 let topic: MStr<Topic> = "data.quotes.BINANCE.BTCUSDT".into();
334 let pattern: MStr<Pattern> = topic.into();
335 assert_eq!(pattern.as_ref(), "data.quotes.BINANCE.BTCUSDT");
336 }
337
338 #[rstest]
339 fn test_topic_from_ustr_valid() {
340 let ustr = Ustr::from("data.quotes.BINANCE");
341 let topic = MStr::<Topic>::topic_from_ustr(ustr).unwrap();
342 assert_eq!(topic.as_ref(), "data.quotes.BINANCE");
343 }
344
345 #[rstest]
346 #[case("")]
347 #[case(" ")]
348 #[case("\t\n")]
349 fn test_topic_from_ustr_rejects_empty_whitespace(#[case] input: &str) {
350 let ustr = Ustr::from(input);
351 assert!(MStr::<Topic>::topic_from_ustr(ustr).is_err());
352 }
353
354 #[rstest]
355 #[case("data.*")]
356 #[case("a?b")]
357 fn test_topic_from_ustr_rejects_wildcards(#[case] input: &str) {
358 let ustr = Ustr::from(input);
359 assert!(MStr::<Topic>::topic_from_ustr(ustr).is_err());
360 }
361
362 #[rstest]
363 fn test_endpoint_from_ustr_valid() {
364 let ustr = Ustr::from("DataEngine.execute");
365 let endpoint = MStr::<Endpoint>::endpoint_from_ustr(ustr).unwrap();
366 assert_eq!(endpoint.as_ref(), "DataEngine.execute");
367 }
368
369 #[rstest]
370 #[case("")]
371 #[case(" ")]
372 fn test_endpoint_from_ustr_rejects_empty_whitespace(#[case] input: &str) {
373 let ustr = Ustr::from(input);
374 assert!(MStr::<Endpoint>::endpoint_from_ustr(ustr).is_err());
375 }
376
377 #[rstest]
378 #[case("Engine.*")]
379 #[case("a?b")]
380 fn test_endpoint_from_ustr_rejects_wildcards(#[case] input: &str) {
381 let ustr = Ustr::from(input);
382 assert!(MStr::<Endpoint>::endpoint_from_ustr(ustr).is_err());
383 }
384
385 #[rstest]
386 fn test_from_impls_equivalent() {
387 let s = "test.topic";
388 let from_str: MStr<Topic> = s.into();
389 let from_string: MStr<Topic> = s.to_string().into();
390 let from_string_ref: MStr<Topic> = (&s.to_string()).into();
391 let from_ustr: MStr<Topic> = Ustr::from(s).into();
392
393 assert_eq!(from_str, from_string);
394 assert_eq!(from_string, from_string_ref);
395 assert_eq!(from_string_ref, from_ustr);
396 }
397
398 #[rstest]
399 fn test_deref_to_ustr() {
400 let topic: MStr<Topic> = "test.topic".into();
401 let ustr: &Ustr = &topic;
402 assert_eq!(ustr.as_str(), "test.topic");
403 }
404
405 fn valid_segment() -> impl Strategy<Value = String> {
406 "[a-zA-Z][a-zA-Z0-9_]{0,15}".prop_filter("non-empty", |s| !s.is_empty())
407 }
408
409 fn valid_topic_string() -> impl Strategy<Value = String> {
410 prop::collection::vec(valid_segment(), 1..=5).prop_map(|segs| segs.join("."))
411 }
412
413 fn string_with_wildcards() -> impl Strategy<Value = String> {
414 prop::collection::vec(
415 prop_oneof![
416 valid_segment(),
417 Just("*".to_string()),
418 Just("?".to_string()),
419 ],
420 1..=5,
421 )
422 .prop_map(|segs| segs.join("."))
423 .prop_filter("must contain wildcard", |s| {
424 s.contains('*') || s.contains('?')
425 })
426 }
427
428 proptest! {
429 #[rstest]
430 fn prop_topic_roundtrip(s in valid_topic_string()) {
431 let topic = MStr::<Topic>::topic(&s).unwrap();
432 prop_assert_eq!(topic.as_ref(), s.as_str());
433 }
434
435 #[rstest]
436 fn prop_endpoint_roundtrip(s in valid_topic_string()) {
437 let endpoint = MStr::<Endpoint>::endpoint(&s).unwrap();
438 prop_assert_eq!(endpoint.as_ref(), s.as_str());
439 }
440
441 #[rstest]
442 fn prop_pattern_accepts_wildcards(s in string_with_wildcards()) {
443 let pattern = MStr::<Pattern>::pattern(&s);
444 prop_assert_eq!(pattern.as_ref(), s.as_str());
445 }
446
447 #[rstest]
448 fn prop_topic_rejects_wildcards(s in string_with_wildcards()) {
449 prop_assert!(MStr::<Topic>::topic(&s).is_err());
450 }
451
452 #[rstest]
453 fn prop_endpoint_rejects_wildcards(s in string_with_wildcards()) {
454 prop_assert!(MStr::<Endpoint>::endpoint(&s).is_err());
455 }
456
457 #[rstest]
458 fn prop_topic_to_pattern_preserves_value(s in valid_topic_string()) {
459 let topic: MStr<Topic> = MStr::topic(&s).unwrap();
460 let pattern: MStr<Pattern> = topic.into();
461 prop_assert_eq!(pattern.as_ref(), s.as_str());
462 }
463
464 #[rstest]
465 fn prop_from_impls_consistent(s in valid_topic_string()) {
466 let from_str: MStr<Topic> = s.as_str().into();
467 let from_string: MStr<Topic> = s.clone().into();
468 let from_ustr: MStr<Topic> = Ustr::from(&s).into();
469
470 prop_assert_eq!(from_str, from_string);
471 prop_assert_eq!(from_string, from_ustr);
472 }
473 }
474}