Skip to main content

olai_codegen/parsing/
http.rs

1/// Represents a segment in a URL template
2#[derive(Debug, Clone, PartialEq)]
3pub enum UrlSegment {
4    /// A static literal segment like "catalogs" or "metadata"
5    Static(String),
6    /// A path parameter like "{name}" or "{catalog_name}"
7    Parameter(String),
8}
9
10/// Parsed representation of an HTTP rule pattern
11#[derive(Debug, Clone)]
12pub struct HttpPattern {
13    /// The original template string
14    pub template: String,
15    /// Parsed segments in order
16    pub segments: Vec<UrlSegment>,
17    /// Just the parameter names in order (for backward compatibility)
18    pub parameters: Vec<String>,
19    /// Static prefix (everything before the first parameter)
20    pub static_prefix: String,
21    /// Static suffix (everything after the last parameter)
22    pub static_suffix: Option<String>,
23}
24
25impl HttpPattern {
26    /// Parse an HTTP rule pattern template
27    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    /// Get the base path (static prefix without leading slash)
62    pub fn base_path(&self) -> String {
63        self.static_prefix
64            .trim_start_matches('/')
65            .trim_end_matches('/')
66            .to_string()
67    }
68
69    /// Get parameter names in the order they appear in the URL
70    pub fn parameter_names(&self) -> &[String] {
71        &self.parameters
72    }
73
74    /// Generate a format string for URL construction
75    /// Returns ("/catalogs/{}", ["name"]) for "/catalogs/{name}"
76    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
99/// Parse URL template into segments
100fn 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            // Save any accumulated static content
108            if !current_static.is_empty() {
109                segments.push(UrlSegment::Static(current_static.clone()));
110                current_static.clear();
111            }
112
113            // Parse parameter name
114            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(); // consume the '}'
119                    closed = true;
120                    break;
121                }
122                param_name.push(chars.next().unwrap());
123            }
124
125            // An unclosed brace (e.g. `{name` with no `}`) is malformed — skip it rather
126            // than emitting a parameter with a truncated or empty name.
127            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    // Add any remaining static content
136    if !current_static.is_empty() {
137        segments.push(UrlSegment::Static(current_static));
138    }
139
140    segments
141}
142
143/// Extract static prefix (everything before first parameter)
144fn 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
155/// Extract static suffix (everything after last parameter)
156fn extract_static_suffix(segments: &[UrlSegment]) -> Option<String> {
157    let mut suffix = String::new();
158    let mut found_last_param_index = None;
159
160    // Find the last parameter index
161    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 we found a parameter, collect everything after it
168    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
179/// Extract path parameter names from URL template like "/catalogs/{name}"
180/// (Kept for backward compatibility)
181pub fn extract_path_parameters(path_template: &str) -> Vec<String> {
182    HttpPattern::parse(path_template).parameters
183}
184
185/// Extract pattern information from an HttpRule
186pub 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        // Test simple static path
219        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        // Test single parameter
225        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        // Test multiple parameters
231        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        // Test parameter with suffix
237        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        // A malformed template like `/catalogs/{name` (missing closing brace)
284        // should not produce a parameter — the unclosed segment is discarded.
285        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        // Test GET pattern
298        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        // Test POST pattern
308        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        // Test None pattern
318        let http_rule = HttpRule {
319            pattern: None,
320            ..Default::default()
321        };
322
323        assert!(extract_http_rule_pattern(&http_rule).is_none());
324    }
325}