use std::fmt;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct VirtualMaloId(String);
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum InvalidVirtualMaloId {
#[error("a virtual MaLo id must be 1..={max} characters, got {len}")]
Length {
len: usize,
max: usize,
},
#[error(
"'{0}' is eleven digits and would collide with the BDEW MaLo-ID space; \
namespace virtual Marktlokationen instead"
)]
LooksLikeMaloId(String),
#[error("'{0}' contains a character outside [A-Za-z0-9._:-]")]
Character(String),
}
impl VirtualMaloId {
pub const MAX_LEN: usize = 64;
pub fn new(s: impl Into<String>) -> Result<Self, InvalidVirtualMaloId> {
let s = s.into();
if s.is_empty() || s.len() > Self::MAX_LEN {
return Err(InvalidVirtualMaloId::Length {
len: s.len(),
max: Self::MAX_LEN,
});
}
if s.len() == 11 && s.bytes().all(|b| b.is_ascii_digit()) {
return Err(InvalidVirtualMaloId::LooksLikeMaloId(s));
}
if !s
.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b':' | b'-'))
{
return Err(InvalidVirtualMaloId::Character(s));
}
Ok(Self(s))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for VirtualMaloId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct SessionId(String);
impl SessionId {
#[must_use]
pub fn new(s: impl Into<String>) -> Self {
Self(s.into())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for SessionId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct TokenRef(String);
impl TokenRef {
#[must_use]
pub fn from_keyed_hash(hash: impl Into<String>) -> Self {
Self(hash.into())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for TokenRef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_eleven_digit_id_is_refused() {
let err = VirtualMaloId::new("51238297068").unwrap_err();
assert!(matches!(err, InvalidVirtualMaloId::LooksLikeMaloId(_)));
}
#[test]
fn eleven_digits_are_refused_even_with_a_wrong_check_digit() {
assert!(VirtualMaloId::new("51238297069").is_err());
}
#[test]
fn eleven_characters_that_are_not_all_digits_are_fine() {
assert!(VirtualMaloId::new("veh-1234567").is_ok());
}
#[test]
fn other_digit_lengths_are_fine() {
assert!(VirtualMaloId::new("512382970").is_ok());
assert!(VirtualMaloId::new("512382970699").is_ok());
}
#[test]
fn empty_and_overlong_are_refused() {
assert!(VirtualMaloId::new("").is_err());
assert!(VirtualMaloId::new("x".repeat(VirtualMaloId::MAX_LEN + 1)).is_err());
}
#[test]
fn separators_are_allowed_but_spaces_are_not() {
assert!(VirtualMaloId::new("cpo:fleet.42_a-b").is_ok());
assert!(VirtualMaloId::new("cpo fleet").is_err());
}
}