use crate::error::{IdentifierError, LengthExpectation};
const LEN: usize = 33;
fn validate(s: &str) -> Result<(), IdentifierError> {
if s.len() != LEN {
return Err(IdentifierError::InvalidLength {
expected: LengthExpectation::Exact(LEN),
actual: s.len(),
});
}
for c in s.chars().take(2) {
if !c.is_ascii_uppercase() {
return Err(IdentifierError::InvalidFormat {
description:
"first two characters must be uppercase ISO 3166-1 country code (e.g. \"DE\")"
.into(),
});
}
}
for (i, c) in s.chars().enumerate().skip(2) {
if !c.is_ascii_alphanumeric() {
return Err(IdentifierError::InvalidCharacter {
position: i,
character: c,
});
}
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "validate", derive(garde::Validate))]
#[cfg_attr(feature = "validate", garde(allow_unvalidated))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(
feature = "schemars",
schemars(schema_with = "crate::schema_helpers::melo_id_schema")
)]
#[cfg_attr(
feature = "schemars",
schemars(description = crate::identifiers::schema::MELO_ID.description)
)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "utoipa", schema(
value_type = String,
pattern = r"^[A-Z]{2}[A-Za-z0-9]{31}$",
example = "DE0000000000000000000000000000001",
description = crate::identifiers::schema::MELO_ID.description
))]
pub struct MeloId(#[cfg_attr(feature = "validate", garde(custom(check_melo_id)))] Box<str>);
#[cfg(feature = "validate")]
fn check_melo_id(value: &str, _: &()) -> Result<(), garde::Error> {
validate(value).map_err(garde::Error::from)
}
impl MeloId {
#[must_use = "the validated identifier is returned; ignoring it discards the result"]
pub fn new(s: &str) -> Result<Self, IdentifierError> {
validate(s)?;
Ok(Self(Box::from(s)))
}
#[must_use]
pub fn country_code(&self) -> &str {
&self.0[..2]
}
#[must_use]
pub fn is_german(&self) -> bool {
self.country_code() == "DE"
}
}
impl_identifier_traits!(MeloId, "a 33-character Messlokations-ID");
#[cfg(test)]
mod tests {
use super::*;
const VALID_33: &str = "DE0000000000000000000000000000001";
#[test]
fn valid_de_prefix_passes() {
assert!(MeloId::new(VALID_33).is_ok());
assert!(MeloId::new("AB0000000000000000000000000000000").is_ok());
assert!(MeloId::new("DEABCDEFGHIJKLMNOPQRSTUVWXYZ12345").is_ok());
assert!(MeloId::new("DEabcdefghijklmnopqrstuvwxyz12345").is_ok());
}
#[test]
fn lowercase_country_prefix_fails() {
let err = MeloId::new("de0000000000000000000000000000001").unwrap_err();
assert!(
matches!(err, IdentifierError::InvalidFormat { .. }),
"expected InvalidFormat, got {err:?}"
);
let err2 = MeloId::new("De0000000000000000000000000000001").unwrap_err();
assert!(
matches!(err2, IdentifierError::InvalidFormat { .. }),
"expected InvalidFormat, got {err2:?}"
);
let err3 = MeloId::new("1E0000000000000000000000000000001").unwrap_err();
assert!(
matches!(err3, IdentifierError::InvalidFormat { .. }),
"expected InvalidFormat, got {err3:?}"
);
}
#[test]
fn too_short_fails() {
let short: String = "A".repeat(32);
assert!(matches!(
MeloId::new(&short).unwrap_err(),
IdentifierError::InvalidLength {
expected: LengthExpectation::Exact(33),
actual: 32
}
));
}
#[test]
fn too_long_fails() {
let long: String = "A".repeat(34);
assert!(matches!(
MeloId::new(&long).unwrap_err(),
IdentifierError::InvalidLength {
expected: LengthExpectation::Exact(33),
actual: 34
}
));
}
#[test]
fn hyphen_in_value_fails() {
let with_hyphen = "DE000000000000000000000000000000-";
let err = MeloId::new(with_hyphen).unwrap_err();
assert!(matches!(
err,
IdentifierError::InvalidCharacter {
position: 32,
character: '-'
}
));
}
#[test]
fn space_fails() {
let with_space = "DE00000000000000000000000000000 1";
assert_eq!(with_space.len(), 33);
let err = MeloId::new(with_space).unwrap_err();
assert!(matches!(err, IdentifierError::InvalidCharacter { .. }));
}
#[test]
fn round_trip() {
let id = MeloId::new(VALID_33).unwrap();
assert_eq!(id.to_string().parse::<MeloId>().unwrap(), id);
}
}