dpp_digital_link/digital_link/
link.rs1use dpp_domain::Gtin;
4
5use super::ai::{AiRole, ai_descriptor};
6use super::codec::{normalize_gtin_to_14, percent_decode, percent_encode};
7use super::error::DigitalLinkError;
8
9#[derive(Debug, Clone, PartialEq)]
10pub struct DigitalLink {
11 pub resolver_base: String,
14 pub gtin: Gtin,
16 pub variant: Option<String>,
18 pub batch: Option<String>,
20 pub serial: Option<String>,
22 pub tpcsn: Option<String>,
24}
25
26impl DigitalLink {
27 pub fn parse(uri: &str) -> Result<Self, DigitalLinkError> {
38 let path_end = uri.find('?').unwrap_or(uri.len());
40 let uri_no_query = &uri[..path_end];
41
42 if !uri_no_query.starts_with("https://") {
43 let scheme = uri_no_query.split("://").next().unwrap_or("").to_owned();
44 return Err(DigitalLinkError::InvalidScheme(scheme));
45 }
46
47 let without_scheme = &uri_no_query["https://".len()..];
48 let slash_pos = without_scheme.find('/').unwrap_or(without_scheme.len());
49 let host = &without_scheme[..slash_pos];
50 let path = &without_scheme[slash_pos..];
51
52 let all_segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
53
54 let gtin_seg_pos = all_segments
57 .iter()
58 .position(|s| *s == "01")
59 .ok_or(DigitalLinkError::MissingGtin)?;
60
61 let path_prefix = if gtin_seg_pos > 0 {
62 format!("/{}", all_segments[..gtin_seg_pos].join("/"))
63 } else {
64 String::new()
65 };
66
67 let ai_segments = &all_segments[gtin_seg_pos..];
69 let mut i = 0;
70 let mut gtin: Option<Gtin> = None;
71 let mut variant: Option<String> = None;
72 let mut batch: Option<String> = None;
73 let mut serial: Option<String> = None;
74 let mut tpcsn: Option<String> = None;
75 let mut last_qualifier_order: u8 = 0;
76 let mut last_qualifier_code: &str = "";
77
78 while i + 1 < ai_segments.len() {
79 let code = ai_segments[i];
80 let desc = ai_descriptor(code)
81 .ok_or_else(|| DigitalLinkError::UnknownApplicationIdentifier(code.to_owned()))?;
82
83 let raw_value = ai_segments[i + 1];
84 let value = percent_decode(raw_value);
85
86 match desc.role {
87 AiRole::PrimaryKey => {
88 let padded = normalize_gtin_to_14(&value)?;
89 gtin = Some(Gtin::parse(&padded)?);
90 }
91 AiRole::Qualifier => {
92 let order = desc.qualifier_order.unwrap_or(0);
93 if order <= last_qualifier_order && last_qualifier_order > 0 {
94 return Err(DigitalLinkError::QualifiersOutOfOrder {
95 before: last_qualifier_code.to_owned(),
96 before_ord: last_qualifier_order,
97 after: code.to_owned(),
98 after_ord: order,
99 });
100 }
101 last_qualifier_order = order;
102 last_qualifier_code = code;
103 match code {
104 "22" => variant = Some(value),
105 "10" => batch = Some(value),
106 "21" => serial = Some(value),
107 "235" => tpcsn = Some(value),
108 _ => {}
109 }
110 }
111 AiRole::DataAttribute => {
112 }
114 }
115
116 i += 2;
117 }
118
119 let gtin = gtin.ok_or(DigitalLinkError::MissingGtin)?;
120
121 Ok(Self {
122 resolver_base: format!("https://{host}{path_prefix}"),
123 gtin,
124 variant,
125 batch,
126 serial,
127 tpcsn,
128 })
129 }
130
131 pub fn build(&self) -> String {
135 let mut uri = format!(
136 "{}/01/{}",
137 self.resolver_base.trim_end_matches('/'),
138 self.gtin.as_str()
139 );
140 if let Some(v) = &self.variant {
141 uri.push_str(&format!("/22/{}", percent_encode(v)));
142 }
143 if let Some(b) = &self.batch {
144 uri.push_str(&format!("/10/{}", percent_encode(b)));
145 }
146 if let Some(s) = &self.serial {
147 uri.push_str(&format!("/21/{}", percent_encode(s)));
148 }
149 if let Some(t) = &self.tpcsn {
150 uri.push_str(&format!("/235/{}", percent_encode(t)));
151 }
152 uri
153 }
154}