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 let value_len = value.chars().count();
89 if value_len > desc.max_len {
90 return Err(DigitalLinkError::ValueTooLong {
91 code: code.to_owned(),
92 max_len: desc.max_len,
93 actual: value_len,
94 });
95 }
96
97 match desc.role {
98 AiRole::PrimaryKey => {
99 if gtin.is_some() {
102 return Err(DigitalLinkError::DuplicatePrimaryKey);
103 }
104 let padded = normalize_gtin_to_14(&value)?;
105 gtin = Some(Gtin::parse(&padded)?);
106 }
107 AiRole::Qualifier => {
108 let order = desc.qualifier_order.unwrap_or(0);
109 if order <= last_qualifier_order && last_qualifier_order > 0 {
110 return Err(DigitalLinkError::QualifiersOutOfOrder {
111 before: last_qualifier_code.to_owned(),
112 before_ord: last_qualifier_order,
113 after: code.to_owned(),
114 after_ord: order,
115 });
116 }
117 last_qualifier_order = order;
118 last_qualifier_code = code;
119 match code {
120 "22" => variant = Some(value),
121 "10" => batch = Some(value),
122 "21" => serial = Some(value),
123 "235" => tpcsn = Some(value),
124 _ => {}
125 }
126 }
127 AiRole::DataAttribute => {
128 }
130 }
131
132 i += 2;
133 }
134
135 if i < ai_segments.len() {
138 return Err(DigitalLinkError::TrailingUnpairedSegment(
139 ai_segments[i].to_owned(),
140 ));
141 }
142
143 let gtin = gtin.ok_or(DigitalLinkError::MissingGtin)?;
144
145 Ok(Self {
146 resolver_base: format!("https://{host}{path_prefix}"),
147 gtin,
148 variant,
149 batch,
150 serial,
151 tpcsn,
152 })
153 }
154
155 pub fn build(&self) -> String {
159 let mut uri = format!(
160 "{}/01/{}",
161 self.resolver_base.trim_end_matches('/'),
162 self.gtin.as_str()
163 );
164 if let Some(v) = &self.variant {
165 uri.push_str(&format!("/22/{}", percent_encode(v)));
166 }
167 if let Some(b) = &self.batch {
168 uri.push_str(&format!("/10/{}", percent_encode(b)));
169 }
170 if let Some(s) = &self.serial {
171 uri.push_str(&format!("/21/{}", percent_encode(s)));
172 }
173 if let Some(t) = &self.tpcsn {
174 uri.push_str(&format!("/235/{}", percent_encode(t)));
175 }
176 uri
177 }
178}