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}