use super::eic_code::{compute_check_char, EicCode};
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::bilanzkreis_id_schema")
)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "utoipa", schema(value_type = String))]
pub struct BilanzkreisId(
#[cfg_attr(feature = "validate", garde(custom(check_bilanzkreis_id)))] Box<str>,
);
#[cfg(feature = "validate")]
fn check_bilanzkreis_id(value: &str, _: &()) -> Result<(), garde::Error> {
validate(value).map_err(garde::Error::from)
}
fn validate(s: &str) -> Result<(), IdentifierError> {
EicCode::new(s)?;
let type_char = s.as_bytes()[2] as char;
if type_char != 'Z' {
return Err(IdentifierError::InvalidFormat {
description: format!(
"Bilanzkreis-ID requires EIC type character 'Z' at position 3, found '{type_char}'"
)
.into(),
});
}
Ok(())
}
impl BilanzkreisId {
#[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_prefix(prefix: &str) -> Result<Self, IdentifierError> {
if prefix.len() != 15 {
return Err(IdentifierError::InvalidLength {
expected: crate::error::LengthExpectation::Exact(15),
actual: prefix.len(),
});
}
if !prefix.is_ascii() {
return Err(IdentifierError::InvalidFormat {
description: "EIC prefix must contain only ASCII characters".into(),
});
}
let type_char = prefix.as_bytes()[2] as char;
if type_char != 'Z' {
return Err(IdentifierError::InvalidFormat {
description: format!(
"Bilanzkreis-ID requires EIC type character 'Z' at position 3, found '{type_char}'"
)
.into(),
});
}
let prefix_bytes: &[u8; 15] = prefix.as_bytes().try_into().expect("length checked above");
let check = compute_check_char(prefix_bytes).ok_or(IdentifierError::InvalidChecksum)?;
let full = format!("{prefix}{check}");
Self::new(&full)
}
}
impl From<BilanzkreisId> for String {
fn from(bk: BilanzkreisId) -> Self {
bk.0.into()
}
}
impl From<BilanzkreisId> for EicCode {
fn from(bk: BilanzkreisId) -> Self {
EicCode::new(bk.as_ref()).expect("BilanzkreisId is always a valid EicCode")
}
}
impl TryFrom<EicCode> for BilanzkreisId {
type Error = IdentifierError;
fn try_from(eic: EicCode) -> Result<Self, Self::Error> {
Self::new(eic.as_ref())
}
}
impl TryFrom<String> for BilanzkreisId {
type Error = IdentifierError;
fn try_from(s: String) -> Result<Self, Self::Error> {
Self::new(&s)
}
}
impl TryFrom<&str> for BilanzkreisId {
type Error = IdentifierError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
Self::new(s)
}
}
impl AsRef<str> for BilanzkreisId {
fn as_ref(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for BilanzkreisId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::str::FromStr for BilanzkreisId {
type Err = IdentifierError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s)
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for BilanzkreisId {
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 BilanzkreisId {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
struct Visitor;
impl<'de> serde::de::Visitor<'de> for Visitor {
type Value = BilanzkreisId;
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("a 16-character EIC code with type character 'Z' (Bilanzkreis-ID)")
}
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<BilanzkreisId, E> {
BilanzkreisId::new(v).map_err(E::custom)
}
}
d.deserialize_str(Visitor)
}
}
#[cfg(test)]
mod tests {
use super::*;
const Z_PREFIX: &str = "11ZVEW---------";
fn make_bk() -> BilanzkreisId {
BilanzkreisId::from_prefix(Z_PREFIX).expect("valid Z-type prefix")
}
#[test]
fn valid_z_type() {
let bk = make_bk();
assert_eq!(bk.as_ref().len(), 16);
assert_eq!(&bk.as_ref()[2..3], "Z");
}
#[test]
fn rejects_non_z_type() {
let area_eic = "10YDE-EON------1";
assert!(BilanzkreisId::new(area_eic).is_err());
assert!(BilanzkreisId::from_prefix("11YVEW---------").is_err());
}
#[test]
fn rejects_invalid_length() {
assert!(BilanzkreisId::new("11Z-----------").is_err()); assert!(BilanzkreisId::from_prefix("11Z-----------").is_err()); }
#[test]
fn from_prefix_short_rejected() {
let err = BilanzkreisId::from_prefix("11Z-----------").unwrap_err();
assert!(matches!(
err,
crate::error::IdentifierError::InvalidLength { .. }
));
}
#[test]
fn from_prefix_non_z_rejected() {
let err = BilanzkreisId::from_prefix("11YVEW---------").unwrap_err();
assert!(matches!(
err,
crate::error::IdentifierError::InvalidFormat { .. }
));
}
#[test]
fn roundtrip_serde() {
let bk = make_bk();
#[cfg(feature = "serde")]
{
let json = serde_json::to_string(&bk).unwrap();
let restored: BilanzkreisId = serde_json::from_str(&json).unwrap();
assert_eq!(bk, restored);
}
}
#[test]
fn from_eic_code_z_type() {
let bk = make_bk();
let eic: EicCode = bk.clone().into();
assert_eq!(eic.type_char(), 'Z');
let back = BilanzkreisId::try_from(eic).unwrap();
assert_eq!(bk, back);
}
#[test]
fn into_string() {
let bk = make_bk();
let s = String::from(bk.clone());
assert_eq!(s, bk.as_ref());
}
#[test]
fn display_equals_inner() {
let bk = make_bk();
assert_eq!(bk.to_string(), bk.as_ref());
}
}