use serde::{Deserialize, Serialize};
use thiserror::Error;
#[must_use]
pub fn gs1_check_digit(data_digits: &[u8]) -> u8 {
let sum: u32 = data_digits
.iter()
.rev()
.enumerate()
.map(|(i, &d)| u32::from(d) * if i % 2 == 0 { 3 } else { 1 })
.sum();
((10 - (sum % 10)) % 10) as u8
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum GtinError {
#[error("GTIN must be exactly 14 ASCII digits, got '{0}'")]
InvalidFormat(String),
#[error("GTIN check digit invalid for '{gtin}': expected {expected}, got {actual}")]
InvalidCheckDigit {
gtin: String,
expected: u8,
actual: u8,
},
}
enum Gs1KeyCheck {
InvalidFormat,
InvalidCheckDigit { expected: u8, actual: u8 },
}
fn check_gs1_key(s: &str, len: usize) -> Result<(), Gs1KeyCheck> {
if s.len() != len || !s.bytes().all(|b| b.is_ascii_digit()) {
return Err(Gs1KeyCheck::InvalidFormat);
}
debug_assert!(
len <= 14,
"check_gs1_key only supports keys up to 14 digits"
);
let mut digits = [0u8; 14];
for (i, b) in s.bytes().enumerate() {
digits[i] = b - b'0';
}
let expected = gs1_check_digit(&digits[..len - 1]);
if digits[len - 1] != expected {
return Err(Gs1KeyCheck::InvalidCheckDigit {
expected,
actual: digits[len - 1],
});
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Gtin(String);
impl Gtin {
pub fn parse(s: &str) -> Result<Self, GtinError> {
match check_gs1_key(s, 14) {
Ok(()) => Ok(Self(s.to_owned())),
Err(Gs1KeyCheck::InvalidFormat) => Err(GtinError::InvalidFormat(s.to_owned())),
Err(Gs1KeyCheck::InvalidCheckDigit { expected, actual }) => {
Err(GtinError::InvalidCheckDigit {
gtin: s.to_owned(),
expected,
actual,
})
}
}
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for Gtin {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl PartialEq<str> for Gtin {
fn eq(&self, other: &str) -> bool {
self.0 == other
}
}
impl Serialize for Gtin {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&self.0)
}
}
impl<'de> Deserialize<'de> for Gtin {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let s = String::deserialize(d)?;
Self::parse(&s).map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum GlnError {
#[error("GLN must be exactly 13 ASCII digits, got '{0}'")]
InvalidFormat(String),
#[error("GLN check digit invalid for '{gln}': expected {expected}, got {actual}")]
InvalidCheckDigit {
gln: String,
expected: u8,
actual: u8,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Gln(String);
impl Gln {
pub fn parse(s: &str) -> Result<Self, GlnError> {
match check_gs1_key(s, 13) {
Ok(()) => Ok(Self(s.to_owned())),
Err(Gs1KeyCheck::InvalidFormat) => Err(GlnError::InvalidFormat(s.to_owned())),
Err(Gs1KeyCheck::InvalidCheckDigit { expected, actual }) => {
Err(GlnError::InvalidCheckDigit {
gln: s.to_owned(),
expected,
actual,
})
}
}
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[cfg(test)]
mod tests {
use super::*;
const VALID: &str = "09506000134352";
#[test]
fn valid_gtin_parses() {
assert!(Gtin::parse(VALID).is_ok());
}
#[test]
fn wrong_length_rejected() {
assert!(matches!(
Gtin::parse("095060001343"),
Err(GtinError::InvalidFormat(_))
));
}
#[test]
fn non_digits_rejected() {
assert!(matches!(
Gtin::parse("0950600013435X"),
Err(GtinError::InvalidFormat(_))
));
}
#[test]
fn bad_check_digit_rejected() {
assert!(matches!(
Gtin::parse("09506000134351"),
Err(GtinError::InvalidCheckDigit { .. })
));
}
#[test]
fn display_equals_inner_string() {
let g = Gtin::parse(VALID).unwrap();
assert_eq!(g.to_string(), VALID);
}
#[test]
fn partial_eq_str() {
let g = Gtin::parse(VALID).unwrap();
assert_eq!(g, *VALID); assert_eq!(g.as_str(), VALID); }
#[test]
fn serde_round_trip() {
let g = Gtin::parse(VALID).unwrap();
let json = serde_json::to_string(&g).unwrap();
assert_eq!(json, format!("\"{}\"", VALID));
let back: Gtin = serde_json::from_str(&json).unwrap();
assert_eq!(g, back);
}
#[test]
fn invalid_gtin_fails_deserialization() {
let result = serde_json::from_str::<Gtin>("\"09506000134351\"");
assert!(result.is_err());
}
#[test]
fn prepend_zero_to_valid_ean13_gives_valid_gtin14() {
assert!(Gtin::parse("01234567890128").is_ok());
}
#[test]
fn valid_gln_parses() {
assert!(Gln::parse("4012345000009").is_ok());
}
#[test]
fn gln_wrong_length_rejected() {
assert!(matches!(
Gln::parse("401234500000"),
Err(GlnError::InvalidFormat(_))
));
}
#[test]
fn gln_non_digits_rejected() {
assert!(matches!(
Gln::parse("401234500000X"),
Err(GlnError::InvalidFormat(_))
));
}
#[test]
fn gln_bad_check_digit_rejected() {
assert!(matches!(
Gln::parse("4000001000002"),
Err(GlnError::InvalidCheckDigit { .. })
));
assert!(Gln::parse("4000001000005").is_ok());
}
#[test]
fn gs1_check_digit_matches_known_keys() {
assert_eq!(gs1_check_digit(&[0, 9, 5, 0, 6, 0, 0, 0, 1, 3, 4, 3, 5]), 2);
assert_eq!(gs1_check_digit(&[4, 0, 1, 2, 3, 4, 5, 0, 0, 0, 0, 0]), 9);
}
}