use super::checksum::{compute_ascii_id_from_base, validate_ascii_id};
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(schema_with = "crate::schema_helpers::tr_id_schema")
)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "utoipa", schema(value_type = String))]
pub struct TrId(#[cfg_attr(feature = "validate", garde(custom(check_tr_id)))] Box<str>);
#[cfg(feature = "validate")]
fn check_tr_id(value: &str, _: &()) -> Result<(), garde::Error> {
validate_ascii_id(value, b'D').map_err(garde::Error::from)
}
impl TrId {
#[must_use = "the validated identifier is returned; ignoring it discards the result"]
pub fn new(s: &str) -> Result<Self, IdentifierError> {
validate_ascii_id(s, b'D')?;
Ok(Self(Box::from(s)))
}
pub fn from_base(base: &str) -> Result<Self, IdentifierError> {
let full = compute_ascii_id_from_base(base, b'D')?;
Ok(Self(Box::from(full.as_str())))
}
pub fn check_digit(base: &str) -> Result<u8, IdentifierError> {
let full = compute_ascii_id_from_base(base, b'D')?;
Ok(full.as_bytes().last().copied().expect("11 chars") - b'0')
}
}
impl TryFrom<String> for TrId {
type Error = IdentifierError;
fn try_from(s: String) -> Result<Self, Self::Error> {
Self::new(&s)
}
}
impl TryFrom<&str> for TrId {
type Error = IdentifierError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
Self::new(s)
}
}
impl AsRef<str> for TrId {
fn as_ref(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for TrId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::str::FromStr for TrId {
type Err = IdentifierError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s)
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for TrId {
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 TrId {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
struct Visitor;
impl<'de> serde::de::Visitor<'de> for Visitor {
type Value = TrId;
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(
"an 11-character Technische-Ressource-ID \
(Codetyp 'D' + 9 alphanumeric + ASCII-Verfahren check digit)",
)
}
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<TrId, E> {
TrId::new(v).map_err(|e| {
crate::identifiers::trace_identifier_deser_error("TrId", v, &e);
serde::de::Error::custom(e)
})
}
}
d.deserialize_str(Visitor)
}
}
#[cfg(test)]
mod tests {
use super::super::checksum::make_valid_ascii_id;
use super::*;
#[test]
fn from_base_all_zeros() {
let id = TrId::from_base("D000000000").unwrap();
assert_eq!(id.as_ref(), "D0000000002");
}
#[test]
fn from_base_trailing_one() {
let id = TrId::from_base("D000000001").unwrap();
assert_eq!(id.as_ref(), "D0000000010");
}
#[test]
fn from_base_all_ones_body() {
let id = TrId::from_base("D111111111").unwrap();
assert_eq!(id.as_ref(), "D1111111118");
}
#[test]
fn check_digit_method() {
assert_eq!(TrId::check_digit("D000000000").unwrap(), 2);
assert_eq!(TrId::check_digit("D000000001").unwrap(), 0);
}
#[test]
fn valid_ids_from_helper_pass() {
let bodies: &[[u8; 9]] = &[*b"000000000", *b"000000001", *b"111111111", *b"987654321"];
for body in bodies {
let s = make_valid_ascii_id(b'D', body);
TrId::new(&s).unwrap_or_else(|e| panic!("{s} should be valid: {e}"));
}
}
#[test]
fn round_trip() {
let s = "D0000000002";
assert_eq!(s.parse::<TrId>().unwrap().to_string(), s);
}
#[test]
fn wrong_length_fails() {
assert!(matches!(
TrId::new("D123456789").unwrap_err(),
IdentifierError::InvalidLength {
expected: LengthExpectation::Exact(11),
actual: 10
}
));
assert!(matches!(
TrId::new("D12345678901").unwrap_err(),
IdentifierError::InvalidLength {
expected: LengthExpectation::Exact(11),
actual: 12
}
));
assert!(matches!(
TrId::new("").unwrap_err(),
IdentifierError::InvalidLength {
expected: LengthExpectation::Exact(11),
actual: 0
}
));
}
#[test]
fn wrong_type_char_fails() {
for s in ["C0000000003", "E0000000019"] {
assert!(
matches!(
TrId::new(s).unwrap_err(),
IdentifierError::InvalidFormat { .. }
),
"{s} should fail with InvalidFormat"
);
}
assert!(matches!(
TrId::new("51238696780").unwrap_err(),
IdentifierError::InvalidFormat { .. }
));
}
#[test]
fn lowercase_body_fails() {
let err = TrId::new("D000b000002").unwrap_err();
assert!(matches!(
err,
IdentifierError::InvalidCharacter {
position: 4,
character: 'b'
}
));
}
#[test]
fn space_character_fails() {
let err = TrId::new("D000 000002").unwrap_err();
assert!(matches!(
err,
IdentifierError::InvalidCharacter {
position: 4,
character: ' '
}
));
}
#[test]
fn wrong_check_digit_fails() {
for wrong in ["D0000000000", "D0000000001", "D0000000003", "D0000000009"] {
assert!(
matches!(
TrId::new(wrong).unwrap_err(),
IdentifierError::InvalidChecksum
),
"{wrong} should fail with InvalidChecksum"
);
}
}
}