Skip to main content

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;
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/// 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: 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<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, // TODO: Resolve references
52                }
53            })
54            .collect()
55    }
56}
57
58impl OperationInfo {
59    /// Convert to ApiMethodData with optional Components for schema resolution
60    pub fn to_api_method_data(&self, _components: Option<&Components>) -> ApiMethodData {
61        let method_name = self.method_name();
62
63        // Extract parameters
64        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                    // TODO: Resolve parameter references
73                    continue;
74                }
75            };
76
77            // Extract schema from parameter
78            let schema = param.schema.clone();
79
80            let required = param.required.unwrap_or(false);
81            let deprecated = param.deprecated.unwrap_or(false);
82
83            // Extract default value from schema
84            let default_value = None; // TODO: Extract from schema
85
86            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), // Treat cookie as header
101            }
102        }
103
104        // Extract return type from responses
105        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                        // TODO: Resolve response references
141                        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
160/// Extract return type from operation responses
161fn 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}