use std::borrow::Cow;
use std::error::Error;
use std::fmt;
use std::str::FromStr;
use std::sync::Arc;
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use crate::{Class, Classify};
const fn is_id(id: &str) -> bool {
let bytes = id.as_bytes();
if bytes.is_empty() || !bytes[0].is_ascii_lowercase() {
return false;
}
let mut i = 1;
while i < bytes.len() {
let b = bytes[i];
if !(b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-') {
return false;
}
i += 1;
}
true
}
macro_rules! id_type {
($(#[$doc:meta])* $name:ident, $what:literal) => {
$(#[$doc])*
#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct $name(Cow<'static, str>);
impl $name {
#[doc = concat!("A ", $what, " id known when the program is built.")]
pub const fn from_static(id: &'static str) -> Self {
assert!(
is_id(id),
"an id is a lowercase letter, then lowercase letters, digits and `-`"
);
Self(Cow::Borrowed(id))
}
#[doc = concat!("The ", $what, " id.")]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl fmt::Debug for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple(stringify!($name)).field(&self.as_str()).finish()
}
}
};
}
id_type! {
VenueId, "venue"
}
id_type! {
ProductId, "product"
}
#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct MarketKey {
venue: VenueId,
product: ProductId,
id: Arc<str>,
}
impl MarketKey {
pub fn venue(&self) -> &VenueId {
&self.venue
}
pub fn product(&self) -> &ProductId {
&self.product
}
pub fn id(&self) -> &str {
&self.id
}
}
impl fmt::Display for MarketKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}:{}", self.venue, self.product, self.id)
}
}
impl fmt::Debug for MarketKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("MarketKey")
.field(&format_args!("{self}"))
.finish()
}
}
impl Serialize for MarketKey {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_str(self)
}
}
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct RawKey {
venue: VenueId,
product: ProductId,
id: String,
}
impl RawKey {
pub fn parse(text: &str) -> Result<Self, KeyError> {
let malformed = |reason| KeyError::Malformed {
text: text.to_owned(),
reason,
};
let (scope, id) = text
.split_once(':')
.ok_or_else(|| malformed("no `:` before the id"))?;
let (venue, product) = scope
.split_once('.')
.ok_or_else(|| malformed("no `.` between the venue and the product"))?;
if !is_id(venue) {
return Err(malformed(
"the venue is not a lowercase letter followed by lowercase letters, digits and `-`",
));
}
if !is_id(product) {
return Err(malformed(
"the product is not a lowercase letter followed by lowercase letters, digits and `-`",
));
}
if id.is_empty() {
return Err(malformed("the id is empty"));
}
Ok(Self {
venue: VenueId(Cow::Owned(venue.to_owned())),
product: ProductId(Cow::Owned(product.to_owned())),
id: id.to_owned(),
})
}
pub fn venue(&self) -> &VenueId {
&self.venue
}
pub fn product(&self) -> &ProductId {
&self.product
}
pub fn id(&self) -> &str {
&self.id
}
}
impl FromStr for RawKey {
type Err = KeyError;
fn from_str(text: &str) -> Result<Self, Self::Err> {
Self::parse(text)
}
}
impl fmt::Display for RawKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}:{}", self.venue, self.product, self.id)
}
}
impl fmt::Debug for RawKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("RawKey")
.field(&format_args!("{self}"))
.finish()
}
}
impl Serialize for RawKey {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_str(self)
}
}
impl<'de> Deserialize<'de> for RawKey {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let text = Cow::<'de, str>::deserialize(deserializer)?;
Self::parse(&text).map_err(de::Error::custom)
}
}
pub trait KeySpace {
const VENUE: VenueId;
const PRODUCT: ProductId;
type Id;
fn parse_id(text: &str) -> Result<Self::Id, String>;
fn id_text(id: &Self::Id) -> Cow<'_, str>;
fn key(id: &Self::Id) -> MarketKey {
MarketKey {
venue: Self::VENUE,
product: Self::PRODUCT,
id: Arc::from(Self::id_text(id).as_ref()),
}
}
fn parse(raw: &RawKey) -> Result<MarketKey, KeyError> {
id_of::<Self>(raw.venue(), raw.product(), raw.id()).map(|id| Self::key(&id))
}
fn id(key: &MarketKey) -> Result<Self::Id, KeyError> {
id_of::<Self>(key.venue(), key.product(), key.id())
}
}
fn id_of<K: KeySpace + ?Sized>(
venue: &VenueId,
product: &ProductId,
id: &str,
) -> Result<K::Id, KeyError> {
if venue != &K::VENUE || product != &K::PRODUCT {
return Err(KeyError::WrongProduct {
expected: format!("{}.{}", K::VENUE, K::PRODUCT),
found: format!("{venue}.{product}"),
});
}
K::parse_id(id).map_err(|reason| KeyError::InvalidId {
product: format!("{}.{}", K::VENUE, K::PRODUCT),
id: id.to_owned(),
reason,
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum KeyError {
Malformed {
text: String,
reason: &'static str,
},
UnknownProduct {
venue: String,
product: String,
},
WrongProduct {
expected: String,
found: String,
},
InvalidId {
product: String,
id: String,
reason: String,
},
}
impl KeyError {
pub fn unknown(raw: &RawKey) -> Self {
Self::UnknownProduct {
venue: raw.venue().as_str().to_owned(),
product: raw.product().as_str().to_owned(),
}
}
}
impl fmt::Display for KeyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Malformed { text, reason } => {
write!(
f,
"{text:?} is not a market key (`venue.product:id`): {reason}"
)
}
Self::UnknownProduct { venue, product } => {
write!(f, "no product this build enables is {venue}.{product}")
}
Self::WrongProduct { expected, found } => {
write!(f, "a {found} key was given where a {expected} key belongs")
}
Self::InvalidId {
product,
id,
reason,
} => write!(f, "{id:?} is not a {product} id: {reason}"),
}
}
}
impl Error for KeyError {}
impl Classify for KeyError {
fn class(&self) -> Class {
Class::InvalidRequest
}
}
#[cfg(test)]
mod tests {
use super::*;
struct Upper;
struct UpperId(String);
impl KeySpace for Upper {
const VENUE: VenueId = VenueId::from_static("acme");
const PRODUCT: ProductId = ProductId::from_static("spot");
type Id = UpperId;
fn parse_id(text: &str) -> Result<UpperId, String> {
if text.is_ascii() {
Ok(UpperId(text.to_ascii_uppercase()))
} else {
Err("an id is ASCII".to_owned())
}
}
fn id_text(id: &UpperId) -> Cow<'_, str> {
Cow::Borrowed(&id.0)
}
}
struct Numbered;
impl KeySpace for Numbered {
const VENUE: VenueId = VenueId::from_static("acme");
const PRODUCT: ProductId = ProductId::from_static("perpetual");
type Id = u64;
fn parse_id(text: &str) -> Result<u64, String> {
text.parse().map_err(|e| format!("{e}"))
}
fn id_text(id: &u64) -> Cow<'_, str> {
Cow::Owned(id.to_string())
}
}
fn upper(id: &str) -> MarketKey {
Upper::key(&Upper::parse_id(id).unwrap())
}
fn raw(text: &str) -> RawKey {
RawKey::parse(text).unwrap()
}
#[test]
fn text_splits_at_the_first_dot_and_the_first_colon() {
let key = raw("acme.spot:abc");
assert_eq!(key.venue().as_str(), "acme");
assert_eq!(key.product().as_str(), "spot");
assert_eq!(key.id(), "abc");
let dotted = raw("acme.spot:a.b");
assert_eq!(dotted.product().as_str(), "spot");
assert_eq!(dotted.id(), "a.b");
}
#[test]
fn a_synthetic_id_holding_a_second_colon_is_kept_whole() {
let key = raw("acme.events:FED-25DEC:yes");
assert_eq!(key.id(), "FED-25DEC:yes");
assert_eq!(key.to_string(), "acme.events:FED-25DEC:yes");
let parsed = Upper::parse(&raw("acme.spot:fed-25dec:yes")).unwrap();
assert_eq!(parsed.id(), "FED-25DEC:YES");
}
#[test]
fn malformed_text_names_its_fault() {
for (text, fault) in [
("acme-spot-abc", "no `:`"),
("acme:abc", "no `.`"),
("Acme.spot:abc", "the venue"),
(".spot:abc", "the venue"),
("acme.Spot:abc", "the product"),
("acme.sp.ot:abc", "the product"),
("acme.:abc", "the product"),
("acme.spot:", "the id is empty"),
(" acme.spot:abc", "the venue"),
] {
let err = RawKey::parse(text).unwrap_err();
assert!(
matches!(&err, KeyError::Malformed { text: t, reason } if t == text && reason.contains(fault)),
"{text:?}: {err}"
);
}
}
#[test]
fn a_venue_and_a_product_may_hold_digits_and_hyphens() {
let key = raw("acme-2.spot-x:a");
assert_eq!(key.venue().as_str(), "acme-2");
assert_eq!(key.product().as_str(), "spot-x");
assert_eq!(key.id(), "a");
}
#[test]
fn case_canonicalization_makes_two_spellings_one_key() {
let lower = Upper::parse(&raw("acme.spot:abcusd")).unwrap();
let upper = Upper::parse(&raw("acme.spot:ABCUSD")).unwrap();
assert_eq!(lower, upper);
assert_eq!(lower.id(), "ABCUSD");
let mut map = std::collections::HashMap::new();
map.insert(lower, 1);
assert_eq!(map.get(&upper), Some(&1));
}
#[test]
fn a_numbered_id_loses_its_leading_zeros() {
let key = Numbered::parse(&raw("acme.perpetual:007")).unwrap();
assert_eq!(key, Numbered::key(&7));
assert_eq!(key.id(), "7");
assert_eq!(Numbered::id(&key), Ok(7));
}
#[test]
fn a_product_refuses_another_products_key_and_an_id_it_cannot_read() {
let spot = upper("ABC");
assert_eq!(
Numbered::id(&spot),
Err(KeyError::WrongProduct {
expected: "acme.perpetual".to_owned(),
found: "acme.spot".to_owned(),
})
);
assert!(matches!(
Numbered::parse(&raw("other.perpetual:7")),
Err(KeyError::WrongProduct { .. })
));
let err = Numbered::parse(&raw("acme.perpetual:seven")).unwrap_err();
assert!(
matches!(&err, KeyError::InvalidId { product, id, .. } if product == "acme.perpetual" && id == "seven"),
"{err}"
);
assert_eq!(err.class(), Class::InvalidRequest);
}
#[test]
fn a_key_and_a_raw_key_serialize_as_their_text() {
let key = upper("ABC");
assert_eq!(serde_json::to_string(&key).unwrap(), r#""acme.spot:ABC""#);
let raw: RawKey = serde_json::from_str(r#""acme.spot:abc""#).unwrap();
assert_eq!(raw, RawKey::parse("acme.spot:abc").unwrap());
assert_eq!(serde_json::to_string(&raw).unwrap(), r#""acme.spot:abc""#);
assert!(serde_json::from_str::<RawKey>(r#""acme-spot""#).is_err());
assert_eq!(Upper::parse(&raw).unwrap(), key);
}
#[test]
fn debug_shows_the_text_form() {
let key = upper("ABC");
assert_eq!(format!("{key:?}"), "MarketKey(acme.spot:ABC)");
assert_eq!(
format!("{:?}", raw("acme.spot:abc")),
"RawKey(acme.spot:abc)"
);
assert_eq!(format!("{:?}", Upper::VENUE), r#"VenueId("acme")"#);
}
#[test]
fn every_key_error_is_an_invalid_request() {
let errors = [
RawKey::parse("x").unwrap_err(),
KeyError::unknown(&raw("acme.spot:abc")),
Numbered::id(&upper("A")).unwrap_err(),
Numbered::parse(&raw("acme.perpetual:x")).unwrap_err(),
];
for err in errors {
assert_eq!(err.class(), Class::InvalidRequest, "{err}");
assert!(err.is_fault());
assert!(!err.is_retriable());
}
}
#[test]
#[should_panic(expected = "an id is a lowercase letter")]
fn a_static_id_must_be_an_id() {
let _ = VenueId::from_static("Acme");
}
}