openapi_nexus_core/data/
operation_info.rs1use std::collections::BTreeMap;
4
5use heck::{ToLowerCamelCase as _, ToPascalCase as _};
6use serde::{Deserialize, Serialize};
7use tracing::error;
8use utoipa::openapi;
9
10use crate::data::api_method_data::ApiMethodData;
11use crate::data::parameter_info::ParameterInfo;
12use crate::data::{HttpResponse, StatusCode};
13use crate::serde::http_method;
14use crate::traits::OpenApiParameterExt as _;
15use crate::traits::OpenApiRefExt as _;
16use crate::traits::OperationInfoExt;
17
18#[derive(Clone, Serialize, Deserialize)]
20pub struct OperationInfo {
21 pub path: String,
22 #[serde(with = "http_method")]
23 pub method: http::Method,
24 pub operation: openapi::path::Operation,
25}
26
27impl OperationInfoExt for OperationInfo {
28 fn method_name(&self) -> String {
29 if let Some(operation_id) = self.operation.operation_id.as_ref() {
30 operation_id.to_lower_camel_case()
31 } else {
32 let method = self.method.as_str();
34 let mut name = method.to_lowercase();
35 for part in self.path.split('/') {
36 if !part.is_empty() && !part.starts_with('{') {
37 name.push_str(&part.to_pascal_case());
38 }
39 }
40 name.to_lower_camel_case()
41 }
42 }
43
44 fn parameters(&self) -> Vec<openapi::path::Parameter> {
45 if let Some(parameters) = &self.operation.parameters {
46 parameters.clone()
47 } else {
48 Vec::new()
49 }
50 }
51}
52
53impl OperationInfo {
54 pub fn to_api_method_data(&self, components: Option<&openapi::Components>) -> ApiMethodData {
56 let method_name = self.method_name();
57
58 let mut path_params = Vec::new();
60 let mut query_params = Vec::new();
61 let mut header_params = Vec::new();
62
63 if let Some(params) = &self.operation.parameters {
64 for param in params {
65 let schema = param.schema.clone();
67
68 let required = param.required();
69 let deprecated = param.deprecated();
70
71 let default_value = param.default_value(components);
73
74 let param_info = ParameterInfo {
75 original_name: param.name.clone(),
76 param_name: param.name.clone(),
77 schema,
78 required,
79 deprecated,
80 description: param.description.clone(),
81 default_value,
82 location: param.parameter_in.clone().into(),
83 };
84 match param.parameter_in {
85 openapi::path::ParameterIn::Path => path_params.push(param_info),
86 openapi::path::ParameterIn::Query => query_params.push(param_info),
87 openapi::path::ParameterIn::Header => header_params.push(param_info),
88 openapi::path::ParameterIn::Cookie => header_params.push(param_info), }
90 }
91 }
92
93 let return_type = extract_return_type_from_responses(&self.operation);
95 ApiMethodData {
96 method_name,
97 http_method: self.method.clone(),
98 path: self.path.clone(),
99 path_params,
100 query_params,
101 header_params,
102 request_body: self.operation.request_body.clone(),
103 return_type,
104 has_auth: self.operation.security.is_some(),
105 has_error_handling: true,
106 }
107 }
108
109 pub fn collect_responses(
110 &self,
111 components: Option<&openapi::Components>,
112 ) -> (
113 BTreeMap<StatusCode, HttpResponse>,
114 BTreeMap<StatusCode, HttpResponse>,
115 Option<HttpResponse>,
116 ) {
117 let mut success = BTreeMap::new();
118 let mut error = BTreeMap::new();
119 let mut default_response = None;
120
121 for (status_code, response_ref) in &self.operation.responses.responses {
122 let status = StatusCode::new(status_code);
123 let response = match response_ref {
124 openapi::RefOr::T(response) => HttpResponse::from_openapi(status.clone(), response),
125 openapi::RefOr::Ref(reference) => {
126 if let Some(resolved) = reference.resolve_response(components) {
127 HttpResponse::from_openapi(status.clone(), resolved)
128 } else {
129 error!(%status, ?reference, "Failed to resolve response reference.");
130 continue;
131 }
132 }
133 };
134
135 if status.is_default() {
136 default_response = Some(response);
137 } else if response.is_success() {
138 success.insert(status, response);
139 } else {
140 error.insert(status, response);
141 }
142 }
143
144 (success, error, default_response)
145 }
146}
147
148fn extract_return_type_from_responses(
150 operation: &openapi::path::Operation,
151) -> Option<openapi::RefOr<openapi::schema::Schema>> {
152 for (status_code, response_ref) in operation.responses.responses.iter() {
153 if status_code.starts_with('2')
154 && let openapi::RefOr::T(response) = response_ref
155 && let Some(json_content) = response.content.get("application/json")
156 {
157 return json_content.schema.clone();
158 }
159 }
160 None
161}