olai_codegen/parsing/
http.rs1#[derive(Debug, Clone, PartialEq)]
3pub enum UrlSegment {
4 Static(String),
6 Parameter(String),
8}
9
10#[derive(Debug, Clone)]
12pub struct HttpPattern {
13 pub template: String,
15 pub segments: Vec<UrlSegment>,
17 pub parameters: Vec<String>,
19 pub static_prefix: String,
21 pub static_suffix: Option<String>,
23}
24
25impl HttpPattern {
26 pub fn parse(template: &str) -> Self {
28 let segments = parse_url_segments(template);
29 let parameters = segments
30 .iter()
31 .filter_map(|seg| match seg {
32 UrlSegment::Parameter(name) => Some(name.clone()),
33 UrlSegment::Static(_) => None,
34 })
35 .collect();
36
37 let static_prefix = extract_static_prefix(&segments);
38 let static_suffix = extract_static_suffix(&segments);
39
40 HttpPattern {
41 template: template.to_string(),
42 segments,
43 parameters,
44 static_prefix,
45 static_suffix,
46 }
47 }
48
49 pub fn ends_with_static(&self) -> bool {
50 self.segments
51 .last()
52 .is_some_and(|seg| matches!(seg, UrlSegment::Static(_)))
53 }
54
55 pub fn ends_with_parameter(&self) -> bool {
56 self.segments
57 .last()
58 .is_some_and(|seg| matches!(seg, UrlSegment::Parameter(_)))
59 }
60
61 pub fn base_path(&self) -> String {
63 self.static_prefix
64 .trim_start_matches('/')
65 .trim_end_matches('/')
66 .to_string()
67 }
68
69 pub fn parameter_names(&self) -> &[String] {
71 &self.parameters
72 }
73
74 pub fn to_format_string(&self) -> (String, Vec<String>) {
77 let mut format_parts = Vec::new();
78 let mut format_args = Vec::new();
79
80 for segment in &self.segments {
81 match segment {
82 UrlSegment::Static(literal) => {
83 format_parts.push(literal.clone());
84 }
85 UrlSegment::Parameter(name) => {
86 format_parts.push("{}".to_string());
87 format_args.push(name.clone());
88 }
89 }
90 }
91
92 (
93 format_parts.join("").trim_start_matches('/').to_string(),
94 format_args,
95 )
96 }
97}
98
99fn parse_url_segments(template: &str) -> Vec<UrlSegment> {
101 let mut segments = Vec::new();
102 let mut chars = template.chars().peekable();
103 let mut current_static = String::new();
104
105 while let Some(ch) = chars.next() {
106 if ch == '{' {
107 if !current_static.is_empty() {
109 segments.push(UrlSegment::Static(current_static.clone()));
110 current_static.clear();
111 }
112
113 let mut param_name = String::new();
115 let mut closed = false;
116 while let Some(&next_ch) = chars.peek() {
117 if next_ch == '}' {
118 chars.next(); closed = true;
120 break;
121 }
122 param_name.push(chars.next().unwrap());
123 }
124
125 if !param_name.is_empty() && closed {
128 segments.push(UrlSegment::Parameter(param_name));
129 }
130 } else {
131 current_static.push(ch);
132 }
133 }
134
135 if !current_static.is_empty() {
137 segments.push(UrlSegment::Static(current_static));
138 }
139
140 segments
141}
142
143fn extract_static_prefix(segments: &[UrlSegment]) -> String {
145 let mut prefix = String::new();
146 for segment in segments {
147 match segment {
148 UrlSegment::Static(literal) => prefix.push_str(literal),
149 UrlSegment::Parameter(_) => break,
150 }
151 }
152 prefix
153}
154
155fn extract_static_suffix(segments: &[UrlSegment]) -> Option<String> {
157 let mut suffix = String::new();
158 let mut found_last_param_index = None;
159
160 for (i, segment) in segments.iter().enumerate() {
162 if matches!(segment, UrlSegment::Parameter(_)) {
163 found_last_param_index = Some(i);
164 }
165 }
166
167 if let Some(last_param_index) = found_last_param_index {
169 for segment in segments.iter().skip(last_param_index + 1) {
170 if let UrlSegment::Static(literal) = segment {
171 suffix.push_str(literal);
172 }
173 }
174 }
175
176 (!suffix.is_empty()).then_some(suffix)
177}
178
179pub fn extract_path_parameters(path_template: &str) -> Vec<String> {
182 HttpPattern::parse(path_template).parameters
183}
184
185pub fn extract_http_rule_pattern(http_rule: &crate::google::api::HttpRule) -> Option<HttpPattern> {
187 use crate::google::api::http_rule::Pattern;
188
189 let template = match &http_rule.pattern {
190 Some(Pattern::Get(path)) => path,
191 Some(Pattern::Post(path)) => path,
192 Some(Pattern::Put(path)) => path,
193 Some(Pattern::Delete(path)) => path,
194 Some(Pattern::Patch(path)) => path,
195 Some(Pattern::Custom(custom)) => &custom.path,
196 None => return None,
197 };
198
199 Some(HttpPattern::parse(template))
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 #[test]
207 fn test_extract_path_parameters() {
208 assert_eq!(extract_path_parameters("/catalogs/{name}"), vec!["name"]);
209 assert_eq!(
210 extract_path_parameters("/shares/{share}/schemas/{schema}/tables/{name}"),
211 vec!["share", "schema", "name"]
212 );
213 assert_eq!(extract_path_parameters("/catalogs"), Vec::<String>::new());
214 }
215
216 #[test]
217 fn test_http_pattern_parsing() {
218 let pattern = HttpPattern::parse("/catalogs");
220 assert_eq!(pattern.parameters, Vec::<String>::new());
221 assert_eq!(pattern.static_prefix, "/catalogs");
222 assert_eq!(pattern.static_suffix, None);
223
224 let pattern = HttpPattern::parse("/catalogs/{name}");
226 assert_eq!(pattern.parameters, vec!["name"]);
227 assert_eq!(pattern.static_prefix, "/catalogs/");
228 assert_eq!(pattern.static_suffix, None);
229
230 let pattern = HttpPattern::parse("/shares/{share}/schemas/{schema}/tables/{name}");
232 assert_eq!(pattern.parameters, vec!["share", "schema", "name"]);
233 assert_eq!(pattern.static_prefix, "/shares/");
234 assert_eq!(pattern.static_suffix, None);
235
236 let pattern = HttpPattern::parse("/catalogs/{name}/metadata");
238 assert_eq!(pattern.parameters, vec!["name"]);
239 assert_eq!(pattern.static_prefix, "/catalogs/");
240 assert_eq!(pattern.static_suffix.as_deref(), Some("/metadata"));
241 }
242
243 #[test]
244 fn test_http_pattern_segments() {
245 let pattern = HttpPattern::parse("/shares/{share}/schemas/{schema}");
246
247 use UrlSegment;
248 assert_eq!(
249 pattern.segments,
250 vec![
251 UrlSegment::Static("/shares/".to_string()),
252 UrlSegment::Parameter("share".to_string()),
253 UrlSegment::Static("/schemas/".to_string()),
254 UrlSegment::Parameter("schema".to_string()),
255 ]
256 );
257 }
258
259 #[test]
260 fn test_http_pattern_to_format_string() {
261 let pattern = HttpPattern::parse("/catalogs/{name}");
262 let (format_str, args) = pattern.to_format_string();
263 assert_eq!(format_str, "catalogs/{}");
264 assert_eq!(args, vec!["name"]);
265
266 let pattern = HttpPattern::parse("/shares/{share}/schemas/{schema}");
267 let (format_str, args) = pattern.to_format_string();
268 assert_eq!(format_str, "shares/{}/schemas/{}");
269 assert_eq!(args, vec!["share", "schema"]);
270 }
271
272 #[test]
273 fn test_http_pattern_base_path() {
274 let pattern = HttpPattern::parse("/catalogs/{name}");
275 assert_eq!(pattern.base_path(), "catalogs");
276
277 let pattern = HttpPattern::parse("/shares/{share}/schemas");
278 assert_eq!(pattern.base_path(), "shares");
279 }
280
281 #[test]
282 fn test_unclosed_brace_is_ignored() {
283 let pattern = HttpPattern::parse("/catalogs/{name");
286 assert!(
287 pattern.parameters.is_empty(),
288 "unclosed brace must not produce a parameter"
289 );
290 assert_eq!(pattern.static_prefix, "/catalogs/");
291 }
292
293 #[test]
294 fn test_extract_http_rule_pattern() {
295 use crate::google::api::{HttpRule, http_rule::Pattern};
296
297 let http_rule = HttpRule {
299 pattern: Some(Pattern::Get("/catalogs/{name}".to_string())),
300 ..Default::default()
301 };
302
303 let pattern = extract_http_rule_pattern(&http_rule).unwrap();
304 assert_eq!(pattern.parameters, vec!["name"]);
305 assert_eq!(pattern.template, "/catalogs/{name}");
306
307 let http_rule = HttpRule {
309 pattern: Some(Pattern::Post("/catalogs".to_string())),
310 ..Default::default()
311 };
312
313 let pattern = extract_http_rule_pattern(&http_rule).unwrap();
314 assert_eq!(pattern.parameters, Vec::<String>::new());
315 assert_eq!(pattern.template, "/catalogs");
316
317 let http_rule = HttpRule {
319 pattern: None,
320 ..Default::default()
321 };
322
323 assert!(extract_http_rule_pattern(&http_rule).is_none());
324 }
325}