use std::{collections::BTreeMap, io};
use crate::ref_to_type_name;
use oas3::Spec;
use oas3::spec::{
ObjectOrReference, ObjectSchema, Operation, ParameterIn, PathItem, Schema, SchemaType,
SchemaTypeSet,
};
use serde_json::Value;
pub type TypeSchemas = BTreeMap<String, Value>;
#[derive(Debug, Clone)]
pub struct Endpoint {
pub method: String,
pub path: String,
pub operation_id: Option<String>,
pub summary: Option<String>,
pub description: Option<String>,
pub path_params: Vec<Parameter>,
pub query_params: Vec<Parameter>,
pub response_type: Option<String>,
pub deprecated: bool,
pub supports_csv: bool,
}
impl Endpoint {
pub fn should_generate(&self) -> bool {
self.method == "GET" && !self.deprecated
}
pub fn operation_name(&self) -> String {
if let Some(op_id) = &self.operation_id {
return op_id.clone();
}
let mut parts: Vec<String> = Vec::new();
let mut prev_segment = "";
for segment in self.path.split('/').filter(|s| !s.is_empty()) {
if segment == "api" {
continue;
}
if let Some(param) = segment.strip_prefix('{').and_then(|s| s.strip_suffix('}')) {
let prev_normalized = prev_segment.replace('-', "_");
if !prev_normalized.ends_with(param) {
parts.push(format!("by_{}", param));
}
} else {
let normalized = segment.replace('-', "_");
parts.push(normalized);
prev_segment = segment;
}
}
format!("get_{}", parts.join("_"))
}
}
#[derive(Debug, Clone)]
pub struct Parameter {
pub name: String,
pub required: bool,
pub param_type: String,
pub description: Option<String>,
}
pub fn parse_openapi_json(json: &str) -> io::Result<Spec> {
let mut value: Value =
serde_json::from_str(json).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
clean_for_oas3(&mut value);
let cleaned_json =
serde_json::to_string(&value).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
oas3::from_json(&cleaned_json).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
}
pub fn extract_schemas(json: &str) -> TypeSchemas {
let Ok(value) = serde_json::from_str::<Value>(json) else {
return BTreeMap::new();
};
value
.get("components")
.and_then(|c| c.get("schemas"))
.and_then(|s| s.as_object())
.map(|schemas| {
schemas
.iter()
.map(|(name, schema)| (name.clone(), schema.clone()))
.collect()
})
.unwrap_or_default()
}
fn clean_for_oas3(value: &mut Value) {
match value {
Value::Object(map) => {
if map.contains_key("$ref") {
map.retain(|k, _| k == "$ref" || k == "summary" || k == "description");
} else {
if let Some(schema) = map.get_mut("schema")
&& schema.is_boolean()
{
*schema = Value::Object(serde_json::Map::new());
}
for v in map.values_mut() {
clean_for_oas3(v);
}
}
}
Value::Array(arr) => {
for v in arr {
clean_for_oas3(v);
}
}
_ => {}
}
}
pub fn extract_endpoints(spec: &Spec) -> Vec<Endpoint> {
let mut endpoints = Vec::new();
let Some(paths) = &spec.paths else {
return endpoints;
};
for (path, path_item) in paths {
for (method, operation) in get_operations(path_item) {
if let Some(endpoint) = extract_endpoint(path, method, operation) {
endpoints.push(endpoint);
}
}
}
endpoints
}
fn get_operations(path_item: &PathItem) -> Vec<(&'static str, &Operation)> {
[
("GET", &path_item.get),
("POST", &path_item.post),
("PUT", &path_item.put),
("DELETE", &path_item.delete),
("PATCH", &path_item.patch),
]
.into_iter()
.filter_map(|(method, op)| op.as_ref().map(|o| (method, o)))
.collect()
}
fn extract_endpoint(path: &str, method: &str, operation: &Operation) -> Option<Endpoint> {
let path_params = extract_path_parameters(path, operation);
let query_params = extract_parameters(operation, ParameterIn::Query);
let response_type = extract_response_type(operation);
let supports_csv = check_csv_support(operation);
Some(Endpoint {
method: method.to_string(),
path: path.to_string(),
operation_id: operation.operation_id.clone(),
summary: operation.summary.clone(),
description: operation.description.clone(),
path_params,
query_params,
response_type,
deprecated: operation.deprecated.unwrap_or(false),
supports_csv,
})
}
fn check_csv_support(operation: &Operation) -> bool {
let Some(responses) = operation.responses.as_ref() else {
return false;
};
let Some(response) = responses.get("200") else {
return false;
};
match response {
ObjectOrReference::Object(response) => response.content.contains_key("text/csv"),
ObjectOrReference::Ref { .. } => false,
}
}
fn extract_path_parameters(path: &str, operation: &Operation) -> Vec<Parameter> {
let path_order: Vec<&str> = path
.split('/')
.filter_map(|segment| segment.strip_prefix('{').and_then(|s| s.strip_suffix('}')))
.collect();
let params = extract_parameters(operation, ParameterIn::Path);
let mut sorted_params: Vec<Parameter> = params;
sorted_params.sort_by_key(|p| {
path_order
.iter()
.position(|&name| name == p.name)
.unwrap_or(usize::MAX)
});
sorted_params
}
fn extract_parameters(operation: &Operation, location: ParameterIn) -> Vec<Parameter> {
operation
.parameters
.iter()
.filter_map(|p| match p {
ObjectOrReference::Object(param) if param.location == location => {
let param_type = param
.schema
.as_ref()
.and_then(|s| match s {
ObjectOrReference::Ref { ref_path, .. } => {
ref_to_type_name(ref_path).map(|s| s.to_string())
}
ObjectOrReference::Object(obj_schema) => schema_to_type_name(obj_schema),
})
.unwrap_or_else(|| "string".to_string());
Some(Parameter {
name: param.name.clone(),
required: param.required.unwrap_or(false),
param_type,
description: param.description.clone(),
})
}
_ => None,
})
.collect()
}
fn extract_response_type(operation: &Operation) -> Option<String> {
let responses = operation.responses.as_ref()?;
let response = responses.get("200")?;
match response {
ObjectOrReference::Object(response) => {
let content = response.content.get("application/json")?;
match &content.schema {
Some(ObjectOrReference::Ref { ref_path, .. }) => {
Some(ref_to_type_name(ref_path)?.to_string())
}
Some(ObjectOrReference::Object(schema)) => schema_to_type_name(schema),
None => None,
}
}
ObjectOrReference::Ref { .. } => None,
}
}
fn schema_type_from_schema(schema: &Schema) -> Option<String> {
match schema {
Schema::Boolean(_) => Some("boolean".to_string()),
Schema::Object(obj_or_ref) => match obj_or_ref.as_ref() {
ObjectOrReference::Object(obj_schema) => schema_to_type_name(obj_schema),
ObjectOrReference::Ref { ref_path, .. } => {
ref_to_type_name(ref_path).map(|s| s.to_string())
}
},
}
}
fn schema_to_type_name(schema: &ObjectSchema) -> Option<String> {
if let Some(schema_type) = schema.schema_type.as_ref() {
return match schema_type {
SchemaTypeSet::Single(t) => single_type_to_name(t, schema),
SchemaTypeSet::Multiple(types) => {
types
.iter()
.find(|t| !matches!(t, SchemaType::Null))
.and_then(|t| single_type_to_name(t, schema))
.or(Some("*".to_string()))
}
};
}
let variants = if !schema.any_of.is_empty() {
&schema.any_of
} else if !schema.one_of.is_empty() {
&schema.one_of
} else {
return None;
};
let types: Vec<String> = variants
.iter()
.filter_map(|v| match v {
ObjectOrReference::Ref { ref_path, .. } => {
ref_to_type_name(ref_path).map(|s| s.to_string())
}
ObjectOrReference::Object(obj) => {
if matches!(
obj.schema_type.as_ref(),
Some(SchemaTypeSet::Single(SchemaType::Null))
) {
return None;
}
schema_to_type_name(obj)
}
})
.collect();
match types.len() {
0 => None,
1 => Some(types.into_iter().next().unwrap()),
_ => Some(types.join(" | ")),
}
}
fn single_type_to_name(t: &SchemaType, schema: &ObjectSchema) -> Option<String> {
match t {
SchemaType::String => Some("string".to_string()),
SchemaType::Number => Some("number".to_string()),
SchemaType::Integer => Some("number".to_string()),
SchemaType::Boolean => Some("boolean".to_string()),
SchemaType::Array => {
let inner = match &schema.items {
Some(boxed_schema) => schema_type_from_schema(boxed_schema),
None => Some("*".to_string()),
};
inner.map(|t| format!("{}[]", t))
}
SchemaType::Object => Some("Object".to_string()),
SchemaType::Null => Some("null".to_string()),
}
}