1#[derive(Debug, Clone, PartialEq)]
3pub enum UrlSegment {
4 Static(String),
6 Parameter(String),
8}
9
10#[derive(Debug, Clone, Default)]
16pub struct HttpPattern {
17 pub template: String,
19 pub segments: Vec<UrlSegment>,
21 pub parameters: Vec<String>,
23 pub static_prefix: String,
25 pub static_suffix: Option<String>,
27 pub multi_segment_parameters: Vec<String>,
30}
31
32impl HttpPattern {
33 pub fn parse(template: &str) -> Self {
35 let (segments, multi_segment_parameters) = parse_url_segments(template);
36 let parameters = segments
37 .iter()
38 .filter_map(|seg| match seg {
39 UrlSegment::Parameter(name) => Some(name.clone()),
40 UrlSegment::Static(_) => None,
41 })
42 .collect();
43
44 let static_prefix = extract_static_prefix(&segments);
45 let static_suffix = extract_static_suffix(&segments);
46
47 HttpPattern {
48 template: template.to_string(),
49 segments,
50 parameters,
51 static_prefix,
52 static_suffix,
53 multi_segment_parameters,
54 }
55 }
56
57 pub fn is_multi_segment(&self, name: &str) -> bool {
59 self.multi_segment_parameters.iter().any(|p| p == name)
60 }
61
62 pub fn ends_with_static(&self) -> bool {
63 self.segments
64 .last()
65 .is_some_and(|seg| matches!(seg, UrlSegment::Static(_)))
66 }
67
68 pub fn ends_with_parameter(&self) -> bool {
69 self.segments
70 .last()
71 .is_some_and(|seg| matches!(seg, UrlSegment::Parameter(_)))
72 }
73
74 pub fn base_path(&self) -> String {
76 self.static_prefix
77 .trim_start_matches('/')
78 .trim_end_matches('/')
79 .to_string()
80 }
81
82 pub fn parameter_names(&self) -> &[String] {
84 &self.parameters
85 }
86
87 pub fn to_format_string(&self) -> (String, Vec<String>) {
90 let mut format = String::new();
91 let mut format_args = Vec::new();
92
93 for segment in &self.segments {
94 match segment {
95 UrlSegment::Static(literal) => format.push_str(literal),
96 UrlSegment::Parameter(name) => {
97 format.push_str("{}");
98 format_args.push(name.clone());
99 }
100 }
101 }
102
103 (format.trim_start_matches('/').to_string(), format_args)
104 }
105}
106
107fn normalize_placeholder(raw: &str) -> Option<String> {
123 let name = raw.split('=').next().unwrap_or("").trim();
124 if name.is_empty() || name.contains('.') {
125 return None;
126 }
127 Some(name.to_string())
128}
129
130fn parse_url_segments(template: &str) -> (Vec<UrlSegment>, Vec<String>) {
133 let mut segments = Vec::new();
134 let mut multi_segment = Vec::new();
135 let mut chars = template.chars().peekable();
136 let mut current_static = String::new();
137
138 while let Some(ch) = chars.next() {
139 if ch == '{' {
140 if !current_static.is_empty() {
142 segments.push(UrlSegment::Static(std::mem::take(&mut current_static)));
143 }
144
145 let mut raw = String::new();
147 let mut closed = false;
148 for next_ch in chars.by_ref() {
149 if next_ch == '}' {
150 closed = true;
151 break;
152 }
153 raw.push(next_ch);
154 }
155
156 match (!raw.is_empty() && closed)
159 .then(|| normalize_placeholder(&raw))
160 .flatten()
161 {
162 Some(param_name) => {
163 if raw
164 .split_once('=')
165 .is_some_and(|(_, pattern)| pattern.contains('/') || pattern.contains("**"))
166 {
167 multi_segment.push(param_name.clone());
168 }
169 segments.push(UrlSegment::Parameter(param_name));
170 }
171 None if closed && !raw.is_empty() => {
176 segments.push(UrlSegment::Static(format!("{{{raw}}}")));
177 }
178 None => {}
179 }
180 } else {
181 current_static.push(ch);
182 }
183 }
184
185 if !current_static.is_empty() {
187 segments.push(UrlSegment::Static(current_static));
188 }
189
190 (segments, multi_segment)
191}
192
193fn extract_static_prefix(segments: &[UrlSegment]) -> String {
195 let mut prefix = String::new();
196 for segment in segments {
197 match segment {
198 UrlSegment::Static(literal) => prefix.push_str(literal),
199 UrlSegment::Parameter(_) => break,
200 }
201 }
202 prefix
203}
204
205fn extract_static_suffix(segments: &[UrlSegment]) -> Option<String> {
207 let last_param_index = segments
208 .iter()
209 .rposition(|segment| matches!(segment, UrlSegment::Parameter(_)))?;
210 let suffix: String = segments[last_param_index + 1..]
211 .iter()
212 .filter_map(|segment| match segment {
213 UrlSegment::Static(literal) => Some(literal.as_str()),
214 UrlSegment::Parameter(_) => None,
215 })
216 .collect();
217
218 (!suffix.is_empty()).then_some(suffix)
219}
220
221pub fn extract_path_parameters(path_template: &str) -> Vec<String> {
224 HttpPattern::parse(path_template).parameters
225}
226
227pub fn extract_http_rule_pattern(http_rule: &crate::google::api::HttpRule) -> Option<HttpPattern> {
229 use crate::google::api::http_rule::Pattern;
230
231 let template = match http_rule.pattern.as_ref()? {
232 Pattern::Get(path)
233 | Pattern::Post(path)
234 | Pattern::Put(path)
235 | Pattern::Delete(path)
236 | Pattern::Patch(path) => path,
237 Pattern::Custom(custom) => &custom.path,
238 };
239
240 Some(HttpPattern::parse(template))
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246
247 #[test]
248 fn test_extract_path_parameters() {
249 assert_eq!(extract_path_parameters("/catalogs/{name}"), vec!["name"]);
250 assert_eq!(
251 extract_path_parameters("/shares/{share}/schemas/{schema}/tables/{name}"),
252 vec!["share", "schema", "name"]
253 );
254 assert_eq!(extract_path_parameters("/catalogs"), Vec::<String>::new());
255 }
256
257 #[test]
258 fn test_http_pattern_parsing() {
259 let pattern = HttpPattern::parse("/catalogs");
261 assert_eq!(pattern.parameters, Vec::<String>::new());
262 assert_eq!(pattern.static_prefix, "/catalogs");
263 assert_eq!(pattern.static_suffix, None);
264
265 let pattern = HttpPattern::parse("/catalogs/{name}");
267 assert_eq!(pattern.parameters, vec!["name"]);
268 assert_eq!(pattern.static_prefix, "/catalogs/");
269 assert_eq!(pattern.static_suffix, None);
270
271 let pattern = HttpPattern::parse("/shares/{share}/schemas/{schema}/tables/{name}");
273 assert_eq!(pattern.parameters, vec!["share", "schema", "name"]);
274 assert_eq!(pattern.static_prefix, "/shares/");
275 assert_eq!(pattern.static_suffix, None);
276
277 let pattern = HttpPattern::parse("/catalogs/{name}/metadata");
279 assert_eq!(pattern.parameters, vec!["name"]);
280 assert_eq!(pattern.static_prefix, "/catalogs/");
281 assert_eq!(pattern.static_suffix.as_deref(), Some("/metadata"));
282 }
283
284 #[test]
285 fn test_http_pattern_segments() {
286 let pattern = HttpPattern::parse("/shares/{share}/schemas/{schema}");
287
288 use UrlSegment;
289 assert_eq!(
290 pattern.segments,
291 vec![
292 UrlSegment::Static("/shares/".to_string()),
293 UrlSegment::Parameter("share".to_string()),
294 UrlSegment::Static("/schemas/".to_string()),
295 UrlSegment::Parameter("schema".to_string()),
296 ]
297 );
298 }
299
300 #[test]
301 fn test_http_pattern_to_format_string() {
302 let pattern = HttpPattern::parse("/catalogs/{name}");
303 let (format_str, args) = pattern.to_format_string();
304 assert_eq!(format_str, "catalogs/{}");
305 assert_eq!(args, vec!["name"]);
306
307 let pattern = HttpPattern::parse("/shares/{share}/schemas/{schema}");
308 let (format_str, args) = pattern.to_format_string();
309 assert_eq!(format_str, "shares/{}/schemas/{}");
310 assert_eq!(args, vec!["share", "schema"]);
311 }
312
313 #[test]
314 fn test_http_pattern_base_path() {
315 let pattern = HttpPattern::parse("/catalogs/{name}");
316 assert_eq!(pattern.base_path(), "catalogs");
317
318 let pattern = HttpPattern::parse("/shares/{share}/schemas");
319 assert_eq!(pattern.base_path(), "shares");
320 }
321
322 #[test]
323 fn test_unclosed_brace_is_ignored() {
324 let pattern = HttpPattern::parse("/catalogs/{name");
327 assert!(
328 pattern.parameters.is_empty(),
329 "unclosed brace must not produce a parameter"
330 );
331 assert_eq!(pattern.static_prefix, "/catalogs/");
332 }
333
334 #[test]
335 fn test_segment_binding_placeholder() {
336 let pattern = HttpPattern::parse("/v1/{name=catalogs/*}");
338 assert_eq!(pattern.parameters, vec!["name"]);
339
340 let (format_str, args) = pattern.to_format_string();
341 assert_eq!(args, vec!["name"]);
342 assert!(!format_str.contains('='), "got: {format_str}");
344 }
345
346 #[test]
347 fn test_dotted_placeholder_is_preserved_not_a_param() {
348 let pattern = HttpPattern::parse("/v1/tables/{table.catalog_name}/x");
352 assert!(
353 pattern.parameters.is_empty(),
354 "dotted placeholder must not become a parameter: {:?}",
355 pattern.parameters
356 );
357 assert!(
358 pattern.template.contains("{table.catalog_name}"),
359 "original template text is retained"
360 );
361 }
362
363 #[test]
364 fn test_degenerate_placeholder_is_ignored() {
365 let pattern = HttpPattern::parse("/v1/{=foo}");
367 assert!(pattern.parameters.is_empty());
368 }
369
370 #[test]
371 fn test_extract_http_rule_pattern() {
372 use crate::google::api::{HttpRule, http_rule::Pattern};
373
374 let http_rule = HttpRule {
376 pattern: Some(Pattern::Get("/catalogs/{name}".to_string())),
377 ..Default::default()
378 };
379
380 let pattern = extract_http_rule_pattern(&http_rule).unwrap();
381 assert_eq!(pattern.parameters, vec!["name"]);
382 assert_eq!(pattern.template, "/catalogs/{name}");
383
384 let http_rule = HttpRule {
386 pattern: Some(Pattern::Post("/catalogs".to_string())),
387 ..Default::default()
388 };
389
390 let pattern = extract_http_rule_pattern(&http_rule).unwrap();
391 assert_eq!(pattern.parameters, Vec::<String>::new());
392 assert_eq!(pattern.template, "/catalogs");
393
394 let http_rule = HttpRule {
396 pattern: None,
397 ..Default::default()
398 };
399
400 assert!(extract_http_rule_pattern(&http_rule).is_none());
401 }
402
403 #[test]
404 fn test_multi_segment_parameters() {
405 let pattern = HttpPattern::parse("/v1/{name=catalogs/*}/schemas/{schema}");
406 assert_eq!(pattern.multi_segment_parameters, vec!["name"]);
407 assert!(pattern.is_multi_segment("name"));
408 assert!(!pattern.is_multi_segment("schema"));
409
410 let pattern = HttpPattern::parse("/files/{path=**}");
411 assert!(pattern.is_multi_segment("path"));
412
413 let pattern = HttpPattern::parse("/catalogs/{name=*}");
415 assert!(!pattern.is_multi_segment("name"));
416 let pattern = HttpPattern::parse("/catalogs/{name}");
417 assert!(pattern.multi_segment_parameters.is_empty());
418 }
419}