use crate::error::IdentifierError;
#[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::tranchennummer_id_schema")
)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "utoipa", schema(value_type = String))]
pub struct TranchennummerId(
#[cfg_attr(feature = "validate", garde(custom(check_tranchennummer_id)))] Box<str>,
);
pub const TRANCHENNUMMER_MAX: u32 = 999_999;
#[cfg(feature = "validate")]
fn check_tranchennummer_id(value: &str, _: &()) -> Result<(), garde::Error> {
validate(value).map_err(garde::Error::from)
}
fn validate(s: &str) -> Result<(), IdentifierError> {
use crate::error::LengthExpectation;
if s.is_empty() || s.len() > 6 {
return Err(IdentifierError::InvalidLength {
expected: LengthExpectation::RangeInclusive { min: 1, max: 6 },
actual: s.len(),
});
}
for (i, c) in s.chars().enumerate() {
if !c.is_ascii_digit() {
return Err(IdentifierError::InvalidCharacter {
position: i,
character: c,
});
}
}
if s.len() > 1 && s.starts_with('0') {
return Err(IdentifierError::InvalidFormat {
description: "TranchennummerId must not have leading zeros (except \"0\")".into(),
});
}
Ok(())
}
impl TranchennummerId {
#[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)))
}
pub fn from_value(value: u32) -> Result<Self, IdentifierError> {
if value > TRANCHENNUMMER_MAX {
return Err(IdentifierError::InvalidFormat {
description: format!(
"TranchennummerId value {value} exceeds maximum {TRANCHENNUMMER_MAX}"
)
.into(),
});
}
Ok(Self(Box::from(value.to_string().as_str())))
}
#[must_use]
pub fn value(&self) -> u32 {
self.0.parse().expect("validated digits")
}
}
impl TryFrom<u32> for TranchennummerId {
type Error = IdentifierError;
fn try_from(v: u32) -> Result<Self, Self::Error> {
Self::from_value(v)
}
}
impl From<TranchennummerId> for u32 {
fn from(t: TranchennummerId) -> Self {
t.value()
}
}
impl_identifier_traits!(
TranchennummerId,
"a Tranchennummer of 1-6 decimal digits without leading zeros"
);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_single_digit() {
let t = TranchennummerId::new("1").unwrap();
assert_eq!(t.value(), 1);
}
#[test]
fn valid_zero() {
let t = TranchennummerId::new("0").unwrap();
assert_eq!(t.value(), 0);
}
#[test]
fn valid_max() {
let t = TranchennummerId::from_value(TRANCHENNUMMER_MAX).unwrap();
assert_eq!(t.value(), TRANCHENNUMMER_MAX);
assert_eq!(t.as_ref(), "999999");
}
#[test]
fn from_value_roundtrip() {
for v in [0u32, 1, 42, 100, 9999, 999_999] {
let t = TranchennummerId::from_value(v).unwrap();
assert_eq!(t.value(), v);
}
}
#[test]
fn rejects_empty() {
assert!(TranchennummerId::new("").is_err());
}
#[test]
fn rejects_leading_zero() {
assert!(TranchennummerId::new("01").is_err());
assert!(TranchennummerId::new("007").is_err());
}
#[test]
fn rejects_non_digit() {
assert!(TranchennummerId::new("1A").is_err());
assert!(TranchennummerId::new("T1").is_err());
}
#[test]
fn rejects_too_long() {
assert!(TranchennummerId::new("1234567").is_err()); }
#[test]
fn rejects_from_value_overflow() {
assert!(TranchennummerId::from_value(1_000_000).is_err());
}
#[test]
fn display_roundtrip() {
let t = TranchennummerId::new("42").unwrap();
assert_eq!(t.to_string(), "42");
}
#[test]
fn try_from_u32() {
let t = TranchennummerId::try_from(7u32).unwrap();
assert_eq!(t.value(), 7);
}
}