#![doc = include_str!("../README.md")]
#![deny(missing_docs)]
#![deny(unsafe_code)]
#![warn(clippy::pedantic)]
#![allow(clippy::module_name_repetitions)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParseError {
InvalidPrefix,
MissingField,
ExtraFields,
InvalidFormat,
}
impl std::fmt::Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ParseError::InvalidPrefix => write!(f, "Invalid identifier prefix"),
ParseError::MissingField => write!(f, "Missing field in identifier"),
ParseError::ExtraFields => write!(f, "Unexpected extra fields"),
ParseError::InvalidFormat => write!(f, "Invalid identifier format"),
}
}
}
impl std::error::Error for ParseError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Sgtin<'a> {
pub company_prefix: &'a str,
pub indicator: &'a str,
pub item_ref: &'a str,
pub serial_number: &'a str,
}
impl<'a> Sgtin<'a> {
pub fn from_urn(urn: &'a str) -> Result<Self, ParseError> {
let body = urn.strip_prefix("urn:epc:id:sgtin:")
.ok_or(ParseError::InvalidPrefix)?;
let mut parts = body.split('.');
let company_prefix = parts.next().ok_or(ParseError::MissingField)?;
let indicator_and_item_ref = parts.next().ok_or(ParseError::MissingField)?;
let serial_number = parts.next().ok_or(ParseError::MissingField)?;
if parts.next().is_some() {
return Err(ParseError::ExtraFields);
}
if indicator_and_item_ref.is_empty() {
return Err(ParseError::InvalidFormat);
}
let indicator = &indicator_and_item_ref[0..1];
let item_ref = &indicator_and_item_ref[1..];
Ok(Self {
company_prefix,
indicator,
item_ref,
serial_number,
})
}
#[must_use]
pub fn to_urn(&self) -> String {
format!(
"urn:epc:id:sgtin:{}.{}{}.{}",
self.company_prefix, self.indicator, self.item_ref, self.serial_number
)
}
pub fn from_digital_link(url: &'a str, prefix_len: usize) -> Result<Self, ParseError> {
let idx = url.find("/01/").ok_or(ParseError::InvalidFormat)?;
let path = &url[idx + 4..];
let mut parts = path.split('/');
let gtin = parts.next().ok_or(ParseError::MissingField)?;
let ai = parts.next().ok_or(ParseError::MissingField)?;
if ai != "21" {
return Err(ParseError::InvalidFormat);
}
let serial_number = parts.next().ok_or(ParseError::MissingField)?;
if gtin.len() != 14 {
return Err(ParseError::InvalidFormat);
}
if prefix_len >= 13 {
return Err(ParseError::InvalidFormat);
}
let indicator = >in[0..1];
let company_prefix = >in[1..=prefix_len];
let item_ref = >in[1 + prefix_len..13];
Ok(Self {
company_prefix,
indicator,
item_ref,
serial_number,
})
}
#[must_use]
pub fn to_digital_link(&self, base_url: &str) -> String {
let gtin_without_check = format!("{}{}{}", self.indicator, self.company_prefix, self.item_ref);
let check_digit = calculate_check_digit(>in_without_check);
format!(
"{}/01/{}{}/21/{}",
base_url.trim_end_matches('/'),
gtin_without_check,
check_digit,
self.serial_number
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Sscc<'a> {
pub company_prefix: &'a str,
pub extension_digit: &'a str,
pub serial_ref: &'a str,
}
impl<'a> Sscc<'a> {
pub fn from_urn(urn: &'a str) -> Result<Self, ParseError> {
let body = urn.strip_prefix("urn:epc:id:sscc:")
.ok_or(ParseError::InvalidPrefix)?;
let mut parts = body.split('.');
let company_prefix = parts.next().ok_or(ParseError::MissingField)?;
let ext_and_serial = parts.next().ok_or(ParseError::MissingField)?;
if parts.next().is_some() {
return Err(ParseError::ExtraFields);
}
if ext_and_serial.is_empty() {
return Err(ParseError::InvalidFormat);
}
let extension_digit = &ext_and_serial[0..1];
let serial_ref = &ext_and_serial[1..];
Ok(Self {
company_prefix,
extension_digit,
serial_ref,
})
}
#[must_use]
pub fn to_urn(&self) -> String {
format!(
"urn:epc:id:sscc:{}.{}{}",
self.company_prefix, self.extension_digit, self.serial_ref
)
}
pub fn from_digital_link(url: &'a str, prefix_len: usize) -> Result<Self, ParseError> {
let idx = url.find("/00/").ok_or(ParseError::InvalidFormat)?;
let path = &url[idx + 4..];
let mut parts = path.split('/');
let sscc = parts.next().ok_or(ParseError::MissingField)?;
if sscc.len() != 18 {
return Err(ParseError::InvalidFormat);
}
if prefix_len >= 17 {
return Err(ParseError::InvalidFormat);
}
let extension_digit = &sscc[0..1];
let company_prefix = &sscc[1..=prefix_len];
let serial_ref = &sscc[1 + prefix_len..17];
Ok(Self {
company_prefix,
extension_digit,
serial_ref,
})
}
#[must_use]
pub fn to_digital_link(&self, base_url: &str) -> String {
let sscc_without_check = format!("{}{}{}", self.extension_digit, self.company_prefix, self.serial_ref);
let check_digit = calculate_check_digit(&sscc_without_check);
format!(
"{}/00/{}{}",
base_url.trim_end_matches('/'),
sscc_without_check,
check_digit
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Sgln<'a> {
pub company_prefix: &'a str,
pub location_reference: &'a str,
pub extension: &'a str,
}
impl<'a> Sgln<'a> {
pub fn from_urn(urn: &'a str) -> Result<Self, ParseError> {
let body = urn.strip_prefix("urn:epc:id:sgln:")
.ok_or(ParseError::InvalidPrefix)?;
let mut parts = body.split('.');
let company_prefix = parts.next().ok_or(ParseError::MissingField)?;
let location_reference = parts.next().ok_or(ParseError::MissingField)?;
let extension = parts.next().ok_or(ParseError::MissingField)?;
if parts.next().is_some() {
return Err(ParseError::ExtraFields);
}
Ok(Self {
company_prefix,
location_reference,
extension,
})
}
#[must_use]
pub fn to_urn(&self) -> String {
format!(
"urn:epc:id:sgln:{}.{}.{}",
self.company_prefix, self.location_reference, self.extension
)
}
pub fn from_digital_link(url: &'a str, prefix_len: usize) -> Result<Self, ParseError> {
let idx = url.find("/414/").ok_or(ParseError::InvalidFormat)?;
let path = &url[idx + 5..];
let mut parts = path.split('/');
let gln = parts.next().ok_or(ParseError::MissingField)?;
let next_ai = parts.next().ok_or(ParseError::MissingField)?;
if next_ai != "254" {
return Err(ParseError::InvalidFormat);
}
let extension = parts.next().ok_or(ParseError::MissingField)?;
if gln.len() != 13 {
return Err(ParseError::InvalidFormat);
}
if prefix_len >= 12 {
return Err(ParseError::InvalidFormat);
}
let company_prefix = &gln[0..prefix_len];
let location_reference = &gln[prefix_len..12];
Ok(Self {
company_prefix,
location_reference,
extension,
})
}
#[must_use]
pub fn to_digital_link(&self, base_url: &str) -> String {
let gln_without_check = format!("{}{}", self.company_prefix, self.location_reference);
let check_digit = calculate_check_digit(&gln_without_check);
format!(
"{}/414/{}{}/254/{}",
base_url.trim_end_matches('/'),
gln_without_check,
check_digit,
self.extension
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Grai<'a> {
pub company_prefix: &'a str,
pub asset_type: &'a str,
pub serial_number: &'a str,
}
impl<'a> Grai<'a> {
pub fn from_urn(urn: &'a str) -> Result<Self, ParseError> {
let body = urn.strip_prefix("urn:epc:id:grai:")
.ok_or(ParseError::InvalidPrefix)?;
let mut parts = body.split('.');
let company_prefix = parts.next().ok_or(ParseError::MissingField)?;
let asset_type = parts.next().ok_or(ParseError::MissingField)?;
let serial_number = parts.next().ok_or(ParseError::MissingField)?;
if parts.next().is_some() {
return Err(ParseError::ExtraFields);
}
Ok(Self {
company_prefix,
asset_type,
serial_number,
})
}
#[must_use]
pub fn to_urn(&self) -> String {
format!(
"urn:epc:id:grai:{}.{}.{}",
self.company_prefix, self.asset_type, self.serial_number
)
}
pub fn from_digital_link(url: &'a str, prefix_len: usize) -> Result<Self, ParseError> {
let idx = url.find("/8003/").ok_or(ParseError::InvalidFormat)?;
let path = &url[idx + 6..];
if path.len() < 14 {
return Err(ParseError::InvalidFormat);
}
let grai_id = &path[0..14];
let serial_number = &path[14..];
if prefix_len >= 12 {
return Err(ParseError::InvalidFormat);
}
let company_prefix = &grai_id[1..=prefix_len];
let asset_type = &grai_id[1 + prefix_len..13];
Ok(Self {
company_prefix,
asset_type,
serial_number,
})
}
#[must_use]
pub fn to_digital_link(&self, base_url: &str) -> String {
let grai_without_check = format!("0{}{}", self.company_prefix, self.asset_type);
let check_digit = calculate_check_digit(&grai_without_check);
format!(
"{}/8003/{}{}{}",
base_url.trim_end_matches('/'),
grai_without_check,
check_digit,
self.serial_number
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Giai<'a> {
pub company_prefix: &'a str,
pub individual_asset_reference: &'a str,
}
impl<'a> Giai<'a> {
pub fn from_urn(urn: &'a str) -> Result<Self, ParseError> {
let body = urn.strip_prefix("urn:epc:id:giai:")
.ok_or(ParseError::InvalidPrefix)?;
let mut parts = body.split('.');
let company_prefix = parts.next().ok_or(ParseError::MissingField)?;
let individual_asset_reference = parts.next().ok_or(ParseError::MissingField)?;
if parts.next().is_some() {
return Err(ParseError::ExtraFields);
}
Ok(Self {
company_prefix,
individual_asset_reference,
})
}
#[must_use]
pub fn to_urn(&self) -> String {
format!(
"urn:epc:id:giai:{}.{}",
self.company_prefix, self.individual_asset_reference
)
}
pub fn from_digital_link(url: &'a str, prefix_len: usize) -> Result<Self, ParseError> {
let idx = url.find("/8004/").ok_or(ParseError::InvalidFormat)?;
let path = &url[idx + 6..];
let mut parts = path.split('/');
let raw_giai = parts.next().ok_or(ParseError::MissingField)?;
if raw_giai.len() <= prefix_len {
return Err(ParseError::InvalidFormat);
}
let company_prefix = &raw_giai[0..prefix_len];
let individual_asset_reference = &raw_giai[prefix_len..];
Ok(Self {
company_prefix,
individual_asset_reference,
})
}
#[must_use]
pub fn to_digital_link(&self, base_url: &str) -> String {
format!(
"{}/8004/{}{}",
base_url.trim_end_matches('/'),
self.company_prefix,
self.individual_asset_reference
)
}
}
fn calculate_check_digit(digits: &str) -> u32 {
let mut sum = 0;
for (i, char) in digits.chars().rev().enumerate() {
if let Some(digit) = char.to_digit(10) {
if i % 2 == 0 {
sum += digit * 3;
} else {
sum += digit;
}
}
}
let remainder = sum % 10;
if remainder == 0 {
0
} else {
10 - remainder
}
}