1use std::{collections::BTreeMap, io};
2
3use crate::ref_to_type_name;
4use oas3::Spec;
5use oas3::spec::{
6 ObjectOrReference, ObjectSchema, Operation, ParameterIn, PathItem, Schema, SchemaType,
7 SchemaTypeSet,
8};
9use serde_json::Value;
10
11pub type TypeSchemas = BTreeMap<String, Value>;
13
14#[derive(Debug, Clone)]
16pub struct Endpoint {
17 pub method: String,
19 pub path: String,
21 pub operation_id: Option<String>,
23 pub summary: Option<String>,
25 pub description: Option<String>,
27 pub path_params: Vec<Parameter>,
29 pub query_params: Vec<Parameter>,
31 pub response_type: Option<String>,
33 pub deprecated: bool,
35 pub supports_csv: bool,
37}
38
39impl Endpoint {
40 pub fn should_generate(&self) -> bool {
43 self.method == "GET" && !self.deprecated
44 }
45
46 pub fn returns_json(&self) -> bool {
48 self.response_type.is_some()
49 }
50
51 pub fn operation_name(&self) -> String {
54 if let Some(op_id) = &self.operation_id {
55 return op_id.clone();
56 }
57 let mut parts: Vec<String> = Vec::new();
60 let mut prev_segment = "";
61
62 for segment in self.path.split('/').filter(|s| !s.is_empty()) {
63 if segment == "api" {
64 continue;
65 }
66 if let Some(param) = segment.strip_prefix('{').and_then(|s| s.strip_suffix('}')) {
67 let prev_normalized = prev_segment.replace('-', "_");
69 if !prev_normalized.ends_with(param) {
70 parts.push(format!("by_{}", param));
71 }
72 } else {
73 let normalized = segment.replace('-', "_");
74 parts.push(normalized);
75 prev_segment = segment;
76 }
77 }
78 format!("get_{}", parts.join("_"))
79 }
80}
81
82#[derive(Debug, Clone)]
84pub struct Parameter {
85 pub name: String,
86 pub required: bool,
87 pub param_type: String,
88 pub description: Option<String>,
89}
90
91pub fn parse_openapi_json(json: &str) -> io::Result<Spec> {
96 let mut value: Value =
97 serde_json::from_str(json).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
98
99 clean_for_oas3(&mut value);
101
102 let cleaned_json =
103 serde_json::to_string(&value).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
104
105 oas3::from_json(&cleaned_json).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
106}
107
108pub fn extract_schemas(json: &str) -> TypeSchemas {
110 let Ok(value) = serde_json::from_str::<Value>(json) else {
111 return BTreeMap::new();
112 };
113
114 value
115 .get("components")
116 .and_then(|c| c.get("schemas"))
117 .and_then(|s| s.as_object())
118 .map(|schemas| {
119 schemas
120 .iter()
121 .map(|(name, schema)| (name.clone(), schema.clone()))
122 .collect()
123 })
124 .unwrap_or_default()
125}
126
127fn clean_for_oas3(value: &mut Value) {
131 match value {
132 Value::Object(map) => {
133 if map.contains_key("$ref") {
135 map.retain(|k, _| k == "$ref" || k == "summary" || k == "description");
136 } else {
137 if let Some(schema) = map.get_mut("schema")
139 && schema.is_boolean()
140 {
141 *schema = Value::Object(serde_json::Map::new());
142 }
143 for v in map.values_mut() {
144 clean_for_oas3(v);
145 }
146 }
147 }
148 Value::Array(arr) => {
149 for v in arr {
150 clean_for_oas3(v);
151 }
152 }
153 _ => {}
154 }
155}
156
157pub fn extract_endpoints(spec: &Spec) -> Vec<Endpoint> {
159 let mut endpoints = Vec::new();
160
161 let Some(paths) = &spec.paths else {
162 return endpoints;
163 };
164
165 for (path, path_item) in paths {
166 for (method, operation) in get_operations(path_item) {
167 if let Some(endpoint) = extract_endpoint(path, method, operation) {
168 endpoints.push(endpoint);
169 }
170 }
171 }
172
173 endpoints
174}
175
176fn get_operations(path_item: &PathItem) -> Vec<(&'static str, &Operation)> {
177 [
178 ("GET", &path_item.get),
179 ("POST", &path_item.post),
180 ("PUT", &path_item.put),
181 ("DELETE", &path_item.delete),
182 ("PATCH", &path_item.patch),
183 ]
184 .into_iter()
185 .filter_map(|(method, op)| op.as_ref().map(|o| (method, o)))
186 .collect()
187}
188
189fn extract_endpoint(path: &str, method: &str, operation: &Operation) -> Option<Endpoint> {
190 let path_params = extract_path_parameters(path, operation);
191 let query_params = extract_parameters(operation, ParameterIn::Query);
192
193 let response_type = extract_response_type(operation);
194 let supports_csv = check_csv_support(operation);
195
196 Some(Endpoint {
197 method: method.to_string(),
198 path: path.to_string(),
199 operation_id: operation.operation_id.clone(),
200 summary: operation.summary.clone(),
201 description: operation.description.clone(),
202 path_params,
203 query_params,
204 response_type,
205 deprecated: operation.deprecated.unwrap_or(false),
206 supports_csv,
207 })
208}
209
210fn check_csv_support(operation: &Operation) -> bool {
212 let Some(responses) = operation.responses.as_ref() else {
213 return false;
214 };
215 let Some(response) = responses.get("200") else {
216 return false;
217 };
218 match response {
219 ObjectOrReference::Object(response) => response.content.contains_key("text/csv"),
220 ObjectOrReference::Ref { .. } => false,
221 }
222}
223
224fn extract_path_parameters(path: &str, operation: &Operation) -> Vec<Parameter> {
226 let path_order: Vec<&str> = path
228 .split('/')
229 .filter_map(|segment| segment.strip_prefix('{').and_then(|s| s.strip_suffix('}')))
230 .collect();
231
232 let params = extract_parameters(operation, ParameterIn::Path);
234
235 let mut sorted_params: Vec<Parameter> = params;
237 sorted_params.sort_by_key(|p| {
238 path_order
239 .iter()
240 .position(|&name| name == p.name)
241 .unwrap_or(usize::MAX)
242 });
243
244 sorted_params
245}
246
247fn extract_parameters(operation: &Operation, location: ParameterIn) -> Vec<Parameter> {
248 operation
249 .parameters
250 .iter()
251 .filter_map(|p| match p {
252 ObjectOrReference::Object(param) if param.location == location => {
253 let param_type = param
254 .schema
255 .as_ref()
256 .and_then(|s| match s {
257 ObjectOrReference::Ref { ref_path, .. } => {
258 ref_to_type_name(ref_path).map(|s| s.to_string())
259 }
260 ObjectOrReference::Object(obj_schema) => schema_to_type_name(obj_schema),
261 })
262 .unwrap_or_else(|| "string".to_string());
263 Some(Parameter {
264 name: param.name.clone(),
265 required: param.required.unwrap_or(false),
266 param_type,
267 description: param.description.clone(),
268 })
269 }
270 _ => None,
271 })
272 .collect()
273}
274
275fn extract_response_type(operation: &Operation) -> Option<String> {
276 let responses = operation.responses.as_ref()?;
277
278 let response = responses.get("200")?;
280
281 match response {
282 ObjectOrReference::Object(response) => {
283 let content = response.content.get("application/json")?;
285
286 match &content.schema {
287 Some(ObjectOrReference::Ref { ref_path, .. }) => {
288 Some(ref_to_type_name(ref_path)?.to_string())
290 }
291 Some(ObjectOrReference::Object(schema)) => schema_to_type_name(schema),
292 None => None,
293 }
294 }
295 ObjectOrReference::Ref { .. } => None,
296 }
297}
298
299fn schema_type_from_schema(schema: &Schema) -> Option<String> {
300 match schema {
301 Schema::Boolean(_) => Some("boolean".to_string()),
302 Schema::Object(obj_or_ref) => match obj_or_ref.as_ref() {
303 ObjectOrReference::Object(obj_schema) => schema_to_type_name(obj_schema),
304 ObjectOrReference::Ref { ref_path, .. } => {
305 ref_to_type_name(ref_path).map(|s| s.to_string())
308 }
309 },
310 }
311}
312
313fn schema_to_type_name(schema: &ObjectSchema) -> Option<String> {
314 if let Some(schema_type) = schema.schema_type.as_ref() {
315 return match schema_type {
316 SchemaTypeSet::Single(t) => single_type_to_name(t, schema),
317 SchemaTypeSet::Multiple(types) => {
318 types
320 .iter()
321 .find(|t| !matches!(t, SchemaType::Null))
322 .and_then(|t| single_type_to_name(t, schema))
323 .or(Some("*".to_string()))
324 }
325 };
326 }
327
328 let variants = if !schema.any_of.is_empty() {
330 &schema.any_of
331 } else if !schema.one_of.is_empty() {
332 &schema.one_of
333 } else {
334 return None;
335 };
336
337 let types: Vec<String> = variants
338 .iter()
339 .filter_map(|v| match v {
340 ObjectOrReference::Ref { ref_path, .. } => {
341 ref_to_type_name(ref_path).map(|s| s.to_string())
342 }
343 ObjectOrReference::Object(obj) => {
344 if matches!(
346 obj.schema_type.as_ref(),
347 Some(SchemaTypeSet::Single(SchemaType::Null))
348 ) {
349 return None;
350 }
351 schema_to_type_name(obj)
352 }
353 })
354 .collect();
355
356 match types.len() {
357 0 => None,
358 1 => Some(types.into_iter().next().unwrap()),
359 _ => Some(types.join(" | ")),
360 }
361}
362
363fn single_type_to_name(t: &SchemaType, schema: &ObjectSchema) -> Option<String> {
364 match t {
365 SchemaType::String => Some("string".to_string()),
366 SchemaType::Number => Some("number".to_string()),
367 SchemaType::Integer => Some("number".to_string()),
368 SchemaType::Boolean => Some("boolean".to_string()),
369 SchemaType::Array => {
370 let inner = match &schema.items {
371 Some(boxed_schema) => schema_type_from_schema(boxed_schema),
372 None => Some("*".to_string()),
373 };
374 inner.map(|t| format!("{}[]", t))
375 }
376 SchemaType::Object => Some("Object".to_string()),
377 SchemaType::Null => Some("null".to_string()),
378 }
379}