use std::collections::HashMap;
use indexmap::IndexMap;
use crate::config::NamingStrategy;
use crate::error::TransformError;
use crate::ir::*;
use crate::parse::operation::{Operation, PathItem};
use crate::parse::parameter::{ParameterLocation, ParameterOrRef};
use crate::parse::ref_resolve::RefResolver;
use crate::parse::request_body::RequestBodyOrRef;
use crate::parse::spec::OpenApiSpec;
use super::name_normalizer::{normalize_name, route_to_name};
use super::promote_inline::promote_inline_objects;
use super::schema_resolver::{schema_or_ref_to_ir_schema, schema_or_ref_to_ir_type};
use super::sse_detector::detect_return_type;
#[derive(Debug, Clone, Default)]
pub struct TransformOptions {
pub naming_strategy: NamingStrategy,
pub aliases: IndexMap<String, String>,
}
pub fn transform(spec: &OpenApiSpec) -> Result<IrSpec, TransformError> {
transform_with_options(spec, &TransformOptions::default())
}
pub fn transform_with_options(
spec: &OpenApiSpec,
options: &TransformOptions,
) -> Result<IrSpec, TransformError> {
let mut resolver = RefResolver::new(spec);
let resolved = resolver.resolve_spec(spec)?;
let schemas = resolve_schemas(&resolved)?;
let operations = resolve_operations(&resolved, options)?;
let modules = group_into_modules(&operations);
let info = IrInfo {
title: resolved.info.title.clone(),
description: resolved.info.description.clone(),
version: resolved.info.version.clone(),
};
let servers = resolved
.servers
.iter()
.map(|s| IrServer {
url: s.url.clone(),
description: s.description.clone(),
})
.collect();
let mut ir = IrSpec {
info,
servers,
schemas,
operations,
modules,
};
promote_inline_objects(&mut ir);
Ok(ir)
}
fn resolve_schemas(spec: &OpenApiSpec) -> Result<Vec<IrSchema>, TransformError> {
let mut schemas = Vec::new();
if let Some(ref components) = spec.components {
for (name, schema_or_ref) in &components.schemas {
let ir_schema = schema_or_ref_to_ir_schema(name, schema_or_ref)?;
schemas.push(ir_schema);
}
}
Ok(schemas)
}
fn resolve_operations(
spec: &OpenApiSpec,
options: &TransformOptions,
) -> Result<Vec<IrOperation>, TransformError> {
let mut operations = Vec::new();
for (path, path_item) in &spec.paths {
let path_params = resolve_parameters(&path_item.parameters);
collect_operations(path, path_item, &path_params, options, &mut operations)?;
}
Ok(operations)
}
fn collect_operations(
path: &str,
item: &PathItem,
path_params: &[IrParameter],
options: &TransformOptions,
out: &mut Vec<IrOperation>,
) -> Result<(), TransformError> {
macro_rules! add_op {
($method:expr, $op:expr) => {
if let Some(ref op) = $op {
let ir_op = build_operation($method, path, op, path_params, options)?;
out.push(ir_op);
}
};
}
add_op!(HttpMethod::Get, item.get);
add_op!(HttpMethod::Post, item.post);
add_op!(HttpMethod::Put, item.put);
add_op!(HttpMethod::Delete, item.delete);
add_op!(HttpMethod::Patch, item.patch);
add_op!(HttpMethod::Options, item.options);
add_op!(HttpMethod::Head, item.head);
add_op!(HttpMethod::Trace, item.trace);
Ok(())
}
fn build_operation(
method: HttpMethod,
path: &str,
op: &Operation,
path_params: &[IrParameter],
options: &TransformOptions,
) -> Result<IrOperation, TransformError> {
let raw_name = match options.naming_strategy {
NamingStrategy::UseOperationId => {
op.operation_id.clone().unwrap_or_else(|| {
route_to_name(method.as_str(), path)
})
}
NamingStrategy::UseRouteBased => route_to_name(method.as_str(), path),
};
let name = if let Some(alias) = options.aliases.get(&raw_name) {
alias.clone()
} else {
raw_name.clone()
};
let mut parameters = path_params.to_vec();
parameters.extend(resolve_parameters(&op.parameters));
let request_body = op.request_body.as_ref().and_then(resolve_request_body);
let return_type = detect_return_type(&name, &op.responses);
Ok(IrOperation {
name: normalize_name(&name),
method,
path: path.to_string(),
summary: op.summary.clone(),
description: op.description.clone(),
tags: op.tags.clone(),
parameters,
request_body,
return_type,
deprecated: op.deprecated.unwrap_or(false),
})
}
fn resolve_parameters(params: &[ParameterOrRef]) -> Vec<IrParameter> {
params
.iter()
.filter_map(|p| match p {
ParameterOrRef::Parameter(param) => {
let location = match param.location {
ParameterLocation::Path => IrParameterLocation::Path,
ParameterLocation::Query => IrParameterLocation::Query,
ParameterLocation::Header => IrParameterLocation::Header,
ParameterLocation::Cookie => IrParameterLocation::Cookie,
};
let param_type = param
.schema
.as_ref()
.map(schema_or_ref_to_ir_type)
.unwrap_or(IrType::Any);
Some(IrParameter {
name: normalize_name(¶m.name),
original_name: param.name.clone(),
location,
param_type,
required: param.required,
description: param.description.clone(),
style: param.style.clone(),
explode: param.explode,
})
}
ParameterOrRef::Ref { .. } => None, })
.collect()
}
fn resolve_request_body(body: &RequestBodyOrRef) -> Option<IrRequestBody> {
match body {
RequestBodyOrRef::RequestBody(rb) => {
let (content_type, mt) = rb
.content
.get_key_value("application/json")
.or_else(|| rb.content.first())?;
let body_type = mt
.schema
.as_ref()
.map(schema_or_ref_to_ir_type)
.unwrap_or(IrType::Any);
let encoding = if mt.encoding.is_empty() {
None
} else {
Some(
mt.encoding
.iter()
.map(|(field_name, enc)| IrFieldEncoding {
field_name: field_name.clone(),
content_type: enc.content_type.clone(),
})
.collect(),
)
};
Some(IrRequestBody {
body_type,
required: rb.required,
content_type: content_type.clone(),
description: rb.description.clone(),
encoding,
})
}
RequestBodyOrRef::Ref { .. } => None, }
}
fn group_into_modules(operations: &[IrOperation]) -> Vec<IrModule> {
let mut tag_groups: HashMap<String, Vec<usize>> = HashMap::new();
for (i, op) in operations.iter().enumerate() {
if op.tags.is_empty() {
tag_groups.entry("default".to_string()).or_default().push(i);
} else {
for tag in &op.tags {
tag_groups.entry(tag.clone()).or_default().push(i);
}
}
}
let mut modules: Vec<IrModule> = tag_groups
.into_iter()
.map(|(name, ops)| IrModule {
name: normalize_name(&name),
operations: ops,
})
.collect();
modules.sort_by(|a, b| a.name.original.cmp(&b.name.original));
modules
}