openapi_nexus_core/data/
operation_info.rs

1//! Operation information for grouping by tag
2
3use 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/// Operation information for grouping by tag
19#[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            // Generate from path and HTTP method
33            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    /// Convert to ApiMethodData with optional Components for schema resolution
55    pub fn to_api_method_data(&self, components: Option<&openapi::Components>) -> ApiMethodData {
56        let method_name = self.method_name();
57
58        // Extract parameters
59        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                // Extract schema from parameter
66                let schema = param.schema.clone();
67
68                let required = param.required();
69                let deprecated = param.deprecated();
70
71                // Extract default value from schema
72                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), // Treat cookie as header
89                }
90            }
91        }
92
93        // Extract return type from responses
94        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
148/// Extract return type from operation responses
149fn 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}