Skip to main content

soma_openapi/
convert.rs

1use crate::error::OpenApiError;
2
3#[derive(Debug, Clone)]
4pub struct OperationDescriptor {
5    pub operation_id: String,
6    pub method: reqwest::Method,
7    pub path_template: String,
8}
9
10const HTTP_METHOD_KEYS: &[&str] = &[
11    "get", "put", "post", "delete", "options", "head", "patch", "trace",
12];
13
14pub fn convert_spec(
15    label: &str,
16    spec_json: &str,
17    allowed: &[String],
18) -> Result<Vec<OperationDescriptor>, OpenApiError> {
19    let value: serde_json::Value =
20        serde_json::from_str(spec_json).map_err(|_| OpenApiError::SpecParse {
21            label: label.to_string(),
22        })?;
23
24    let paths = value
25        .get("paths")
26        .and_then(serde_json::Value::as_object)
27        .ok_or_else(|| OpenApiError::SpecParse {
28            label: label.to_string(),
29        })?;
30
31    let mut out = Vec::new();
32    for (path_template, path_item) in paths {
33        let Some(path_item) = path_item.as_object() else {
34            continue;
35        };
36        for method_key in HTTP_METHOD_KEYS {
37            let Some(operation) = path_item
38                .get(*method_key)
39                .and_then(serde_json::Value::as_object)
40            else {
41                continue;
42            };
43            let Some(operation_id) = operation
44                .get("operationId")
45                .and_then(serde_json::Value::as_str)
46            else {
47                continue;
48            };
49            if !allowed.iter().any(|allowed| allowed == operation_id) {
50                continue;
51            }
52            validate_operation_path_template(label, path_template)?;
53            out.push(OperationDescriptor {
54                operation_id: operation_id.to_string(),
55                method: parse_method(label, operation_id, method_key),
56                path_template: path_template.clone(),
57            });
58        }
59    }
60    Ok(out)
61}
62
63fn validate_operation_path_template(label: &str, path_template: &str) -> Result<(), OpenApiError> {
64    if !path_template.starts_with('/') || path_template.contains('\\') {
65        return Err(OpenApiError::SpecParse {
66            label: label.to_string(),
67        });
68    }
69    if path_template
70        .split('/')
71        .any(|segment| matches!(segment, "." | ".."))
72    {
73        return Err(OpenApiError::SpecParse {
74            label: label.to_string(),
75        });
76    }
77    Ok(())
78}
79
80fn parse_method(label: &str, operation_id: &str, raw: &str) -> reqwest::Method {
81    raw.to_ascii_uppercase()
82        .parse::<reqwest::Method>()
83        .unwrap_or_else(|_| {
84            tracing::warn!(
85                service = "openapi",
86                label = %label,
87                operation = %operation_id,
88                "openapi: unparseable HTTP method in spec, falling back to GET"
89            );
90            reqwest::Method::GET
91        })
92}