#![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 !is_ascii_digits(company_prefix) || !is_ascii_digits(indicator_and_item_ref) {
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 || !is_ascii_digits(gtin) {
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 !is_ascii_digits(company_prefix) || !is_ascii_digits(ext_and_serial) {
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 || !is_ascii_digits(sscc) {
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);
}
if !is_ascii_digits(company_prefix)
|| !location_reference.bytes().all(|b| b.is_ascii_digit())
{
return Err(ParseError::InvalidFormat);
}
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 extension = match parts.next() {
None => "0",
Some("254") => parts.next().ok_or(ParseError::MissingField)?,
Some(_) => return Err(ParseError::InvalidFormat),
};
if gln.len() != 13 || !is_ascii_digits(gln) {
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);
if self.extension == "0" {
format!(
"{}/414/{}{}",
base_url.trim_end_matches('/'),
gln_without_check,
check_digit
)
} else {
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);
}
if !is_ascii_digits(company_prefix) || !asset_type.bytes().all(|b| b.is_ascii_digit()) {
return Err(ParseError::InvalidFormat);
}
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..];
let Some((grai_id, serial_number)) = path.split_at_checked(14) else {
return Err(ParseError::InvalidFormat);
};
if !is_ascii_digits(grai_id) {
return Err(ParseError::InvalidFormat);
}
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);
}
if !is_ascii_digits(company_prefix) {
return Err(ParseError::InvalidFormat);
}
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)?;
let Some((company_prefix, individual_asset_reference)) =
raw_giai.split_at_checked(prefix_len)
else {
return Err(ParseError::InvalidFormat);
};
if !is_ascii_digits(company_prefix) || individual_asset_reference.is_empty() {
return Err(ParseError::InvalidFormat);
}
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
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Pgln<'a> {
pub company_prefix: &'a str,
pub party_ref: &'a str,
}
impl<'a> Pgln<'a> {
pub fn from_urn(urn: &'a str) -> Result<Self, ParseError> {
let body = urn
.strip_prefix("urn:epc:id:pgln:")
.ok_or(ParseError::InvalidPrefix)?;
let mut parts = body.split('.');
let company_prefix = parts.next().ok_or(ParseError::MissingField)?;
let party_ref = parts.next().ok_or(ParseError::MissingField)?;
if parts.next().is_some() {
return Err(ParseError::ExtraFields);
}
if !is_ascii_digits(company_prefix) || !party_ref.bytes().all(|b| b.is_ascii_digit()) {
return Err(ParseError::InvalidFormat);
}
Ok(Self {
company_prefix,
party_ref,
})
}
#[must_use]
pub fn to_urn(&self) -> String {
format!("urn:epc:id:pgln:{}.{}", self.company_prefix, self.party_ref)
}
pub fn from_digital_link(url: &'a str, prefix_len: usize) -> Result<Self, ParseError> {
let idx = url.find("/417/").ok_or(ParseError::InvalidFormat)?;
let path = &url[idx + 5..];
let gln = path.split('/').next().ok_or(ParseError::MissingField)?;
if gln.len() != 13 || !is_ascii_digits(gln) {
return Err(ParseError::InvalidFormat);
}
if prefix_len >= 12 {
return Err(ParseError::InvalidFormat);
}
Ok(Self {
company_prefix: &gln[0..prefix_len],
party_ref: &gln[prefix_len..12],
})
}
#[must_use]
pub fn to_digital_link(&self, base_url: &str) -> String {
let gln_without_check = format!("{}{}", self.company_prefix, self.party_ref);
let check_digit = calculate_check_digit(&gln_without_check);
format!(
"{}/417/{}{}",
base_url.trim_end_matches('/'),
gln_without_check,
check_digit
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Gdti<'a> {
pub company_prefix: &'a str,
pub doc_type: &'a str,
pub serial_number: &'a str,
}
impl<'a> Gdti<'a> {
pub fn from_urn(urn: &'a str) -> Result<Self, ParseError> {
let body = urn
.strip_prefix("urn:epc:id:gdti:")
.ok_or(ParseError::InvalidPrefix)?;
let mut parts = body.split('.');
let company_prefix = parts.next().ok_or(ParseError::MissingField)?;
let doc_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);
}
if !is_ascii_digits(company_prefix) || !doc_type.bytes().all(|b| b.is_ascii_digit()) {
return Err(ParseError::InvalidFormat);
}
Ok(Self {
company_prefix,
doc_type,
serial_number,
})
}
#[must_use]
pub fn to_urn(&self) -> String {
format!(
"urn:epc:id:gdti:{}.{}.{}",
self.company_prefix, self.doc_type, self.serial_number
)
}
pub fn from_digital_link(url: &'a str, prefix_len: usize) -> Result<Self, ParseError> {
let idx = url.find("/253/").ok_or(ParseError::InvalidFormat)?;
let path = &url[idx + 5..];
let segment = path.split('/').next().ok_or(ParseError::MissingField)?;
let Some((gdti_id, serial_number)) = segment.split_at_checked(13) else {
return Err(ParseError::InvalidFormat);
};
if !is_ascii_digits(gdti_id) || prefix_len >= 12 {
return Err(ParseError::InvalidFormat);
}
Ok(Self {
company_prefix: &gdti_id[0..prefix_len],
doc_type: &gdti_id[prefix_len..12],
serial_number,
})
}
#[must_use]
pub fn to_digital_link(&self, base_url: &str) -> String {
let gdti_without_check = format!("{}{}", self.company_prefix, self.doc_type);
let check_digit = calculate_check_digit(&gdti_without_check);
format!(
"{}/253/{}{}{}",
base_url.trim_end_matches('/'),
gdti_without_check,
check_digit,
self.serial_number
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Gsrn<'a> {
pub company_prefix: &'a str,
pub service_ref: &'a str,
}
impl<'a> Gsrn<'a> {
pub fn from_urn(urn: &'a str) -> Result<Self, ParseError> {
gsrn_like_from_urn(urn, "urn:epc:id:gsrn:").map(|(company_prefix, service_ref)| Self {
company_prefix,
service_ref,
})
}
#[must_use]
pub fn to_urn(&self) -> String {
format!(
"urn:epc:id:gsrn:{}.{}",
self.company_prefix, self.service_ref
)
}
pub fn from_digital_link(url: &'a str, prefix_len: usize) -> Result<Self, ParseError> {
gsrn_like_from_digital_link(url, "/8018/", prefix_len).map(
|(company_prefix, service_ref)| Self {
company_prefix,
service_ref,
},
)
}
#[must_use]
pub fn to_digital_link(&self, base_url: &str) -> String {
gsrn_like_to_digital_link(base_url, "8018", self.company_prefix, self.service_ref)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Gsrnp<'a> {
pub company_prefix: &'a str,
pub service_ref: &'a str,
}
impl<'a> Gsrnp<'a> {
pub fn from_urn(urn: &'a str) -> Result<Self, ParseError> {
gsrn_like_from_urn(urn, "urn:epc:id:gsrnp:").map(|(company_prefix, service_ref)| Self {
company_prefix,
service_ref,
})
}
#[must_use]
pub fn to_urn(&self) -> String {
format!(
"urn:epc:id:gsrnp:{}.{}",
self.company_prefix, self.service_ref
)
}
pub fn from_digital_link(url: &'a str, prefix_len: usize) -> Result<Self, ParseError> {
gsrn_like_from_digital_link(url, "/8017/", prefix_len).map(
|(company_prefix, service_ref)| Self {
company_prefix,
service_ref,
},
)
}
#[must_use]
pub fn to_digital_link(&self, base_url: &str) -> String {
gsrn_like_to_digital_link(base_url, "8017", self.company_prefix, self.service_ref)
}
}
fn gsrn_like_from_urn<'a>(urn: &'a str, prefix: &str) -> Result<(&'a str, &'a str), ParseError> {
let body = urn.strip_prefix(prefix).ok_or(ParseError::InvalidPrefix)?;
let mut parts = body.split('.');
let company_prefix = parts.next().ok_or(ParseError::MissingField)?;
let service_ref = parts.next().ok_or(ParseError::MissingField)?;
if parts.next().is_some() {
return Err(ParseError::ExtraFields);
}
if !is_ascii_digits(company_prefix) || !service_ref.bytes().all(|b| b.is_ascii_digit()) {
return Err(ParseError::InvalidFormat);
}
Ok((company_prefix, service_ref))
}
fn gsrn_like_from_digital_link<'a>(
url: &'a str,
ai: &str,
prefix_len: usize,
) -> Result<(&'a str, &'a str), ParseError> {
let idx = url.find(ai).ok_or(ParseError::InvalidFormat)?;
let path = &url[idx + ai.len()..];
let gsrn = path.split('/').next().ok_or(ParseError::MissingField)?;
if gsrn.len() != 18 || !is_ascii_digits(gsrn) {
return Err(ParseError::InvalidFormat);
}
if prefix_len >= 17 {
return Err(ParseError::InvalidFormat);
}
Ok((&gsrn[0..prefix_len], &gsrn[prefix_len..17]))
}
fn gsrn_like_to_digital_link(
base_url: &str,
ai: &str,
company_prefix: &str,
service_ref: &str,
) -> String {
let without_check = format!("{company_prefix}{service_ref}");
let check_digit = calculate_check_digit(&without_check);
format!(
"{}/{ai}/{without_check}{check_digit}",
base_url.trim_end_matches('/')
)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Sgcn<'a> {
pub company_prefix: &'a str,
pub coupon_ref: &'a str,
pub serial_number: &'a str,
}
impl<'a> Sgcn<'a> {
pub fn from_urn(urn: &'a str) -> Result<Self, ParseError> {
let body = urn
.strip_prefix("urn:epc:id:sgcn:")
.ok_or(ParseError::InvalidPrefix)?;
let mut parts = body.split('.');
let company_prefix = parts.next().ok_or(ParseError::MissingField)?;
let coupon_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 !is_ascii_digits(company_prefix)
|| !coupon_ref.bytes().all(|b| b.is_ascii_digit())
|| !is_ascii_digits(serial_number)
{
return Err(ParseError::InvalidFormat);
}
Ok(Self {
company_prefix,
coupon_ref,
serial_number,
})
}
#[must_use]
pub fn to_urn(&self) -> String {
format!(
"urn:epc:id:sgcn:{}.{}.{}",
self.company_prefix, self.coupon_ref, self.serial_number
)
}
pub fn from_digital_link(url: &'a str, prefix_len: usize) -> Result<Self, ParseError> {
let idx = url.find("/255/").ok_or(ParseError::InvalidFormat)?;
let path = &url[idx + 5..];
let segment = path.split('/').next().ok_or(ParseError::MissingField)?;
let Some((gcn_id, serial_number)) = segment.split_at_checked(13) else {
return Err(ParseError::InvalidFormat);
};
if !is_ascii_digits(gcn_id) || !is_ascii_digits(serial_number) || prefix_len >= 12 {
return Err(ParseError::InvalidFormat);
}
Ok(Self {
company_prefix: &gcn_id[0..prefix_len],
coupon_ref: &gcn_id[prefix_len..12],
serial_number,
})
}
#[must_use]
pub fn to_digital_link(&self, base_url: &str) -> String {
let gcn_without_check = format!("{}{}", self.company_prefix, self.coupon_ref);
let check_digit = calculate_check_digit(&gcn_without_check);
format!(
"{}/255/{}{}{}",
base_url.trim_end_matches('/'),
gcn_without_check,
check_digit,
self.serial_number
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Ginc<'a> {
pub company_prefix: &'a str,
pub consignment_ref: &'a str,
}
impl<'a> Ginc<'a> {
pub fn from_urn(urn: &'a str) -> Result<Self, ParseError> {
let body = urn
.strip_prefix("urn:epc:id:ginc:")
.ok_or(ParseError::InvalidPrefix)?;
let mut parts = body.split('.');
let company_prefix = parts.next().ok_or(ParseError::MissingField)?;
let consignment_ref = parts.next().ok_or(ParseError::MissingField)?;
if parts.next().is_some() {
return Err(ParseError::ExtraFields);
}
if !is_ascii_digits(company_prefix) || consignment_ref.is_empty() {
return Err(ParseError::InvalidFormat);
}
Ok(Self {
company_prefix,
consignment_ref,
})
}
#[must_use]
pub fn to_urn(&self) -> String {
format!(
"urn:epc:id:ginc:{}.{}",
self.company_prefix, self.consignment_ref
)
}
pub fn from_digital_link(url: &'a str, prefix_len: usize) -> Result<Self, ParseError> {
let idx = url.find("/401/").ok_or(ParseError::InvalidFormat)?;
let path = &url[idx + 5..];
let ginc = path.split('/').next().ok_or(ParseError::MissingField)?;
let Some((company_prefix, consignment_ref)) = ginc.split_at_checked(prefix_len) else {
return Err(ParseError::InvalidFormat);
};
if !is_ascii_digits(company_prefix) || consignment_ref.is_empty() {
return Err(ParseError::InvalidFormat);
}
Ok(Self {
company_prefix,
consignment_ref,
})
}
#[must_use]
pub fn to_digital_link(&self, base_url: &str) -> String {
format!(
"{}/401/{}{}",
base_url.trim_end_matches('/'),
self.company_prefix,
self.consignment_ref
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Gsin<'a> {
pub company_prefix: &'a str,
pub shipper_ref: &'a str,
}
impl<'a> Gsin<'a> {
pub fn from_urn(urn: &'a str) -> Result<Self, ParseError> {
let body = urn
.strip_prefix("urn:epc:id:gsin:")
.ok_or(ParseError::InvalidPrefix)?;
let mut parts = body.split('.');
let company_prefix = parts.next().ok_or(ParseError::MissingField)?;
let shipper_ref = parts.next().ok_or(ParseError::MissingField)?;
if parts.next().is_some() {
return Err(ParseError::ExtraFields);
}
if !is_ascii_digits(company_prefix) || !shipper_ref.bytes().all(|b| b.is_ascii_digit()) {
return Err(ParseError::InvalidFormat);
}
Ok(Self {
company_prefix,
shipper_ref,
})
}
#[must_use]
pub fn to_urn(&self) -> String {
format!(
"urn:epc:id:gsin:{}.{}",
self.company_prefix, self.shipper_ref
)
}
pub fn from_digital_link(url: &'a str, prefix_len: usize) -> Result<Self, ParseError> {
let idx = url.find("/402/").ok_or(ParseError::InvalidFormat)?;
let path = &url[idx + 5..];
let gsin = path.split('/').next().ok_or(ParseError::MissingField)?;
if gsin.len() != 17 || !is_ascii_digits(gsin) {
return Err(ParseError::InvalidFormat);
}
if prefix_len >= 16 {
return Err(ParseError::InvalidFormat);
}
Ok(Self {
company_prefix: &gsin[0..prefix_len],
shipper_ref: &gsin[prefix_len..16],
})
}
#[must_use]
pub fn to_digital_link(&self, base_url: &str) -> String {
let gsin_without_check = format!("{}{}", self.company_prefix, self.shipper_ref);
let check_digit = calculate_check_digit(&gsin_without_check);
format!(
"{}/402/{}{}",
base_url.trim_end_matches('/'),
gsin_without_check,
check_digit
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Itip<'a> {
pub company_prefix: &'a str,
pub indicator: &'a str,
pub item_ref: &'a str,
pub piece: &'a str,
pub total: &'a str,
pub serial_number: &'a str,
}
impl<'a> Itip<'a> {
pub fn from_urn(urn: &'a str) -> Result<Self, ParseError> {
let body = urn
.strip_prefix("urn:epc:id:itip:")
.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 piece = parts.next().ok_or(ParseError::MissingField)?;
let total = 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 !is_ascii_digits(company_prefix)
|| !is_ascii_digits(indicator_and_item_ref)
|| piece.len() != 2
|| !is_ascii_digits(piece)
|| total.len() != 2
|| !is_ascii_digits(total)
{
return Err(ParseError::InvalidFormat);
}
Ok(Self {
company_prefix,
indicator: &indicator_and_item_ref[0..1],
item_ref: &indicator_and_item_ref[1..],
piece,
total,
serial_number,
})
}
#[must_use]
pub fn to_urn(&self) -> String {
format!(
"urn:epc:id:itip:{}.{}{}.{}.{}.{}",
self.company_prefix,
self.indicator,
self.item_ref,
self.piece,
self.total,
self.serial_number
)
}
pub fn from_digital_link(url: &'a str, prefix_len: usize) -> Result<Self, ParseError> {
let idx = url.find("/8006/").ok_or(ParseError::InvalidFormat)?;
let path = &url[idx + 6..];
let mut parts = path.split('/');
let itip = 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 itip.len() != 18 || !is_ascii_digits(itip) {
return Err(ParseError::InvalidFormat);
}
if prefix_len >= 13 {
return Err(ParseError::InvalidFormat);
}
Ok(Self {
indicator: &itip[0..1],
company_prefix: &itip[1..=prefix_len],
item_ref: &itip[1 + prefix_len..13],
piece: &itip[14..16],
total: &itip[16..18],
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!(
"{}/8006/{}{}{}{}/21/{}",
base_url.trim_end_matches('/'),
gtin_without_check,
check_digit,
self.piece,
self.total,
self.serial_number
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Upui<'a> {
pub company_prefix: &'a str,
pub indicator: &'a str,
pub item_ref: &'a str,
pub serial_number: &'a str,
}
impl<'a> Upui<'a> {
pub fn from_urn(urn: &'a str) -> Result<Self, ParseError> {
let body = urn
.strip_prefix("urn:epc:id:upui:")
.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 !is_ascii_digits(company_prefix) || !is_ascii_digits(indicator_and_item_ref) {
return Err(ParseError::InvalidFormat);
}
Ok(Self {
company_prefix,
indicator: &indicator_and_item_ref[0..1],
item_ref: &indicator_and_item_ref[1..],
serial_number,
})
}
#[must_use]
pub fn to_urn(&self) -> String {
format!(
"urn:epc:id:upui:{}.{}{}.{}",
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 (gtin, serial_number) = gtin_with_qualifier(url, "235")?;
if prefix_len >= 13 {
return Err(ParseError::InvalidFormat);
}
Ok(Self {
indicator: >in[0..1],
company_prefix: >in[1..=prefix_len],
item_ref: >in[1 + prefix_len..13],
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/{}{}/235/{}",
base_url.trim_end_matches('/'),
gtin_without_check,
check_digit,
self.serial_number
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Cpi<'a> {
pub company_prefix: &'a str,
pub part_ref: &'a str,
pub serial_number: &'a str,
}
impl<'a> Cpi<'a> {
pub fn from_urn(urn: &'a str) -> Result<Self, ParseError> {
let body = urn
.strip_prefix("urn:epc:id:cpi:")
.ok_or(ParseError::InvalidPrefix)?;
let mut parts = body.split('.');
let company_prefix = parts.next().ok_or(ParseError::MissingField)?;
let part_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 !is_ascii_digits(company_prefix)
|| part_ref.is_empty()
|| !is_ascii_digits(serial_number)
{
return Err(ParseError::InvalidFormat);
}
Ok(Self {
company_prefix,
part_ref,
serial_number,
})
}
#[must_use]
pub fn to_urn(&self) -> String {
format!(
"urn:epc:id:cpi:{}.{}.{}",
self.company_prefix, self.part_ref, self.serial_number
)
}
pub fn from_digital_link(url: &'a str, prefix_len: usize) -> Result<Self, ParseError> {
let idx = url.find("/8010/").ok_or(ParseError::InvalidFormat)?;
let path = &url[idx + 6..];
let mut parts = path.split('/');
let cpid = parts.next().ok_or(ParseError::MissingField)?;
let ai = parts.next().ok_or(ParseError::MissingField)?;
if ai != "8011" {
return Err(ParseError::InvalidFormat);
}
let serial_number = parts.next().ok_or(ParseError::MissingField)?;
let Some((company_prefix, part_ref)) = cpid.split_at_checked(prefix_len) else {
return Err(ParseError::InvalidFormat);
};
if !is_ascii_digits(company_prefix)
|| part_ref.is_empty()
|| !is_ascii_digits(serial_number)
{
return Err(ParseError::InvalidFormat);
}
Ok(Self {
company_prefix,
part_ref,
serial_number,
})
}
#[must_use]
pub fn to_digital_link(&self, base_url: &str) -> String {
format!(
"{}/8010/{}{}/8011/{}",
base_url.trim_end_matches('/'),
self.company_prefix,
self.part_ref,
self.serial_number
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Lgtin<'a> {
pub company_prefix: &'a str,
pub indicator: &'a str,
pub item_ref: &'a str,
pub lot: &'a str,
}
impl<'a> Lgtin<'a> {
pub fn from_urn(urn: &'a str) -> Result<Self, ParseError> {
let body = urn
.strip_prefix("urn:epc:class:lgtin:")
.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 lot = parts.next().ok_or(ParseError::MissingField)?;
if parts.next().is_some() {
return Err(ParseError::ExtraFields);
}
if !is_ascii_digits(company_prefix) || !is_ascii_digits(indicator_and_item_ref) {
return Err(ParseError::InvalidFormat);
}
Ok(Self {
company_prefix,
indicator: &indicator_and_item_ref[0..1],
item_ref: &indicator_and_item_ref[1..],
lot,
})
}
#[must_use]
pub fn to_urn(&self) -> String {
format!(
"urn:epc:class:lgtin:{}.{}{}.{}",
self.company_prefix, self.indicator, self.item_ref, self.lot
)
}
pub fn from_digital_link(url: &'a str, prefix_len: usize) -> Result<Self, ParseError> {
let (gtin, lot) = gtin_with_qualifier(url, "10")?;
if prefix_len >= 13 {
return Err(ParseError::InvalidFormat);
}
Ok(Self {
indicator: >in[0..1],
company_prefix: >in[1..=prefix_len],
item_ref: >in[1 + prefix_len..13],
lot,
})
}
#[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/{}{}/10/{}",
base_url.trim_end_matches('/'),
gtin_without_check,
check_digit,
self.lot
)
}
}
fn gtin_with_qualifier<'a>(
url: &'a str,
qualifier: &str,
) -> Result<(&'a str, &'a str), 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 != qualifier {
return Err(ParseError::InvalidFormat);
}
let value = parts.next().ok_or(ParseError::MissingField)?;
if gtin.len() != 14 || !is_ascii_digits(gtin) {
return Err(ParseError::InvalidFormat);
}
Ok((gtin, value))
}
fn is_ascii_digits(s: &str) -> bool {
!s.is_empty() && s.bytes().all(|b| b.is_ascii_digit())
}
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 }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sgtin_parse_errors() {
assert_eq!(
Sgtin::from_urn("urn:epc:id:sscc:4012345.098765.12345"),
Err(ParseError::InvalidPrefix)
);
assert_eq!(
Sgtin::from_urn("urn:epc:id:sgtin:4012345"),
Err(ParseError::MissingField)
);
assert_eq!(
Sgtin::from_urn("urn:epc:id:sgtin:4012345.098765.12345.extra"),
Err(ParseError::ExtraFields)
);
assert_eq!(
Sgtin::from_urn("urn:epc:id:sgtin:4012345..12345"),
Err(ParseError::InvalidFormat)
);
assert_eq!(
Sgtin::from_digital_link("https://id.gs1.org/00/340123450123456784", 7),
Err(ParseError::InvalidFormat)
);
assert_eq!(
Sgtin::from_digital_link("https://id.gs1.org/01/04012345987652/22/12345", 7),
Err(ParseError::InvalidFormat)
);
assert_eq!(
Sgtin::from_digital_link("https://id.gs1.org/01/0401234598765/21/12345", 7),
Err(ParseError::InvalidFormat)
); assert_eq!(
Sgtin::from_digital_link("https://id.gs1.org/01/04012345987652/21/12345", 14),
Err(ParseError::InvalidFormat)
); }
#[test]
fn test_sscc_parse_errors() {
assert_eq!(
Sscc::from_urn("urn:epc:id:sgtin:4012345.0123456789"),
Err(ParseError::InvalidPrefix)
);
assert_eq!(
Sscc::from_urn("urn:epc:id:sscc:4012345"),
Err(ParseError::MissingField)
);
assert_eq!(
Sscc::from_urn("urn:epc:id:sscc:4012345.0123456789.extra"),
Err(ParseError::ExtraFields)
);
assert_eq!(
Sscc::from_urn("urn:epc:id:sscc:"),
Err(ParseError::MissingField)
);
assert_eq!(
Sscc::from_digital_link("https://id.gs1.org/01/04012345987652/21/12345", 7),
Err(ParseError::InvalidFormat)
);
assert_eq!(
Sscc::from_digital_link("https://id.gs1.org/00/34012345012345678", 7),
Err(ParseError::InvalidFormat)
); assert_eq!(
Sscc::from_digital_link("https://id.gs1.org/00/340123450123456784", 17),
Err(ParseError::InvalidFormat)
); }
#[test]
fn test_sgln_parse_errors() {
assert_eq!(
Sgln::from_urn("urn:epc:id:sscc:4012345.00001.0"),
Err(ParseError::InvalidPrefix)
);
assert_eq!(
Sgln::from_urn("urn:epc:id:sgln:4012345.00001"),
Err(ParseError::MissingField)
);
assert_eq!(
Sgln::from_urn("urn:epc:id:sgln:4012345.00001.0.extra"),
Err(ParseError::ExtraFields)
);
assert_eq!(
Sgln::from_digital_link("https://id.gs1.org/415/4012345000016/254/0", 7),
Err(ParseError::InvalidFormat)
);
assert_eq!(
Sgln::from_digital_link("https://id.gs1.org/414/4012345000016/255/0", 7),
Err(ParseError::InvalidFormat)
);
assert_eq!(
Sgln::from_digital_link("https://id.gs1.org/414/401234500001/254/0", 7),
Err(ParseError::InvalidFormat)
); assert_eq!(
Sgln::from_digital_link("https://id.gs1.org/414/4012345000016/254/0", 12),
Err(ParseError::InvalidFormat)
); }
#[test]
fn test_grai_parse_errors() {
assert_eq!(
Grai::from_urn("urn:epc:id:sscc:4012345.00001.12345"),
Err(ParseError::InvalidPrefix)
);
assert_eq!(
Grai::from_urn("urn:epc:id:grai:4012345.00001"),
Err(ParseError::MissingField)
);
assert_eq!(
Grai::from_urn("urn:epc:id:grai:4012345.00001.12345.extra"),
Err(ParseError::ExtraFields)
);
assert_eq!(
Grai::from_digital_link("https://id.gs1.org/8004/0401234500001612345", 7),
Err(ParseError::InvalidFormat)
);
assert_eq!(
Grai::from_digital_link("https://id.gs1.org/8003/0401234500001", 7),
Err(ParseError::InvalidFormat)
); assert_eq!(
Grai::from_digital_link("https://id.gs1.org/8003/0401234500001612345", 12),
Err(ParseError::InvalidFormat)
); }
#[test]
fn test_giai_parse_errors() {
assert_eq!(
Giai::from_urn("urn:epc:id:sscc:4012345.12345"),
Err(ParseError::InvalidPrefix)
);
assert_eq!(
Giai::from_urn("urn:epc:id:giai:4012345"),
Err(ParseError::MissingField)
);
assert_eq!(
Giai::from_urn("urn:epc:id:giai:4012345.12345.extra"),
Err(ParseError::ExtraFields)
);
assert_eq!(
Giai::from_digital_link("https://id.gs1.org/8003/401234512345", 7),
Err(ParseError::InvalidFormat)
);
assert_eq!(
Giai::from_digital_link("https://id.gs1.org/8004/401234512345", 15),
Err(ParseError::InvalidFormat)
); }
#[test]
fn test_multibyte_input_returns_error_not_panic() {
assert_eq!(
Sgtin::from_urn("urn:epc:id:sgtin:4012345.é8765.1"),
Err(ParseError::InvalidFormat)
);
assert_eq!(
Sscc::from_urn("urn:epc:id:sscc:4012345.é123456789"),
Err(ParseError::InvalidFormat)
);
assert_eq!(
Sgln::from_urn("urn:epc:id:sgln:4012345.é0001.0"),
Err(ParseError::InvalidFormat)
);
assert_eq!(
Grai::from_urn("urn:epc:id:grai:4012345.é0001.1"),
Err(ParseError::InvalidFormat)
);
assert_eq!(
Giai::from_urn("urn:epc:id:giai:é012345.12345"),
Err(ParseError::InvalidFormat)
);
assert_eq!(
Sgtin::from_digital_link("https://id.gs1.org/01/é4012345987652/21/1", 7),
Err(ParseError::InvalidFormat)
);
assert_eq!(
Grai::from_digital_link("https://id.gs1.org/8003/é401234500001612345", 7),
Err(ParseError::InvalidFormat)
);
assert_eq!(
Giai::from_digital_link("https://id.gs1.org/8004/é01234512345", 7),
Err(ParseError::InvalidFormat)
);
}
#[test]
fn test_non_numeric_segments_rejected() {
assert_eq!(
Sgtin::from_urn("urn:epc:id:sgtin:abcdefg.hijkl.serial"),
Err(ParseError::InvalidFormat)
);
assert_eq!(
Sscc::from_urn("urn:epc:id:sscc:4012345.0123x5678"),
Err(ParseError::InvalidFormat)
);
assert_eq!(
Sgln::from_urn("urn:epc:id:sgln:40123a5.00001.0"),
Err(ParseError::InvalidFormat)
);
assert_eq!(
Grai::from_urn("urn:epc:id:grai:4012345.000x1.12345"),
Err(ParseError::InvalidFormat)
);
assert_eq!(
Sgtin::from_digital_link("https://id.gs1.org/01/0401234598765X/21/12345", 7),
Err(ParseError::InvalidFormat)
);
assert!(Sgtin::from_urn("urn:epc:id:sgtin:4012345.098765.ABC-123").is_ok());
assert!(Giai::from_urn("urn:epc:id:giai:4012345.ASSET-9").is_ok());
}
#[test]
fn test_sgln_extension_zero_omits_qualifier() {
let sgln = Sgln::from_urn("urn:epc:id:sgln:4012345.00001.0").unwrap();
assert_eq!(
sgln.to_digital_link("https://id.gs1.org"),
"https://id.gs1.org/414/4012345000016"
);
let parsed = Sgln::from_digital_link("https://id.gs1.org/414/4012345000016", 7).unwrap();
assert_eq!(parsed.extension, "0");
assert_eq!(parsed.to_urn(), "urn:epc:id:sgln:4012345.00001.0");
}
#[test]
fn test_translation_performance_guard() {
use std::time::Instant;
let urn = "urn:epc:id:sgtin:4012345.098765.12345";
let dl = "https://id.gs1.org/01/04012345987652/21/12345";
let start = Instant::now();
for _ in 0..100_000 {
let sgtin = Sgtin::from_urn(urn).unwrap();
let _dl_out = sgtin.to_digital_link("https://id.gs1.org");
let sgtin_dl = Sgtin::from_digital_link(dl, 7).unwrap();
let _urn_out = sgtin_dl.to_urn();
}
let elapsed = start.elapsed();
println!("Translated 100k inputs in: {elapsed:?}");
assert!(
elapsed.as_millis() < 500,
"Performance regression detected! 100k roundtrips took {elapsed:?}"
);
}
}
#[cfg(feature = "wasm")]
use wasm_bindgen::prelude::*;
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn translate_urn_to_dl_wasm(urn: &str, base_url: &str) -> Result<String, String> {
if urn.starts_with("urn:epc:class:lgtin:") {
return Lgtin::from_urn(urn)
.map(|k| k.to_digital_link(base_url))
.map_err(|e| format!("Failed to parse LGTIN: {e:?}"));
}
let parts: Vec<&str> = urn.split(':').collect();
if parts.len() < 5 {
return Err("Invalid URN format. Expected e.g. urn:epc:id:sgtin:...".to_string());
}
let scheme = parts[3];
match scheme {
"sgtin" => {
let sgtin =
Sgtin::from_urn(urn).map_err(|e| format!("Failed to parse SGTIN: {:?}", e))?;
Ok(sgtin.to_digital_link(base_url))
}
"sscc" => {
let sscc = Sscc::from_urn(urn).map_err(|e| format!("Failed to parse SSCC: {:?}", e))?;
Ok(sscc.to_digital_link(base_url))
}
"sgln" => {
let sgln = Sgln::from_urn(urn).map_err(|e| format!("Failed to parse SGLN: {:?}", e))?;
Ok(sgln.to_digital_link(base_url))
}
"grai" => {
let grai = Grai::from_urn(urn).map_err(|e| format!("Failed to parse GRAI: {:?}", e))?;
Ok(grai.to_digital_link(base_url))
}
"giai" => {
let giai = Giai::from_urn(urn).map_err(|e| format!("Failed to parse GIAI: {:?}", e))?;
Ok(giai.to_digital_link(base_url))
}
"pgln" => Pgln::from_urn(urn)
.map(|k| k.to_digital_link(base_url))
.map_err(|e| format!("Failed to parse PGLN: {e:?}")),
"gdti" => Gdti::from_urn(urn)
.map(|k| k.to_digital_link(base_url))
.map_err(|e| format!("Failed to parse GDTI: {e:?}")),
"gsrn" => Gsrn::from_urn(urn)
.map(|k| k.to_digital_link(base_url))
.map_err(|e| format!("Failed to parse GSRN: {e:?}")),
"gsrnp" => Gsrnp::from_urn(urn)
.map(|k| k.to_digital_link(base_url))
.map_err(|e| format!("Failed to parse GSRNP: {e:?}")),
"sgcn" => Sgcn::from_urn(urn)
.map(|k| k.to_digital_link(base_url))
.map_err(|e| format!("Failed to parse SGCN: {e:?}")),
"ginc" => Ginc::from_urn(urn)
.map(|k| k.to_digital_link(base_url))
.map_err(|e| format!("Failed to parse GINC: {e:?}")),
"gsin" => Gsin::from_urn(urn)
.map(|k| k.to_digital_link(base_url))
.map_err(|e| format!("Failed to parse GSIN: {e:?}")),
"itip" => Itip::from_urn(urn)
.map(|k| k.to_digital_link(base_url))
.map_err(|e| format!("Failed to parse ITIP: {e:?}")),
"upui" => Upui::from_urn(urn)
.map(|k| k.to_digital_link(base_url))
.map_err(|e| format!("Failed to parse UPUI: {e:?}")),
"cpi" => Cpi::from_urn(urn)
.map(|k| k.to_digital_link(base_url))
.map_err(|e| format!("Failed to parse CPI: {e:?}")),
other => Err(format!("Unsupported URN scheme: {}", other)),
}
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn translate_dl_to_urn_wasm(dl: &str, prefix_len: usize) -> Result<String, String> {
if dl.contains("/01/") && dl.contains("/235/") {
return Upui::from_digital_link(dl, prefix_len)
.map(|k| k.to_urn())
.map_err(|e| format!("Failed to parse UPUI DL: {e:?}"));
}
if dl.contains("/01/") && dl.contains("/10/") {
return Lgtin::from_digital_link(dl, prefix_len)
.map(|k| k.to_urn())
.map_err(|e| format!("Failed to parse LGTIN DL: {e:?}"));
}
for (ai, translate) in [
(
"/8006/",
(|d, l| Itip::from_digital_link(d, l).map(|k| k.to_urn()))
as fn(&str, usize) -> Result<String, ParseError>,
),
("/8010/", |d, l| {
Cpi::from_digital_link(d, l).map(|k| k.to_urn())
}),
("/8017/", |d, l| {
Gsrnp::from_digital_link(d, l).map(|k| k.to_urn())
}),
("/8018/", |d, l| {
Gsrn::from_digital_link(d, l).map(|k| k.to_urn())
}),
("/253/", |d, l| {
Gdti::from_digital_link(d, l).map(|k| k.to_urn())
}),
("/255/", |d, l| {
Sgcn::from_digital_link(d, l).map(|k| k.to_urn())
}),
("/401/", |d, l| {
Ginc::from_digital_link(d, l).map(|k| k.to_urn())
}),
("/402/", |d, l| {
Gsin::from_digital_link(d, l).map(|k| k.to_urn())
}),
("/417/", |d, l| {
Pgln::from_digital_link(d, l).map(|k| k.to_urn())
}),
] {
if dl.contains(ai) {
return translate(dl, prefix_len)
.map_err(|e| format!("Failed to parse {ai} DL: {e:?}"));
}
}
if dl.contains("/01/") {
let sgtin = Sgtin::from_digital_link(dl, prefix_len)
.map_err(|e| format!("Failed to parse SGTIN DL: {:?}", e))?;
Ok(sgtin.to_urn())
} else if dl.contains("/00/") {
let sscc = Sscc::from_digital_link(dl, prefix_len)
.map_err(|e| format!("Failed to parse SSCC DL: {:?}", e))?;
Ok(sscc.to_urn())
} else if dl.contains("/414/") {
let sgln = Sgln::from_digital_link(dl, prefix_len)
.map_err(|e| format!("Failed to parse SGLN DL: {:?}", e))?;
Ok(sgln.to_urn())
} else if dl.contains("/8003/") {
let grai = Grai::from_digital_link(dl, prefix_len)
.map_err(|e| format!("Failed to parse GRAI DL: {:?}", e))?;
Ok(grai.to_urn())
} else if dl.contains("/8004/") {
let giai = Giai::from_digital_link(dl, prefix_len)
.map_err(|e| format!("Failed to parse GIAI DL: {:?}", e))?;
Ok(giai.to_urn())
} else {
Err("Could not detect GS1 Application Identifier (AI) in Digital Link path (expected e.g. /01/, /00/, /414/, /8003/, /8004/)".to_string())
}
}