openapi_nexus_core/data/
operation_info.rs1use std::collections::BTreeMap;
4
5use heck::{ToLowerCamelCase as _, ToPascalCase as _};
6use serde::{Deserialize, Serialize};
7use tracing::error;
8
9use crate::data::api_method_data::ApiMethodData;
10use crate::data::parameter_info::ParameterInfo;
11use crate::data::{HttpResponse, StatusCode};
12use crate::serde::http_method;
13use crate::traits::OperationInfoExt;
14use openapi_nexus_spec::oas31::spec::{
15 Components, ObjectOrReference, ObjectSchema, Operation, Parameter, ParameterIn,
16};
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: 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<Parameter> {
45 self.operation
46 .parameters
47 .iter()
48 .filter_map(|param_ref| {
49 match param_ref {
50 ObjectOrReference::Object(param) => Some(param.clone()),
51 ObjectOrReference::Ref { .. } => None, }
53 })
54 .collect()
55 }
56}
57
58impl OperationInfo {
59 pub fn to_api_method_data(&self, _components: Option<&Components>) -> ApiMethodData {
61 let method_name = self.method_name();
62
63 let mut path_params = Vec::new();
65 let mut query_params = Vec::new();
66 let mut header_params = Vec::new();
67
68 for param_ref in &self.operation.parameters {
69 let param = match param_ref {
70 ObjectOrReference::Object(param) => param,
71 ObjectOrReference::Ref { .. } => {
72 continue;
74 }
75 };
76
77 let schema = param.schema.clone();
79
80 let required = param.required.unwrap_or(false);
81 let deprecated = param.deprecated.unwrap_or(false);
82
83 let default_value = None; let param_info = ParameterInfo {
87 original_name: param.name.clone(),
88 param_name: param.name.clone(),
89 schema,
90 required,
91 deprecated,
92 description: param.description.clone(),
93 default_value,
94 location: param.location.into(),
95 };
96 match param.location {
97 ParameterIn::Path => path_params.push(param_info),
98 ParameterIn::Query => query_params.push(param_info),
99 ParameterIn::Header => header_params.push(param_info),
100 ParameterIn::Cookie => header_params.push(param_info), }
102 }
103
104 let return_type = extract_return_type_from_responses(&self.operation);
106 ApiMethodData {
107 method_name,
108 http_method: self.method.clone(),
109 path: self.path.clone(),
110 path_params,
111 query_params,
112 header_params,
113 request_body: self.operation.request_body.clone(),
114 return_type,
115 has_auth: !self.operation.security.is_empty(),
116 has_error_handling: true,
117 }
118 }
119
120 pub fn collect_responses(
121 &self,
122 _components: Option<&Components>,
123 ) -> (
124 BTreeMap<StatusCode, HttpResponse>,
125 BTreeMap<StatusCode, HttpResponse>,
126 Option<HttpResponse>,
127 ) {
128 let mut success = BTreeMap::new();
129 let mut error = BTreeMap::new();
130 let mut default_response = None;
131
132 if let Some(responses) = &self.operation.responses {
133 for (status_code, response_ref) in responses {
134 let status = StatusCode::new(status_code);
135 let response = match response_ref {
136 ObjectOrReference::Object(response) => {
137 HttpResponse::from_openapi(status.clone(), response)
138 }
139 ObjectOrReference::Ref { ref_path, .. } => {
140 error!(%status, %ref_path, "Failed to resolve response reference.");
142 continue;
143 }
144 };
145
146 if status.is_default() {
147 default_response = Some(response);
148 } else if response.is_success() {
149 success.insert(status, response);
150 } else {
151 error.insert(status, response);
152 }
153 }
154 }
155
156 (success, error, default_response)
157 }
158}
159
160fn extract_return_type_from_responses(
162 operation: &Operation,
163) -> Option<ObjectOrReference<ObjectSchema>> {
164 if let Some(responses) = &operation.responses {
165 for (status_code, response_ref) in responses {
166 if status_code.starts_with('2')
167 && let ObjectOrReference::Object(response) = response_ref
168 && let Some(json_content) = response.content.get("application/json")
169 {
170 return json_content.schema.clone();
171 }
172 }
173 }
174 None
175}