1use std::collections::HashMap;
2
3use super::{OpenApiV3, Parameter, Property};
4
5use heck::{ToLowerCamelCase, ToSnakeCase, ToUpperCamelCase};
6use itertools::Itertools;
7use ramhorns_derive::Content;
8
9use log::debug;
10
11#[derive(Default, Content, Clone, Debug)]
12#[ramhorns(rename_all = "camelCase")]
13pub(crate) struct Operation {
14 classname: String,
15 class_filename: String,
16
17 response_headers: Vec<Property>,
18
19 return_type_is_primitive: bool,
20 return_simple_type: bool,
21 subresource_operation: bool,
22 is_multipart: bool,
23 is_response_binary: bool,
24 is_response_file: bool,
25 is_response_optional: bool,
26 has_reference: bool,
27 is_restful_index: bool,
28 is_restful_show: bool,
29 is_restful_create: bool,
30 is_restful_update: bool,
31 is_restful_destroy: bool,
32 is_restful: bool,
33 is_deprecated: Option<bool>,
34 is_callback_request: bool,
35 unique_items: bool,
36 has_default_response: bool,
37 has_error_response_object: bool,
39
40 path: String,
41 operation_id: Option<String>,
42 return_type: Option<String>,
43 return_format: String,
44 http_method: String,
45 return_base_type: String,
46 return_container: String,
47 summary: Option<String>,
48 unescaped_notes: String,
49 basename: String,
50 default_response: String,
51
52 consumes: Vec<std::collections::HashMap<String, String>>,
53 has_consumes: bool,
54 produces: Vec<std::collections::HashMap<String, String>>,
55 has_produces: bool,
56 prioritized_content_types: Vec<std::collections::HashMap<String, String>>,
57
58 all_params: Vec<Parameter>,
59 has_params: bool,
60 path_params: Vec<Parameter>,
61 has_path_params: bool,
62 query_params: Vec<Parameter>,
63 has_query_params: bool,
64 header_params: Vec<Parameter>,
65 has_header_params: bool,
66 has_body_param: bool,
67 body_param: Option<Parameter>,
68 implicit_headers_params: Vec<Parameter>,
69 has_implicit_headers_params: bool,
70 form_params: Vec<Parameter>,
71 has_form_params: bool,
72 required_params: Vec<Parameter>,
73 has_required_params: bool,
74 optional_params: Vec<Parameter>,
75 has_optional_params: bool,
76 auth_methods: Vec<AuthMethod>,
77 pub(crate) has_auth_methods: bool,
78
79 tags: Vec<String>,
80 responses: Vec<()>,
81 callbacks: Vec<()>,
82
83 examples: Vec<HashMap<String, String>>,
84 request_body_examples: Vec<HashMap<String, String>>,
85
86 vendor_extensions: HashMap<String, String>,
87
88 pub(crate) operation_id_original: Option<String>,
89 operation_id_camel_case: Option<String>,
90 operation_id_lower_case: Option<String>,
91 support_multiple_responses: bool,
92
93 description: Option<String>,
94
95 api_doc_path: &'static str,
96 model_doc_path: &'static str,
97}
98
99fn query_param(api: &OpenApiV3, value: &openapiv3::Parameter) -> Option<Parameter> {
100 match value {
101 openapiv3::Parameter::Query { parameter_data, .. } => {
102 let parameter = Parameter::new(api, parameter_data);
103 Some(parameter)
104 }
105 _ => None,
106 }
107}
108fn path_param(api: &OpenApiV3, value: &openapiv3::Parameter) -> Option<Parameter> {
109 match value {
110 openapiv3::Parameter::Path { parameter_data, .. } => {
111 let parameter = Parameter::new(api, parameter_data);
112 Some(parameter)
113 }
114 _ => None,
115 }
116}
117#[allow(unused)]
118fn header_param(api: &OpenApiV3, value: &openapiv3::Parameter) -> Option<Parameter> {
119 match value {
120 openapiv3::Parameter::Header { parameter_data, .. } => {
121 let parameter = Parameter::new(api, parameter_data);
122 Some(parameter)
123 }
124 _ => None,
125 }
126}
127fn body_param(api: &OpenApiV3, value: &openapiv3::RequestBody) -> Option<Parameter> {
128 Parameter::from_body(api, value)
129}
130
131impl Operation {
132 pub(crate) fn new(
134 root: &OpenApiV3,
135 path: &str,
136 method: &str,
137 operation: &openapiv3::Operation,
138 ) -> Self {
139 debug!(
140 "Operation::{id:?} => {method}::{path}::{tags:?}",
141 id = operation.operation_id,
142 tags = operation.tags
143 );
144 let mut vendor_extensions = operation
145 .extensions
146 .iter()
147 .map(|(k, v)| (k.clone(), v.to_string()))
148 .collect::<HashMap<_, _>>();
149
150 vendor_extensions.insert("x-httpMethodLower".into(), method.to_ascii_lowercase());
151 vendor_extensions.insert("x-httpMethodUpper".into(), method.to_ascii_uppercase());
152
153 let query_params = operation
154 .parameters
155 .iter()
156 .flat_map(|p| {
157 match p {
158 openapiv3::ReferenceOr::Reference { .. } => todo!(),
160 openapiv3::ReferenceOr::Item(item) => query_param(root, item),
161 }
162 })
163 .collect::<Vec<_>>();
164 let path_params = operation
165 .parameters
166 .iter()
167 .flat_map(|p| {
168 match p {
169 openapiv3::ReferenceOr::Reference { .. } => todo!(),
171 openapiv3::ReferenceOr::Item(item) => path_param(root, item),
172 }
173 })
174 .sorted_by(|a, b| b.required().cmp(&a.required()))
175 .collect::<Vec<_>>();
176 let body_param = operation.request_body.as_ref().and_then(|p| {
177 match p {
178 openapiv3::ReferenceOr::Reference { .. } => todo!(),
180 openapiv3::ReferenceOr::Item(item) => body_param(root, item),
181 }
182 });
183
184 let mut ext_path = path.to_string();
185 for param in &path_params {
186 if param.data_format() == "url" {
187 ext_path = path.replace(param.name(), &format!("{}:.*", param.base_name()));
188 vendor_extensions.insert("x-actix-query-string".into(), "true".into());
189 }
190 }
191 vendor_extensions.insert("x-actixPath".into(), ext_path);
192
193 let all_params = path_params
194 .iter()
195 .chain(
196 query_params
197 .iter()
198 .sorted_by(|a, b| b.required().cmp(&a.required())),
199 )
200 .chain(&body_param)
201 .cloned()
202 .collect::<Vec<_>>();
203 let return_model = match operation
205 .responses
206 .responses
207 .get(&openapiv3::StatusCode::Code(200))
208 .or(operation
209 .responses
210 .responses
211 .get(&openapiv3::StatusCode::Code(204)))
212 {
213 Some(ref_or) => root.resolve_reference_or_resp("application/json", ref_or),
214 None => todo!(),
215 };
216 let return_model = return_model.post_process_data_type();
218 let (class, class_file) = match operation.tags.first() {
219 Some(class) => (class.clone(), format!("{class}_api").to_snake_case()),
220 None => (String::new(), String::new()),
222 };
223 Self {
224 description: operation.description.as_ref().map(|d| d.replace('\n', " ")),
225 classname: class,
226 class_filename: class_file,
227 summary: operation.summary.clone(),
228 tags: operation.tags.clone(),
229 is_deprecated: Some(operation.deprecated),
230 operation_id_lower_case: operation.operation_id.as_ref().map(|o| o.to_lowercase()),
231 operation_id_camel_case: operation
232 .operation_id
233 .as_ref()
234 .map(|o| o.to_lower_camel_case()),
235 operation_id: operation.operation_id.clone(),
236 operation_id_original: operation.operation_id.clone(),
237 has_params: !all_params.is_empty(),
238 all_params,
239 has_path_params: !path_params.is_empty(),
240 path_params,
241 has_query_params: !query_params.is_empty(),
242 query_params,
243 header_params: vec![],
244 has_header_params: false,
245 has_body_param: body_param.is_some(),
246 body_param,
247 path: path.to_string(),
248 http_method: method.to_upper_camel_case(),
249 support_multiple_responses: false,
250 return_type: {
251 let data_type = return_model.data_type();
252 if data_type == "()" {
253 None
254 } else {
255 Some(data_type)
256 }
257 },
258 has_auth_methods: operation.security.is_some(),
259 auth_methods: match &operation.security {
260 None => vec![],
261 Some(sec) => sec
262 .iter()
263 .flat_map(|a| {
264 a.iter()
265 .map(|(key, _)| match key.as_str() {
266 "JWT" => AuthMethod {
267 scheme: "JWT".to_string(),
268 is_basic: true,
269 is_basic_bearer: true,
270 },
271 scheme => AuthMethod {
272 scheme: scheme.to_string(),
273 is_basic: false,
274 is_basic_bearer: false,
275 },
276 })
277 .collect::<Vec<_>>()
278 })
279 .collect::<Vec<_>>(),
280 },
281 vendor_extensions,
282 api_doc_path: "docs/apis/",
283 model_doc_path: "docs/models/",
284 ..Default::default()
285 }
286 }
287 pub fn tags(&self) -> &Vec<String> {
289 &self.tags
290 }
291 pub fn classname(&self) -> &str {
293 &self.classname
294 }
295 pub fn class_filename(&self) -> &str {
297 &self.class_filename
298 }
299}
300
301#[derive(Default, Content, Clone, Debug)]
302#[ramhorns(rename_all = "camelCase")]
303struct AuthMethod {
304 scheme: String,
305
306 is_basic: bool,
307 is_basic_bearer: bool,
308}