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