#[cfg(test)]
use super::checksum::bdew_check_digit;
use super::checksum::validate_11digit_bdew;
use crate::error::IdentifierError;
#[cfg(test)]
use crate::error::LengthExpectation;
#[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(with = "String"))]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "utoipa", schema(value_type = String))]
pub struct MaloId(#[cfg_attr(feature = "validate", garde(custom(check_malo_id)))] Box<str>);
#[cfg(feature = "validate")]
fn check_malo_id(value: &str, _: &()) -> Result<(), garde::Error> {
validate_11digit_bdew(value).map_err(garde::Error::from)
}
impl MaloId {
#[must_use = "the validated identifier is returned; ignoring it discards the result"]
pub fn new(s: &str) -> Result<Self, IdentifierError> {
validate_11digit_bdew(s)?;
Ok(Self(Box::from(s)))
}
}
impl TryFrom<String> for MaloId {
type Error = IdentifierError;
fn try_from(s: String) -> Result<Self, Self::Error> {
Self::new(&s)
}
}
impl TryFrom<&str> for MaloId {
type Error = IdentifierError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
Self::new(s)
}
}
impl AsRef<str> for MaloId {
fn as_ref(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for MaloId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::str::FromStr for MaloId {
type Err = IdentifierError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s)
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for MaloId {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&self.0)
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for MaloId {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
struct Visitor;
impl<'de> serde::de::Visitor<'de> for Visitor {
type Value = MaloId;
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("an 11-digit Marktlokations-ID (BDEW checksum)")
}
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<MaloId, E> {
MaloId::new(v).map_err(|e| {
crate::identifiers::trace_identifier_deser_error("MaloId", v, &e);
serde::de::Error::custom(e)
})
}
}
d.deserialize_str(Visitor)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_valid(prefix: &[u8; 10]) -> String {
super::super::checksum::make_valid_11digit(prefix)
}
#[test]
fn valid_constructed_ids_pass() {
let prefixes: &[[u8; 10]] = &[
[5, 1, 2, 3, 8, 6, 9, 6, 7, 8], [1, 2, 3, 4, 5, 6, 7, 8, 9, 0], [0, 9, 8, 7, 6, 5, 4, 3, 2, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [2, 2, 2, 2, 2, 2, 2, 2, 2, 2], [9, 9, 9, 9, 9, 9, 9, 9, 9, 9], [1, 2, 3, 4, 5, 1, 2, 3, 4, 5], [5, 6, 7, 8, 9, 5, 6, 7, 8, 9], [1, 3, 5, 7, 9, 2, 4, 6, 8, 0], [1, 0, 0, 0, 0, 0, 0, 0, 0, 0], ];
for prefix in prefixes {
let id_str = make_valid(prefix);
let id = MaloId::new(&id_str)
.unwrap_or_else(|e| panic!("{id_str} should be valid but got: {e}"));
assert_eq!(id.to_string().parse::<MaloId>().unwrap(), id);
}
}
#[test]
fn too_short_returns_invalid_length() {
let err = MaloId::new("1234567890").unwrap_err();
assert!(matches!(
err,
IdentifierError::InvalidLength {
expected: LengthExpectation::Exact(11),
actual: 10
}
));
}
#[test]
fn too_long_returns_invalid_length() {
let err = MaloId::new("123456789012").unwrap_err();
assert!(matches!(
err,
IdentifierError::InvalidLength {
expected: LengthExpectation::Exact(11),
actual: 12
}
));
}
#[test]
fn empty_returns_invalid_length() {
let err = MaloId::new("").unwrap_err();
assert!(matches!(
err,
IdentifierError::InvalidLength {
expected: LengthExpectation::Exact(11),
actual: 0
}
));
}
#[test]
fn non_digit_returns_invalid_character() {
let err = MaloId::new("1234X678901").unwrap_err();
assert!(matches!(
err,
IdentifierError::InvalidCharacter {
position: 4,
character: 'X'
}
));
}
#[test]
fn wrong_check_digit_returns_invalid_checksum() {
let err = MaloId::new("51238696781").unwrap_err();
assert!(matches!(err, IdentifierError::InvalidChecksum));
}
#[test]
fn multiple_wrong_check_digits() {
for wrong in ["51238696782", "51238696783", "51238696789"] {
let err = MaloId::new(wrong).unwrap_err();
assert!(matches!(err, IdentifierError::InvalidChecksum));
}
}
#[test]
fn space_character_returns_invalid_character() {
let err = MaloId::new("5123 696780").unwrap_err();
assert!(matches!(
err,
IdentifierError::InvalidCharacter {
position: 4,
character: ' '
}
));
}
#[test]
fn leading_hyphen_returns_invalid_character() {
let err = MaloId::new("-1238696780").unwrap_err();
assert!(matches!(
err,
IdentifierError::InvalidCharacter {
position: 0,
character: '-'
}
));
}
#[test]
fn display_equals_inner_string() {
let id_str = make_valid(&[5, 1, 2, 3, 8, 6, 9, 6, 7, 8]);
let id = MaloId::new(&id_str).unwrap();
assert_eq!(id.to_string(), id_str);
assert_eq!(id.as_ref(), id_str);
}
#[test]
fn try_from_str_delegates_to_new() {
let valid = make_valid(&[1, 2, 3, 4, 5, 1, 2, 3, 4, 5]);
assert!(MaloId::try_from(valid.as_str()).is_ok());
assert!(MaloId::try_from("bad").is_err());
}
#[test]
fn try_from_string_delegates_to_new() {
let valid = make_valid(&[9, 9, 9, 9, 9, 9, 9, 9, 9, 9]);
assert!(MaloId::try_from(valid).is_ok());
}
#[test]
fn compute_check_digit_is_deterministic() {
let prefix = [1u8, 2, 3, 4, 5, 6, 7, 8, 9, 0];
assert_eq!(bdew_check_digit(&prefix), bdew_check_digit(&prefix));
}
#[test]
fn all_zeros_check_is_zero() {
assert_eq!(bdew_check_digit(&[0u8; 10]), 0);
}
}