use super::checksum::{compute_numeric_id_from_base, validate_numeric_id};
use crate::error::IdentifierError;
const LEN: usize = 13;
const MIN_FIRST_DIGIT: u8 = 0;
macro_rules! lokationsbuendel_code {
(
$ty:ident,
$schema_fn:literal,
$schema_meta:expr,
$pattern:literal,
$expecting:literal,
$example_base:literal,
$example_full:literal,
$(#[$doc:meta])*
) => {
$(#[$doc])*
#[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 = $schema_fn))]
#[cfg_attr(feature = "schemars", schemars(description = $schema_meta.description))]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "utoipa", schema(
value_type = String,
pattern = $pattern,
example = $example_full,
description = $schema_meta.description
))]
pub struct $ty(
#[cfg_attr(feature = "validate", garde(custom(validate_code)))] Box<str>,
);
impl $ty {
#[must_use = "the validated code is returned; ignoring it discards the result"]
pub fn new(s: &str) -> Result<Self, IdentifierError> {
validate_numeric_id(s, LEN, MIN_FIRST_DIGIT)?;
Ok(Self(Box::from(s)))
}
#[doc = concat!("use rubo4e::identifiers::", stringify!($ty), ";")]
#[doc = concat!("let code = ", stringify!($ty), "::from_base(\"", $example_base, "\").unwrap();")]
#[doc = concat!("assert_eq!(code.as_ref(), \"", $example_full, "\");")]
#[must_use = "the validated code is returned; ignoring it discards the result"]
pub fn from_base(base: &str) -> Result<Self, IdentifierError> {
compute_numeric_id_from_base(base, LEN, MIN_FIRST_DIGIT).map(|s| Self(s.into_boxed_str()))
}
pub fn check_digit(base: &str) -> Result<u8, IdentifierError> {
let full = compute_numeric_id_from_base(base, LEN, MIN_FIRST_DIGIT)?;
Ok(full.as_bytes()[LEN - 1] - b'0')
}
#[must_use]
pub fn grouped(&self) -> String {
let s: &str = &self.0;
format!("{} {} {} {}", &s[0..4], &s[4..9], &s[9..12], &s[12..13])
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl_identifier_traits!($ty, $expecting);
};
}
#[cfg(feature = "validate")]
fn validate_code(value: &str, _: &()) -> Result<(), garde::Error> {
validate_numeric_id(value, LEN, MIN_FIRST_DIGIT).map_err(garde::Error::from)
}
lokationsbuendel_code!(
Lokationsbuendelcode,
"crate::schema_helpers::lokationsbuendel_code_schema",
crate::identifiers::schema::LOKATIONSBUENDEL_CODE,
r"^[0-9]{13}$",
"a 13-digit Lokationsbündelstruktur code with a valid BDEW check digit",
"999200000002",
"9992000000026",
);
lokationsbuendel_code!(
LokationsbuendelObjektcode,
"crate::schema_helpers::lokationsbuendel_objektcode_schema",
crate::identifiers::schema::LOKATIONSBUENDEL_OBJEKTCODE,
r"^[0-9]{13}$",
"a 13-digit Lokationsbündel object code with a valid BDEW check digit",
"999200000101",
"9992000001016",
);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn published_structure_codes_all_verify() {
for code in crate::lokationsbuendel::STRUKTUREN {
assert!(
Lokationsbuendelcode::new(code.code).is_ok(),
"{} must satisfy the BDEW §8.1 check digit",
code.code
);
}
}
#[test]
fn published_object_codes_all_verify() {
for rolle in crate::lokationsbuendel::OBJEKTROLLEN {
assert!(
LokationsbuendelObjektcode::new(rolle.code).is_ok(),
"{} must satisfy the BDEW §8.1 check digit",
rolle.code
);
}
}
#[test]
fn grouped_matches_the_codelist_printing() {
let c = Lokationsbuendelcode::new("9992000000026").unwrap();
assert_eq!(c.grouped(), "9992 00000 002 6");
let o = LokationsbuendelObjektcode::new("9992000001090").unwrap();
assert_eq!(o.grouped(), "9992 00000 109 0");
}
#[test]
fn wrong_length_and_checksum_are_rejected() {
assert!(matches!(
Lokationsbuendelcode::new("999200000002"),
Err(IdentifierError::InvalidLength { .. })
));
assert!(matches!(
Lokationsbuendelcode::new("9992000000027"),
Err(IdentifierError::InvalidChecksum)
));
assert!(matches!(
LokationsbuendelObjektcode::new("999200000101X"),
Err(IdentifierError::InvalidCharacter { .. })
));
}
#[test]
fn from_base_appends_the_check_digit() {
assert_eq!(
LokationsbuendelObjektcode::from_base("999200000101").unwrap(),
LokationsbuendelObjektcode::new("9992000001016").unwrap()
);
assert_eq!(
LokationsbuendelObjektcode::check_digit("999200000101").unwrap(),
6
);
}
#[test]
fn the_two_codes_are_distinct_types() {
let s = Lokationsbuendelcode::new("9992000000026").unwrap();
let o = LokationsbuendelObjektcode::new("9992000000026").unwrap();
assert_eq!(s.as_str(), o.as_str());
}
}