paperclip_ng/v3/
operation.rs

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    // if 4xx, 5xx responses have at least one error object defined
38    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    /// Create an Operation based on the deserialized openapi operation.
133    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                    // todo: need to handle this
159                    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                    // todo: need to handle this
170                    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                // todo: need to handle this
179                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        // todo: support multiple responses
206        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        // todo: should we post process after all operations are processed?
219        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            // How should this be handled? Shuld it be required? What if there's more than 1 tag?
223            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    /// Get a reference to the operation tags list.
290    pub fn tags(&self) -> &Vec<String> {
291        &self.tags
292    }
293    /// Get a reference to the operation class name.
294    pub fn classname(&self) -> &str {
295        &self.classname
296    }
297    /// Get a reference to the operation class filename.
298    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}