use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum CommodityCodeError {
#[error("commodity code must be 6 (HS), 8 (CN) or 10 (TARIC) ASCII digits, got '{0}'")]
InvalidFormat(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct CommodityCode(String);
impl CommodityCode {
pub fn parse(s: &str) -> Result<Self, CommodityCodeError> {
let trimmed = s.trim();
let valid_length = matches!(trimmed.len(), 6 | 8 | 10);
if !valid_length || !trimmed.bytes().all(|b| b.is_ascii_digit()) {
return Err(CommodityCodeError::InvalidFormat(s.to_owned()));
}
Ok(Self(trimmed.to_owned()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn hs_subheading(&self) -> &str {
&self.0[..6]
}
}
impl std::fmt::Display for CommodityCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_three_tariff_levels_parse() {
for code in ["850760", "85076000", "8507600090"] {
let parsed = CommodityCode::parse(code).expect("must parse");
assert_eq!(parsed.as_str(), code);
assert_eq!(parsed.hs_subheading(), "850760");
}
}
#[test]
fn surrounding_whitespace_is_trimmed() {
assert_eq!(
CommodityCode::parse(" 85076000 ").unwrap().as_str(),
"85076000"
);
}
#[test]
fn separators_are_refused_not_stripped() {
for code in ["8507 60 00", "8507.60.00", "8507-60-00"] {
assert!(
CommodityCode::parse(code).is_err(),
"{code} must be refused rather than compacted"
);
}
}
#[test]
fn wrong_lengths_are_refused() {
for code in ["8507", "8507600", "850760009", "850760009012", ""] {
assert!(
CommodityCode::parse(code).is_err(),
"{code} must be refused"
);
}
}
#[test]
fn round_trips_as_a_bare_json_string() {
let code = CommodityCode::parse("85076000").unwrap();
let json = serde_json::to_string(&code).unwrap();
assert_eq!(json, "\"85076000\"");
assert_eq!(
serde_json::from_str::<CommodityCode>(&json).unwrap(),
code,
"serde(transparent): the wire form is the code itself"
);
}
}