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.vendor_extension("x-actix-tail-match") == Some("true") {
187 ext_path = ext_path.replace(param.name(), &format!("{}:.*", param.base_name()));
188 } else if param.data_format() == "url" {
189 ext_path = ext_path.replace(param.name(), &format!("{}:.*", param.base_name()));
190 vendor_extensions.insert("x-actix-query-string".into(), "true".into());
191 }
192 }
193 vendor_extensions.insert("x-actixPath".into(), ext_path);
194
195 let all_params = path_params
196 .iter()
197 .chain(
198 query_params
199 .iter()
200 .sorted_by(|a, b| b.required().cmp(&a.required())),
201 )
202 .chain(&body_param)
203 .cloned()
204 .collect::<Vec<_>>();
205 let return_model = match operation
207 .responses
208 .responses
209 .get(&openapiv3::StatusCode::Code(200))
210 .or(operation
211 .responses
212 .responses
213 .get(&openapiv3::StatusCode::Code(204)))
214 {
215 Some(ref_or) => root.resolve_reference_or_resp("application/json", ref_or),
216 None => todo!(),
217 };
218 let return_model = return_model.post_process_data_type();
220 let (class, class_file) = match operation.tags.first() {
221 Some(class) => (class.clone(), format!("{class}_api").to_snake_case()),
222 None => (String::new(), String::new()),
224 };
225 Self {
226 description: operation.description.as_ref().map(|d| d.replace('\n', " ")),
227 classname: class,
228 class_filename: class_file,
229 summary: operation.summary.clone(),
230 tags: operation.tags.clone(),
231 is_deprecated: Some(operation.deprecated),
232 operation_id_lower_case: operation.operation_id.as_ref().map(|o| o.to_lowercase()),
233 operation_id_camel_case: operation
234 .operation_id
235 .as_ref()
236 .map(|o| o.to_lower_camel_case()),
237 operation_id: operation.operation_id.clone(),
238 operation_id_original: operation.operation_id.clone(),
239 has_params: !all_params.is_empty(),
240 all_params,
241 has_path_params: !path_params.is_empty(),
242 path_params,
243 has_query_params: !query_params.is_empty(),
244 query_params,
245 header_params: vec![],
246 has_header_params: false,
247 has_body_param: body_param.is_some(),
248 body_param,
249 path: path.to_string(),
250 http_method: method.to_upper_camel_case(),
251 support_multiple_responses: false,
252 return_type: {
253 let data_type = return_model.data_type();
254 if data_type == "()" {
255 None
256 } else {
257 Some(data_type)
258 }
259 },
260 has_auth_methods: operation.security.is_some(),
261 auth_methods: match &operation.security {
262 None => vec![],
263 Some(sec) => sec
264 .iter()
265 .flat_map(|a| {
266 a.iter()
267 .map(|(key, _)| match key.as_str() {
268 "JWT" => AuthMethod {
269 scheme: "JWT".to_string(),
270 is_basic: true,
271 is_basic_bearer: true,
272 },
273 scheme => AuthMethod {
274 scheme: scheme.to_string(),
275 is_basic: false,
276 is_basic_bearer: false,
277 },
278 })
279 .collect::<Vec<_>>()
280 })
281 .collect::<Vec<_>>(),
282 },
283 vendor_extensions,
284 api_doc_path: "docs/apis/",
285 model_doc_path: "docs/models/",
286 ..Default::default()
287 }
288 }
289 pub fn tags(&self) -> &Vec<String> {
291 &self.tags
292 }
293 pub fn classname(&self) -> &str {
295 &self.classname
296 }
297 pub fn class_filename(&self) -> &str {
299 &self.class_filename
300 }
301}
302
303#[derive(Default, Content, Clone, Debug)]
304#[ramhorns(rename_all = "camelCase")]
305struct AuthMethod {
306 scheme: String,
307
308 is_basic: bool,
309 is_basic_bearer: bool,
310}