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_parts = Vec::new();
91 let mut format_args = Vec::new();
92
93 for segment in &self.segments {
94 match segment {
95 UrlSegment::Static(literal) => {
96 format_parts.push(literal.clone());
97 }
98 UrlSegment::Parameter(name) => {
99 format_parts.push("{}".to_string());
100 format_args.push(name.clone());
101 }
102 }
103 }
104
105 (
106 format_parts.join("").trim_start_matches('/').to_string(),
107 format_args,
108 )
109 }
110}
111
112fn normalize_placeholder(raw: &str) -> Option<String> {
128 let name = raw.split('=').next().unwrap_or("").trim();
129 if name.is_empty() || name.contains('.') {
130 return None;
131 }
132 Some(name.to_string())
133}
134
135fn parse_url_segments(template: &str) -> (Vec<UrlSegment>, Vec<String>) {
138 let mut segments = Vec::new();
139 let mut multi_segment = Vec::new();
140 let mut chars = template.chars().peekable();
141 let mut current_static = String::new();
142
143 while let Some(ch) = chars.next() {
144 if ch == '{' {
145 if !current_static.is_empty() {
147 segments.push(UrlSegment::Static(current_static.clone()));
148 current_static.clear();
149 }
150
151 let mut raw = String::new();
153 let mut closed = false;
154 for next_ch in chars.by_ref() {
155 if next_ch == '}' {
156 closed = true;
157 break;
158 }
159 raw.push(next_ch);
160 }
161
162 match (!raw.is_empty() && closed)
165 .then(|| normalize_placeholder(&raw))
166 .flatten()
167 {
168 Some(param_name) => {
169 if raw
170 .split_once('=')
171 .is_some_and(|(_, pattern)| pattern.contains('/') || pattern.contains("**"))
172 {
173 multi_segment.push(param_name.clone());
174 }
175 segments.push(UrlSegment::Parameter(param_name));
176 }
177 None if closed && !raw.is_empty() => {
182 segments.push(UrlSegment::Static(format!("{{{raw}}}")));
183 }
184 None => {}
185 }
186 } else {
187 current_static.push(ch);
188 }
189 }
190
191 if !current_static.is_empty() {
193 segments.push(UrlSegment::Static(current_static));
194 }
195
196 (segments, multi_segment)
197}
198
199fn extract_static_prefix(segments: &[UrlSegment]) -> String {
201 let mut prefix = String::new();
202 for segment in segments {
203 match segment {
204 UrlSegment::Static(literal) => prefix.push_str(literal),
205 UrlSegment::Parameter(_) => break,
206 }
207 }
208 prefix
209}
210
211fn extract_static_suffix(segments: &[UrlSegment]) -> Option<String> {
213 let mut suffix = String::new();
214 let mut found_last_param_index = None;
215
216 for (i, segment) in segments.iter().enumerate() {
218 if matches!(segment, UrlSegment::Parameter(_)) {
219 found_last_param_index = Some(i);
220 }
221 }
222
223 if let Some(last_param_index) = found_last_param_index {
225 for segment in segments.iter().skip(last_param_index + 1) {
226 if let UrlSegment::Static(literal) = segment {
227 suffix.push_str(literal);
228 }
229 }
230 }
231
232 (!suffix.is_empty()).then_some(suffix)
233}
234
235pub fn extract_path_parameters(path_template: &str) -> Vec<String> {
238 HttpPattern::parse(path_template).parameters
239}
240
241pub fn extract_http_rule_pattern(http_rule: &crate::google::api::HttpRule) -> Option<HttpPattern> {
243 use crate::google::api::http_rule::Pattern;
244
245 let template = match &http_rule.pattern {
246 Some(Pattern::Get(path)) => path,
247 Some(Pattern::Post(path)) => path,
248 Some(Pattern::Put(path)) => path,
249 Some(Pattern::Delete(path)) => path,
250 Some(Pattern::Patch(path)) => path,
251 Some(Pattern::Custom(custom)) => &custom.path,
252 None => return None,
253 };
254
255 Some(HttpPattern::parse(template))
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 #[test]
263 fn test_extract_path_parameters() {
264 assert_eq!(extract_path_parameters("/catalogs/{name}"), vec!["name"]);
265 assert_eq!(
266 extract_path_parameters("/shares/{share}/schemas/{schema}/tables/{name}"),
267 vec!["share", "schema", "name"]
268 );
269 assert_eq!(extract_path_parameters("/catalogs"), Vec::<String>::new());
270 }
271
272 #[test]
273 fn test_http_pattern_parsing() {
274 let pattern = HttpPattern::parse("/catalogs");
276 assert_eq!(pattern.parameters, Vec::<String>::new());
277 assert_eq!(pattern.static_prefix, "/catalogs");
278 assert_eq!(pattern.static_suffix, None);
279
280 let pattern = HttpPattern::parse("/catalogs/{name}");
282 assert_eq!(pattern.parameters, vec!["name"]);
283 assert_eq!(pattern.static_prefix, "/catalogs/");
284 assert_eq!(pattern.static_suffix, None);
285
286 let pattern = HttpPattern::parse("/shares/{share}/schemas/{schema}/tables/{name}");
288 assert_eq!(pattern.parameters, vec!["share", "schema", "name"]);
289 assert_eq!(pattern.static_prefix, "/shares/");
290 assert_eq!(pattern.static_suffix, None);
291
292 let pattern = HttpPattern::parse("/catalogs/{name}/metadata");
294 assert_eq!(pattern.parameters, vec!["name"]);
295 assert_eq!(pattern.static_prefix, "/catalogs/");
296 assert_eq!(pattern.static_suffix.as_deref(), Some("/metadata"));
297 }
298
299 #[test]
300 fn test_http_pattern_segments() {
301 let pattern = HttpPattern::parse("/shares/{share}/schemas/{schema}");
302
303 use UrlSegment;
304 assert_eq!(
305 pattern.segments,
306 vec![
307 UrlSegment::Static("/shares/".to_string()),
308 UrlSegment::Parameter("share".to_string()),
309 UrlSegment::Static("/schemas/".to_string()),
310 UrlSegment::Parameter("schema".to_string()),
311 ]
312 );
313 }
314
315 #[test]
316 fn test_http_pattern_to_format_string() {
317 let pattern = HttpPattern::parse("/catalogs/{name}");
318 let (format_str, args) = pattern.to_format_string();
319 assert_eq!(format_str, "catalogs/{}");
320 assert_eq!(args, vec!["name"]);
321
322 let pattern = HttpPattern::parse("/shares/{share}/schemas/{schema}");
323 let (format_str, args) = pattern.to_format_string();
324 assert_eq!(format_str, "shares/{}/schemas/{}");
325 assert_eq!(args, vec!["share", "schema"]);
326 }
327
328 #[test]
329 fn test_http_pattern_base_path() {
330 let pattern = HttpPattern::parse("/catalogs/{name}");
331 assert_eq!(pattern.base_path(), "catalogs");
332
333 let pattern = HttpPattern::parse("/shares/{share}/schemas");
334 assert_eq!(pattern.base_path(), "shares");
335 }
336
337 #[test]
338 fn test_unclosed_brace_is_ignored() {
339 let pattern = HttpPattern::parse("/catalogs/{name");
342 assert!(
343 pattern.parameters.is_empty(),
344 "unclosed brace must not produce a parameter"
345 );
346 assert_eq!(pattern.static_prefix, "/catalogs/");
347 }
348
349 #[test]
350 fn test_segment_binding_placeholder() {
351 let pattern = HttpPattern::parse("/v1/{name=catalogs/*}");
353 assert_eq!(pattern.parameters, vec!["name"]);
354
355 let (format_str, args) = pattern.to_format_string();
356 assert_eq!(args, vec!["name"]);
357 assert!(!format_str.contains('='), "got: {format_str}");
359 }
360
361 #[test]
362 fn test_dotted_placeholder_is_preserved_not_a_param() {
363 let pattern = HttpPattern::parse("/v1/tables/{table.catalog_name}/x");
367 assert!(
368 pattern.parameters.is_empty(),
369 "dotted placeholder must not become a parameter: {:?}",
370 pattern.parameters
371 );
372 assert!(
373 pattern.template.contains("{table.catalog_name}"),
374 "original template text is retained"
375 );
376 }
377
378 #[test]
379 fn test_degenerate_placeholder_is_ignored() {
380 let pattern = HttpPattern::parse("/v1/{=foo}");
382 assert!(pattern.parameters.is_empty());
383 }
384
385 #[test]
386 fn test_extract_http_rule_pattern() {
387 use crate::google::api::{HttpRule, http_rule::Pattern};
388
389 let http_rule = HttpRule {
391 pattern: Some(Pattern::Get("/catalogs/{name}".to_string())),
392 ..Default::default()
393 };
394
395 let pattern = extract_http_rule_pattern(&http_rule).unwrap();
396 assert_eq!(pattern.parameters, vec!["name"]);
397 assert_eq!(pattern.template, "/catalogs/{name}");
398
399 let http_rule = HttpRule {
401 pattern: Some(Pattern::Post("/catalogs".to_string())),
402 ..Default::default()
403 };
404
405 let pattern = extract_http_rule_pattern(&http_rule).unwrap();
406 assert_eq!(pattern.parameters, Vec::<String>::new());
407 assert_eq!(pattern.template, "/catalogs");
408
409 let http_rule = HttpRule {
411 pattern: None,
412 ..Default::default()
413 };
414
415 assert!(extract_http_rule_pattern(&http_rule).is_none());
416 }
417
418 #[test]
419 fn test_multi_segment_parameters() {
420 let pattern = HttpPattern::parse("/v1/{name=catalogs/*}/schemas/{schema}");
421 assert_eq!(pattern.multi_segment_parameters, vec!["name"]);
422 assert!(pattern.is_multi_segment("name"));
423 assert!(!pattern.is_multi_segment("schema"));
424
425 let pattern = HttpPattern::parse("/files/{path=**}");
426 assert!(pattern.is_multi_segment("path"));
427
428 let pattern = HttpPattern::parse("/catalogs/{name=*}");
430 assert!(!pattern.is_multi_segment("name"));
431 let pattern = HttpPattern::parse("/catalogs/{name}");
432 assert!(pattern.multi_segment_parameters.is_empty());
433 }
434}