use crate::codegen::SchemaRegistry;
use anyhow::Result;
use openapiv3::{OpenAPI, Operation, ReferenceOr, Schema};
pub trait OpenApiGenerator {
fn spec(&self) -> &OpenAPI;
fn registry(&self) -> &SchemaRegistry;
fn generate_header(&self) -> String;
fn generate_models(&self) -> Result<String>;
fn generate_routes(&self) -> Result<String>;
fn generate_footer(&self) -> String {
String::new()
}
fn generate(&self) -> Result<String> {
let mut output = String::new();
output.push_str(&self.generate_header());
output.push_str(&self.generate_models()?);
output.push_str(&self.generate_routes()?);
let footer = self.generate_footer();
if !footer.is_empty() {
output.push_str(&footer);
}
Ok(output)
}
fn iter_paths<F>(&self, mut f: F) -> Result<()>
where
F: FnMut(&str, &str, &Operation) -> Result<()>,
{
for (path, path_item_ref) in &self.spec().paths.paths {
let path_item = match path_item_ref {
ReferenceOr::Item(item) => item,
ReferenceOr::Reference { .. } => continue,
};
if let Some(op) = &path_item.get {
f(path, "get", op)?;
}
if let Some(op) = &path_item.post {
f(path, "post", op)?;
}
if let Some(op) = &path_item.put {
f(path, "put", op)?;
}
if let Some(op) = &path_item.delete {
f(path, "delete", op)?;
}
if let Some(op) = &path_item.patch {
f(path, "patch", op)?;
}
}
Ok(())
}
fn iter_schemas<F>(&self, mut f: F) -> Result<()>
where
F: FnMut(&str, &Schema) -> Result<()>,
{
if let Some(components) = &self.spec().components {
for (name, schema_ref) in &components.schemas {
match schema_ref {
ReferenceOr::Item(schema) => {
f(name, schema)?;
}
ReferenceOr::Reference { .. } => continue,
}
}
}
Ok(())
}
fn extract_request_body_type(&self, operation: &Operation) -> Option<String> {
operation.request_body.as_ref().and_then(|body_ref| match body_ref {
ReferenceOr::Item(request_body) => request_body.content.get("application/json").and_then(|media_type| {
media_type
.schema
.as_ref()
.map(|schema_ref| self.extract_type_from_schema_ref(schema_ref))
}),
ReferenceOr::Reference { reference } => {
let ref_name = reference.split('/').next_back().unwrap();
Some(self.format_type_name(ref_name))
}
})
}
fn extract_response_type(&self, operation: &Operation) -> String {
use openapiv3::StatusCode;
let response = operation
.responses
.responses
.get(&StatusCode::Code(200))
.or_else(|| operation.responses.responses.get(&StatusCode::Code(201)))
.or_else(|| operation.responses.responses.get(&StatusCode::Range(2)));
if let Some(response_ref) = response {
match response_ref {
ReferenceOr::Item(response) => {
if let Some(content) = response.content.get("application/json")
&& let Some(schema_ref) = &content.schema
{
return self.extract_type_from_schema_ref(schema_ref);
}
}
ReferenceOr::Reference { reference } => {
let ref_name = reference.split('/').next_back().unwrap();
return self.format_type_name(ref_name);
}
}
}
self.default_response_type()
}
fn extract_type_from_schema_ref(&self, schema_ref: &ReferenceOr<Schema>) -> String {
match schema_ref {
ReferenceOr::Reference { reference } => {
let ref_name = reference.split('/').next_back().unwrap();
self.format_type_name(ref_name)
}
ReferenceOr::Item(_schema) => self.default_response_type(),
}
}
fn format_type_name(&self, name: &str) -> String {
heck::ToPascalCase::to_pascal_case(name)
}
fn default_response_type(&self) -> String {
"unknown".to_string()
}
fn generate_operation_id(&self, path: &str, method: &str, operation: &Operation) -> String {
operation
.operation_id
.as_ref()
.map(|id| heck::ToSnakeCase::to_snake_case(id.as_str()))
.unwrap_or_else(|| {
format!(
"{}_{}",
method,
path.replace('/', "_").replace(['{', '}'], "").trim_matches('_')
)
})
}
}