olai_codegen/parsing/
http.rs1#[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}
28
29impl HttpPattern {
30 pub fn parse(template: &str) -> Self {
32 let segments = parse_url_segments(template);
33 let parameters = segments
34 .iter()
35 .filter_map(|seg| match seg {
36 UrlSegment::Parameter(name) => Some(name.clone()),
37 UrlSegment::Static(_) => None,
38 })
39 .collect();
40
41 let static_prefix = extract_static_prefix(&segments);
42 let static_suffix = extract_static_suffix(&segments);
43
44 HttpPattern {
45 template: template.to_string(),
46 segments,
47 parameters,
48 static_prefix,
49 static_suffix,
50 }
51 }
52
53 pub fn ends_with_static(&self) -> bool {
54 self.segments
55 .last()
56 .is_some_and(|seg| matches!(seg, UrlSegment::Static(_)))
57 }
58
59 pub fn ends_with_parameter(&self) -> bool {
60 self.segments
61 .last()
62 .is_some_and(|seg| matches!(seg, UrlSegment::Parameter(_)))
63 }
64
65 pub fn base_path(&self) -> String {
67 self.static_prefix
68 .trim_start_matches('/')
69 .trim_end_matches('/')
70 .to_string()
71 }
72
73 pub fn parameter_names(&self) -> &[String] {
75 &self.parameters
76 }
77
78 pub fn to_format_string(&self) -> (String, Vec<String>) {
81 let mut format_parts = Vec::new();
82 let mut format_args = Vec::new();
83
84 for segment in &self.segments {
85 match segment {
86 UrlSegment::Static(literal) => {
87 format_parts.push(literal.clone());
88 }
89 UrlSegment::Parameter(name) => {
90 format_parts.push("{}".to_string());
91 format_args.push(name.clone());
92 }
93 }
94 }
95
96 (
97 format_parts.join("").trim_start_matches('/').to_string(),
98 format_args,
99 )
100 }
101}
102
103fn normalize_placeholder(raw: &str) -> Option<String> {
119 let name = raw.split('=').next().unwrap_or("").trim();
120 if name.is_empty() || name.contains('.') {
121 return None;
122 }
123 Some(name.to_string())
124}
125
126fn parse_url_segments(template: &str) -> Vec<UrlSegment> {
128 let mut segments = Vec::new();
129 let mut chars = template.chars().peekable();
130 let mut current_static = String::new();
131
132 while let Some(ch) = chars.next() {
133 if ch == '{' {
134 if !current_static.is_empty() {
136 segments.push(UrlSegment::Static(current_static.clone()));
137 current_static.clear();
138 }
139
140 let mut raw = String::new();
142 let mut closed = false;
143 for next_ch in chars.by_ref() {
144 if next_ch == '}' {
145 closed = true;
146 break;
147 }
148 raw.push(next_ch);
149 }
150
151 match (!raw.is_empty() && closed)
154 .then(|| normalize_placeholder(&raw))
155 .flatten()
156 {
157 Some(param_name) => segments.push(UrlSegment::Parameter(param_name)),
158 None if closed && !raw.is_empty() => {
163 segments.push(UrlSegment::Static(format!("{{{raw}}}")));
164 }
165 None => {}
166 }
167 } else {
168 current_static.push(ch);
169 }
170 }
171
172 if !current_static.is_empty() {
174 segments.push(UrlSegment::Static(current_static));
175 }
176
177 segments
178}
179
180fn extract_static_prefix(segments: &[UrlSegment]) -> String {
182 let mut prefix = String::new();
183 for segment in segments {
184 match segment {
185 UrlSegment::Static(literal) => prefix.push_str(literal),
186 UrlSegment::Parameter(_) => break,
187 }
188 }
189 prefix
190}
191
192fn extract_static_suffix(segments: &[UrlSegment]) -> Option<String> {
194 let mut suffix = String::new();
195 let mut found_last_param_index = None;
196
197 for (i, segment) in segments.iter().enumerate() {
199 if matches!(segment, UrlSegment::Parameter(_)) {
200 found_last_param_index = Some(i);
201 }
202 }
203
204 if let Some(last_param_index) = found_last_param_index {
206 for segment in segments.iter().skip(last_param_index + 1) {
207 if let UrlSegment::Static(literal) = segment {
208 suffix.push_str(literal);
209 }
210 }
211 }
212
213 (!suffix.is_empty()).then_some(suffix)
214}
215
216pub fn extract_path_parameters(path_template: &str) -> Vec<String> {
219 HttpPattern::parse(path_template).parameters
220}
221
222pub fn extract_http_rule_pattern(http_rule: &crate::google::api::HttpRule) -> Option<HttpPattern> {
224 use crate::google::api::http_rule::Pattern;
225
226 let template = match &http_rule.pattern {
227 Some(Pattern::Get(path)) => path,
228 Some(Pattern::Post(path)) => path,
229 Some(Pattern::Put(path)) => path,
230 Some(Pattern::Delete(path)) => path,
231 Some(Pattern::Patch(path)) => path,
232 Some(Pattern::Custom(custom)) => &custom.path,
233 None => return None,
234 };
235
236 Some(HttpPattern::parse(template))
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242
243 #[test]
244 fn test_extract_path_parameters() {
245 assert_eq!(extract_path_parameters("/catalogs/{name}"), vec!["name"]);
246 assert_eq!(
247 extract_path_parameters("/shares/{share}/schemas/{schema}/tables/{name}"),
248 vec!["share", "schema", "name"]
249 );
250 assert_eq!(extract_path_parameters("/catalogs"), Vec::<String>::new());
251 }
252
253 #[test]
254 fn test_http_pattern_parsing() {
255 let pattern = HttpPattern::parse("/catalogs");
257 assert_eq!(pattern.parameters, Vec::<String>::new());
258 assert_eq!(pattern.static_prefix, "/catalogs");
259 assert_eq!(pattern.static_suffix, None);
260
261 let pattern = HttpPattern::parse("/catalogs/{name}");
263 assert_eq!(pattern.parameters, vec!["name"]);
264 assert_eq!(pattern.static_prefix, "/catalogs/");
265 assert_eq!(pattern.static_suffix, None);
266
267 let pattern = HttpPattern::parse("/shares/{share}/schemas/{schema}/tables/{name}");
269 assert_eq!(pattern.parameters, vec!["share", "schema", "name"]);
270 assert_eq!(pattern.static_prefix, "/shares/");
271 assert_eq!(pattern.static_suffix, None);
272
273 let pattern = HttpPattern::parse("/catalogs/{name}/metadata");
275 assert_eq!(pattern.parameters, vec!["name"]);
276 assert_eq!(pattern.static_prefix, "/catalogs/");
277 assert_eq!(pattern.static_suffix.as_deref(), Some("/metadata"));
278 }
279
280 #[test]
281 fn test_http_pattern_segments() {
282 let pattern = HttpPattern::parse("/shares/{share}/schemas/{schema}");
283
284 use UrlSegment;
285 assert_eq!(
286 pattern.segments,
287 vec![
288 UrlSegment::Static("/shares/".to_string()),
289 UrlSegment::Parameter("share".to_string()),
290 UrlSegment::Static("/schemas/".to_string()),
291 UrlSegment::Parameter("schema".to_string()),
292 ]
293 );
294 }
295
296 #[test]
297 fn test_http_pattern_to_format_string() {
298 let pattern = HttpPattern::parse("/catalogs/{name}");
299 let (format_str, args) = pattern.to_format_string();
300 assert_eq!(format_str, "catalogs/{}");
301 assert_eq!(args, vec!["name"]);
302
303 let pattern = HttpPattern::parse("/shares/{share}/schemas/{schema}");
304 let (format_str, args) = pattern.to_format_string();
305 assert_eq!(format_str, "shares/{}/schemas/{}");
306 assert_eq!(args, vec!["share", "schema"]);
307 }
308
309 #[test]
310 fn test_http_pattern_base_path() {
311 let pattern = HttpPattern::parse("/catalogs/{name}");
312 assert_eq!(pattern.base_path(), "catalogs");
313
314 let pattern = HttpPattern::parse("/shares/{share}/schemas");
315 assert_eq!(pattern.base_path(), "shares");
316 }
317
318 #[test]
319 fn test_unclosed_brace_is_ignored() {
320 let pattern = HttpPattern::parse("/catalogs/{name");
323 assert!(
324 pattern.parameters.is_empty(),
325 "unclosed brace must not produce a parameter"
326 );
327 assert_eq!(pattern.static_prefix, "/catalogs/");
328 }
329
330 #[test]
331 fn test_segment_binding_placeholder() {
332 let pattern = HttpPattern::parse("/v1/{name=catalogs/*}");
334 assert_eq!(pattern.parameters, vec!["name"]);
335
336 let (format_str, args) = pattern.to_format_string();
337 assert_eq!(args, vec!["name"]);
338 assert!(!format_str.contains('='), "got: {format_str}");
340 }
341
342 #[test]
343 fn test_dotted_placeholder_is_preserved_not_a_param() {
344 let pattern = HttpPattern::parse("/v1/tables/{table.catalog_name}/x");
348 assert!(
349 pattern.parameters.is_empty(),
350 "dotted placeholder must not become a parameter: {:?}",
351 pattern.parameters
352 );
353 assert!(
354 pattern.template.contains("{table.catalog_name}"),
355 "original template text is retained"
356 );
357 }
358
359 #[test]
360 fn test_degenerate_placeholder_is_ignored() {
361 let pattern = HttpPattern::parse("/v1/{=foo}");
363 assert!(pattern.parameters.is_empty());
364 }
365
366 #[test]
367 fn test_extract_http_rule_pattern() {
368 use crate::google::api::{HttpRule, http_rule::Pattern};
369
370 let http_rule = HttpRule {
372 pattern: Some(Pattern::Get("/catalogs/{name}".to_string())),
373 ..Default::default()
374 };
375
376 let pattern = extract_http_rule_pattern(&http_rule).unwrap();
377 assert_eq!(pattern.parameters, vec!["name"]);
378 assert_eq!(pattern.template, "/catalogs/{name}");
379
380 let http_rule = HttpRule {
382 pattern: Some(Pattern::Post("/catalogs".to_string())),
383 ..Default::default()
384 };
385
386 let pattern = extract_http_rule_pattern(&http_rule).unwrap();
387 assert_eq!(pattern.parameters, Vec::<String>::new());
388 assert_eq!(pattern.template, "/catalogs");
389
390 let http_rule = HttpRule {
392 pattern: None,
393 ..Default::default()
394 };
395
396 assert!(extract_http_rule_pattern(&http_rule).is_none());
397 }
398}