mod ai;
mod codec;
mod error;
#[cfg(test)]
mod tests;
use dpp_domain::Gtin;
pub use ai::{AI_TABLE, AiDescriptor, AiRole, ai_descriptor};
pub use error::DigitalLinkError;
use codec::{normalize_gtin_to_14, percent_decode, percent_encode};
pub fn validate_gtin(gtin: &str) -> Result<(), DigitalLinkError> {
Gtin::parse(gtin)
.map(|_| ())
.map_err(DigitalLinkError::from)
}
pub fn build_qr_url(
resolver_base: &str,
gtin: &str,
passport_id: &str,
batch: Option<&str>,
) -> String {
let mut uri = format!("{}/01/{}", resolver_base.trim_end_matches('/'), gtin);
if let Some(b) = batch {
uri.push_str(&format!("/10/{}", percent_encode(b)));
}
uri.push_str(&format!("/21/{}", percent_encode(passport_id)));
uri
}
#[derive(Debug, Clone, PartialEq)]
pub struct DigitalLink {
pub resolver_base: String,
pub gtin: Gtin,
pub variant: Option<String>,
pub batch: Option<String>,
pub serial: Option<String>,
pub tpcsn: Option<String>,
}
impl DigitalLink {
pub fn parse(uri: &str) -> Result<Self, DigitalLinkError> {
let path_end = uri.find('?').unwrap_or(uri.len());
let uri_no_query = &uri[..path_end];
if !uri_no_query.starts_with("https://") {
let scheme = uri_no_query.split("://").next().unwrap_or("").to_owned();
return Err(DigitalLinkError::InvalidScheme(scheme));
}
let without_scheme = &uri_no_query["https://".len()..];
let slash_pos = without_scheme.find('/').unwrap_or(without_scheme.len());
let host = &without_scheme[..slash_pos];
let path = &without_scheme[slash_pos..];
let all_segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
let gtin_seg_pos = all_segments
.iter()
.position(|s| *s == "01")
.ok_or(DigitalLinkError::MissingGtin)?;
let path_prefix = if gtin_seg_pos > 0 {
format!("/{}", all_segments[..gtin_seg_pos].join("/"))
} else {
String::new()
};
let ai_segments = &all_segments[gtin_seg_pos..];
let mut i = 0;
let mut gtin: Option<Gtin> = None;
let mut variant: Option<String> = None;
let mut batch: Option<String> = None;
let mut serial: Option<String> = None;
let mut tpcsn: Option<String> = None;
let mut last_qualifier_order: u8 = 0;
let mut last_qualifier_code: &str = "";
while i + 1 < ai_segments.len() {
let code = ai_segments[i];
let desc = ai_descriptor(code)
.ok_or_else(|| DigitalLinkError::UnknownApplicationIdentifier(code.to_owned()))?;
let raw_value = ai_segments[i + 1];
let value = percent_decode(raw_value);
match desc.role {
AiRole::PrimaryKey => {
let padded = normalize_gtin_to_14(&value)?;
gtin = Some(Gtin::parse(&padded)?);
}
AiRole::Qualifier => {
let order = desc.qualifier_order.unwrap_or(0);
if order <= last_qualifier_order && last_qualifier_order > 0 {
return Err(DigitalLinkError::QualifiersOutOfOrder {
before: last_qualifier_code.to_owned(),
before_ord: last_qualifier_order,
after: code.to_owned(),
after_ord: order,
});
}
last_qualifier_order = order;
last_qualifier_code = code;
match code {
"22" => variant = Some(value),
"10" => batch = Some(value),
"21" => serial = Some(value),
"235" => tpcsn = Some(value),
_ => {}
}
}
AiRole::DataAttribute => {
}
}
i += 2;
}
let gtin = gtin.ok_or(DigitalLinkError::MissingGtin)?;
Ok(Self {
resolver_base: format!("https://{host}{path_prefix}"),
gtin,
variant,
batch,
serial,
tpcsn,
})
}
pub fn build(&self) -> String {
let mut uri = format!(
"{}/01/{}",
self.resolver_base.trim_end_matches('/'),
self.gtin.as_str()
);
if let Some(v) = &self.variant {
uri.push_str(&format!("/22/{}", percent_encode(v)));
}
if let Some(b) = &self.batch {
uri.push_str(&format!("/10/{}", percent_encode(b)));
}
if let Some(s) = &self.serial {
uri.push_str(&format!("/21/{}", percent_encode(s)));
}
if let Some(t) = &self.tpcsn {
uri.push_str(&format!("/235/{}", percent_encode(t)));
}
uri
}
}