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::akiv_id_schema")
)]
#[cfg_attr(
feature = "schemars",
schemars(description = crate::identifiers::schema::AKIV_ID.description)
)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "utoipa", schema(
value_type = String,
pattern = r"^[!-~]{1,36}$",
example = "550e8400-e29b-41d4-a716-446655440000",
description = crate::identifiers::schema::AKIV_ID.description
))]
pub struct AkivId(#[cfg_attr(feature = "validate", garde(custom(check_akiv_id)))] Box<str>);
pub const AKIV_ID_MAX_LEN: usize = 36;
#[cfg(feature = "validate")]
fn check_akiv_id(value: &str, _: &()) -> Result<(), garde::Error> {
validate(value).map_err(garde::Error::from)
}
fn validate(s: &str) -> Result<(), IdentifierError> {
if s.is_empty() || s.len() > AKIV_ID_MAX_LEN {
return Err(IdentifierError::InvalidLength {
expected: crate::error::LengthExpectation::RangeInclusive {
min: 1,
max: AKIV_ID_MAX_LEN,
},
actual: s.len(),
});
}
for (i, c) in s.chars().enumerate() {
if !c.is_ascii() || c.is_ascii_control() || c == ' ' {
return Err(IdentifierError::InvalidCharacter {
position: i,
character: c,
});
}
}
Ok(())
}
impl AkivId {
#[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)))
}
}
impl_identifier_traits!(
AkivId,
"an Aktivierungsidentifikator of 1-36 printable ASCII characters"
);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_uuid_style() {
let id = AkivId::new("550e8400-e29b-41d4-a716-446655440000").unwrap();
assert_eq!(id.as_ref(), "550e8400-e29b-41d4-a716-446655440000");
}
#[test]
fn valid_short() {
AkivId::new("A").unwrap();
}
#[test]
fn valid_max_length() {
let s: String = "X".repeat(AKIV_ID_MAX_LEN);
AkivId::new(&s).unwrap();
}
#[test]
fn rejects_too_long() {
let s: String = "X".repeat(AKIV_ID_MAX_LEN + 1);
assert!(AkivId::new(&s).is_err());
}
#[test]
fn rejects_empty() {
assert!(AkivId::new("").is_err());
}
#[test]
fn rejects_space() {
assert!(AkivId::new("AKIV 001").is_err());
}
#[test]
fn rejects_control_char() {
assert!(AkivId::new("AKIV\x01001").is_err());
}
#[test]
fn display_roundtrip() {
let id = AkivId::new("AKIV-2026-00001").unwrap();
assert_eq!(id.to_string(), "AKIV-2026-00001");
}
#[test]
fn from_str() {
use std::str::FromStr;
let id = AkivId::from_str("AKIV-2026-00001").unwrap();
assert_eq!(id.as_ref(), "AKIV-2026-00001");
}
}