1use std::borrow::Cow;
4use std::error::Error;
5use std::fmt;
6use std::str::FromStr;
7use std::sync::Arc;
8
9use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
10
11use crate::{Class, Classify};
12
13const fn is_id(id: &str) -> bool {
16 let bytes = id.as_bytes();
17 if bytes.is_empty() || !bytes[0].is_ascii_lowercase() {
18 return false;
19 }
20 let mut i = 1;
21 while i < bytes.len() {
22 let b = bytes[i];
23 if !(b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-') {
24 return false;
25 }
26 i += 1;
27 }
28 true
29}
30
31macro_rules! id_type {
32 ($(#[$doc:meta])* $name:ident, $what:literal) => {
33 $(#[$doc])*
34 #[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
35 pub struct $name(Cow<'static, str>);
36
37 impl $name {
38 #[doc = concat!("A ", $what, " id known when the program is built.")]
39 pub const fn from_static(id: &'static str) -> Self {
45 assert!(
46 is_id(id),
47 "an id is a lowercase letter, then lowercase letters, digits and `-`"
48 );
49 Self(Cow::Borrowed(id))
50 }
51
52 #[doc = concat!("The ", $what, " id.")]
53 pub fn as_str(&self) -> &str {
54 &self.0
55 }
56 }
57
58 impl fmt::Display for $name {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 f.write_str(&self.0)
61 }
62 }
63
64 impl fmt::Debug for $name {
65 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66 f.debug_tuple(stringify!($name)).field(&self.as_str()).finish()
67 }
68 }
69 };
70}
71
72id_type! {
73 VenueId, "venue"
78}
79
80id_type! {
81 ProductId, "product"
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
95#[non_exhaustive]
96pub enum ProductClass {
97 EventContract,
101 PerpetualFuture,
104 Spot,
108}
109
110impl ProductClass {
111 pub fn as_str(self) -> &'static str {
113 match self {
114 Self::EventContract => "event-contract",
115 Self::PerpetualFuture => "perpetual-future",
116 Self::Spot => "spot",
117 }
118 }
119}
120
121impl fmt::Display for ProductClass {
122 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123 f.write_str(self.as_str())
124 }
125}
126
127#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
174pub struct MarketKey {
175 venue: VenueId,
176 product: ProductId,
177 class: ProductClass,
180 id: Arc<str>,
181}
182
183impl MarketKey {
184 pub fn venue(&self) -> &VenueId {
186 &self.venue
187 }
188
189 pub fn product(&self) -> &ProductId {
191 &self.product
192 }
193
194 pub fn class(&self) -> ProductClass {
197 self.class
198 }
199
200 pub fn id(&self) -> &str {
203 &self.id
204 }
205}
206
207impl fmt::Display for MarketKey {
208 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
209 write!(f, "{}.{}:{}", self.venue, self.product, self.id)
210 }
211}
212
213impl fmt::Debug for MarketKey {
214 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
215 f.debug_tuple("MarketKey")
216 .field(&format_args!("{self}"))
217 .finish()
218 }
219}
220
221impl Serialize for MarketKey {
222 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
223 serializer.collect_str(self)
224 }
225}
226
227#[derive(Clone, PartialEq, Eq, Hash)]
250pub struct RawKey {
251 venue: VenueId,
252 product: ProductId,
253 id: String,
254}
255
256impl RawKey {
257 pub fn parse(text: &str) -> Result<Self, KeyError> {
264 let malformed = |reason| KeyError::Malformed {
265 text: text.to_owned(),
266 reason,
267 };
268 let (scope, id) = text
269 .split_once(':')
270 .ok_or_else(|| malformed("no `:` before the id"))?;
271 let (venue, product) = scope
272 .split_once('.')
273 .ok_or_else(|| malformed("no `.` between the venue and the product"))?;
274 if !is_id(venue) {
275 return Err(malformed(
276 "the venue is not a lowercase letter followed by lowercase letters, digits and `-`",
277 ));
278 }
279 if !is_id(product) {
280 return Err(malformed(
281 "the product is not a lowercase letter followed by lowercase letters, digits and `-`",
282 ));
283 }
284 if id.is_empty() {
285 return Err(malformed("the id is empty"));
286 }
287 Ok(Self {
288 venue: VenueId(Cow::Owned(venue.to_owned())),
289 product: ProductId(Cow::Owned(product.to_owned())),
290 id: id.to_owned(),
291 })
292 }
293
294 pub fn venue(&self) -> &VenueId {
296 &self.venue
297 }
298
299 pub fn product(&self) -> &ProductId {
301 &self.product
302 }
303
304 pub fn id(&self) -> &str {
306 &self.id
307 }
308}
309
310impl FromStr for RawKey {
311 type Err = KeyError;
312
313 fn from_str(text: &str) -> Result<Self, Self::Err> {
314 Self::parse(text)
315 }
316}
317
318impl fmt::Display for RawKey {
319 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
320 write!(f, "{}.{}:{}", self.venue, self.product, self.id)
321 }
322}
323
324impl fmt::Debug for RawKey {
325 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
326 f.debug_tuple("RawKey")
327 .field(&format_args!("{self}"))
328 .finish()
329 }
330}
331
332impl Serialize for RawKey {
333 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
334 serializer.collect_str(self)
335 }
336}
337
338impl<'de> Deserialize<'de> for RawKey {
339 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
340 let text = Cow::<'de, str>::deserialize(deserializer)?;
341 Self::parse(&text).map_err(de::Error::custom)
342 }
343}
344
345pub trait KeySpace {
352 const VENUE: VenueId;
354 const PRODUCT: ProductId;
356 const CLASS: ProductClass;
359 type Id;
361
362 fn parse_id(text: &str) -> Result<Self::Id, String>;
368
369 fn id_text(id: &Self::Id) -> Cow<'_, str>;
371
372 fn key(id: &Self::Id) -> MarketKey {
374 MarketKey {
375 venue: Self::VENUE,
376 product: Self::PRODUCT,
377 class: Self::CLASS,
378 id: Arc::from(Self::id_text(id).as_ref()),
379 }
380 }
381
382 fn parse(raw: &RawKey) -> Result<MarketKey, KeyError> {
389 id_of::<Self>(raw.venue(), raw.product(), raw.id()).map(|id| Self::key(&id))
390 }
391
392 fn id(key: &MarketKey) -> Result<Self::Id, KeyError> {
398 id_of::<Self>(key.venue(), key.product(), key.id())
399 }
400}
401
402fn id_of<K: KeySpace + ?Sized>(
405 venue: &VenueId,
406 product: &ProductId,
407 id: &str,
408) -> Result<K::Id, KeyError> {
409 if venue != &K::VENUE || product != &K::PRODUCT {
410 return Err(KeyError::WrongProduct {
411 expected: format!("{}.{}", K::VENUE, K::PRODUCT),
412 found: format!("{venue}.{product}"),
413 });
414 }
415 K::parse_id(id).map_err(|reason| KeyError::InvalidId {
416 product: format!("{}.{}", K::VENUE, K::PRODUCT),
417 id: id.to_owned(),
418 reason,
419 })
420}
421
422#[derive(Debug, Clone, PartialEq, Eq)]
426#[non_exhaustive]
427pub enum KeyError {
428 Malformed {
430 text: String,
432 reason: &'static str,
434 },
435 UnknownProduct {
437 venue: String,
439 product: String,
441 },
442 WrongProduct {
444 expected: String,
446 found: String,
448 },
449 InvalidId {
451 product: String,
453 id: String,
455 reason: String,
457 },
458}
459
460impl KeyError {
461 pub fn unknown(raw: &RawKey) -> Self {
463 Self::UnknownProduct {
464 venue: raw.venue().as_str().to_owned(),
465 product: raw.product().as_str().to_owned(),
466 }
467 }
468}
469
470impl fmt::Display for KeyError {
471 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
472 match self {
473 Self::Malformed { text, reason } => {
474 write!(
475 f,
476 "{text:?} is not a market key (`venue.product:id`): {reason}"
477 )
478 }
479 Self::UnknownProduct { venue, product } => {
480 write!(f, "no product this build enables is {venue}.{product}")
481 }
482 Self::WrongProduct { expected, found } => {
483 write!(f, "a {found} key was given where a {expected} key belongs")
484 }
485 Self::InvalidId {
486 product,
487 id,
488 reason,
489 } => write!(f, "{id:?} is not a {product} id: {reason}"),
490 }
491 }
492}
493
494impl Error for KeyError {}
495
496impl Classify for KeyError {
498 fn class(&self) -> Class {
499 Class::InvalidRequest
500 }
501}
502
503#[cfg(test)]
504mod tests {
505 use super::*;
506
507 struct Upper;
509
510 struct UpperId(String);
512
513 impl KeySpace for Upper {
514 const VENUE: VenueId = VenueId::from_static("acme");
515 const PRODUCT: ProductId = ProductId::from_static("spot");
516 const CLASS: ProductClass = ProductClass::EventContract;
517 type Id = UpperId;
518
519 fn parse_id(text: &str) -> Result<UpperId, String> {
520 if text.is_ascii() {
521 Ok(UpperId(text.to_ascii_uppercase()))
522 } else {
523 Err("an id is ASCII".to_owned())
524 }
525 }
526
527 fn id_text(id: &UpperId) -> Cow<'_, str> {
528 Cow::Borrowed(&id.0)
529 }
530 }
531
532 struct Numbered;
534
535 impl KeySpace for Numbered {
536 const VENUE: VenueId = VenueId::from_static("acme");
537 const PRODUCT: ProductId = ProductId::from_static("perpetual");
538 const CLASS: ProductClass = ProductClass::PerpetualFuture;
539 type Id = u64;
540
541 fn parse_id(text: &str) -> Result<u64, String> {
542 text.parse().map_err(|e| format!("{e}"))
543 }
544
545 fn id_text(id: &u64) -> Cow<'_, str> {
546 Cow::Owned(id.to_string())
547 }
548 }
549
550 fn upper(id: &str) -> MarketKey {
552 Upper::key(&Upper::parse_id(id).unwrap())
553 }
554
555 fn raw(text: &str) -> RawKey {
556 RawKey::parse(text).unwrap()
557 }
558
559 #[test]
560 fn text_splits_at_the_first_dot_and_the_first_colon() {
561 let key = raw("acme.spot:abc");
562 assert_eq!(key.venue().as_str(), "acme");
563 assert_eq!(key.product().as_str(), "spot");
564 assert_eq!(key.id(), "abc");
565
566 let dotted = raw("acme.spot:a.b");
567 assert_eq!(dotted.product().as_str(), "spot");
568 assert_eq!(dotted.id(), "a.b");
569 }
570
571 #[test]
572 fn a_synthetic_id_holding_a_second_colon_is_kept_whole() {
573 let key = raw("acme.events:FED-25DEC:yes");
574 assert_eq!(key.id(), "FED-25DEC:yes");
575 assert_eq!(key.to_string(), "acme.events:FED-25DEC:yes");
576 let parsed = Upper::parse(&raw("acme.spot:fed-25dec:yes")).unwrap();
577 assert_eq!(parsed.id(), "FED-25DEC:YES");
578 }
579
580 #[test]
581 fn malformed_text_names_its_fault() {
582 for (text, fault) in [
583 ("acme-spot-abc", "no `:`"),
584 ("acme:abc", "no `.`"),
585 ("Acme.spot:abc", "the venue"),
586 (".spot:abc", "the venue"),
587 ("acme.Spot:abc", "the product"),
588 ("acme.sp.ot:abc", "the product"),
589 ("acme.:abc", "the product"),
590 ("acme.spot:", "the id is empty"),
591 (" acme.spot:abc", "the venue"),
592 ] {
593 let err = RawKey::parse(text).unwrap_err();
594 assert!(
595 matches!(&err, KeyError::Malformed { text: t, reason } if t == text && reason.contains(fault)),
596 "{text:?}: {err}"
597 );
598 }
599 }
600
601 #[test]
602 fn a_venue_and_a_product_may_hold_digits_and_hyphens() {
603 let key = raw("acme-2.spot-x:a");
604 assert_eq!(key.venue().as_str(), "acme-2");
605 assert_eq!(key.product().as_str(), "spot-x");
606 assert_eq!(key.id(), "a");
607 }
608
609 #[test]
610 fn case_canonicalization_makes_two_spellings_one_key() {
611 let lower = Upper::parse(&raw("acme.spot:abcusd")).unwrap();
612 let upper = Upper::parse(&raw("acme.spot:ABCUSD")).unwrap();
613 assert_eq!(lower, upper);
614 assert_eq!(lower.id(), "ABCUSD");
615 let mut map = std::collections::HashMap::new();
616 map.insert(lower, 1);
617 assert_eq!(map.get(&upper), Some(&1));
618 }
619
620 #[test]
621 fn a_numbered_id_loses_its_leading_zeros() {
622 let key = Numbered::parse(&raw("acme.perpetual:007")).unwrap();
623 assert_eq!(key, Numbered::key(&7));
624 assert_eq!(key.id(), "7");
625 assert_eq!(Numbered::id(&key), Ok(7));
626 }
627
628 #[test]
629 fn a_product_refuses_another_products_key_and_an_id_it_cannot_read() {
630 let spot = upper("ABC");
631 assert_eq!(
632 Numbered::id(&spot),
633 Err(KeyError::WrongProduct {
634 expected: "acme.perpetual".to_owned(),
635 found: "acme.spot".to_owned(),
636 })
637 );
638 assert!(matches!(
639 Numbered::parse(&raw("other.perpetual:7")),
640 Err(KeyError::WrongProduct { .. })
641 ));
642 let err = Numbered::parse(&raw("acme.perpetual:seven")).unwrap_err();
643 assert!(
644 matches!(&err, KeyError::InvalidId { product, id, .. } if product == "acme.perpetual" && id == "seven"),
645 "{err}"
646 );
647 assert_eq!(err.class(), Class::InvalidRequest);
648 }
649
650 #[test]
651 fn a_key_and_a_raw_key_serialize_as_their_text() {
652 let key = upper("ABC");
653 assert_eq!(serde_json::to_string(&key).unwrap(), r#""acme.spot:ABC""#);
654 let raw: RawKey = serde_json::from_str(r#""acme.spot:abc""#).unwrap();
655 assert_eq!(raw, RawKey::parse("acme.spot:abc").unwrap());
656 assert_eq!(serde_json::to_string(&raw).unwrap(), r#""acme.spot:abc""#);
657 assert!(serde_json::from_str::<RawKey>(r#""acme-spot""#).is_err());
658 assert_eq!(Upper::parse(&raw).unwrap(), key);
659 }
660
661 #[test]
662 fn debug_shows_the_text_form() {
663 let key = upper("ABC");
664 assert_eq!(format!("{key:?}"), "MarketKey(acme.spot:ABC)");
665 assert_eq!(
666 format!("{:?}", raw("acme.spot:abc")),
667 "RawKey(acme.spot:abc)"
668 );
669 assert_eq!(format!("{:?}", Upper::VENUE), r#"VenueId("acme")"#);
670 }
671
672 #[test]
673 fn every_key_error_is_an_invalid_request() {
674 let errors = [
675 RawKey::parse("x").unwrap_err(),
676 KeyError::unknown(&raw("acme.spot:abc")),
677 Numbered::id(&upper("A")).unwrap_err(),
678 Numbered::parse(&raw("acme.perpetual:x")).unwrap_err(),
679 ];
680 for err in errors {
681 assert_eq!(err.class(), Class::InvalidRequest, "{err}");
682 assert!(err.is_fault());
683 assert!(!err.is_retriable());
684 }
685 }
686
687 #[test]
688 #[should_panic(expected = "an id is a lowercase letter")]
689 fn a_static_id_must_be_an_id() {
690 let _ = VenueId::from_static("Acme");
691 }
692
693 #[test]
694 fn a_key_carries_the_class_its_product_declares() {
695 assert_eq!(upper("ABC").class(), ProductClass::EventContract);
696 let parsed = Numbered::parse(&raw("acme.perpetual:007")).unwrap();
697 assert_eq!(parsed.class(), ProductClass::PerpetualFuture);
698 assert_eq!(parsed.class(), Numbered::CLASS);
699 assert_eq!(parsed.to_string(), "acme.perpetual:7");
701 }
702
703 #[test]
704 fn a_class_is_spelled_as_a_registration_spells_it() {
705 assert_eq!(ProductClass::EventContract.as_str(), "event-contract");
706 assert_eq!(
707 ProductClass::PerpetualFuture.to_string(),
708 "perpetual-future"
709 );
710 }
711}