use utoipa::openapi::{OpenApi, OpenApiBuilder, InfoBuilder, ComponentsBuilder};
use utoipa::openapi::{PathsBuilder, PathItemType};
use utoipa::openapi::path::{PathItemBuilder, OperationBuilder, ParameterBuilder, ParameterIn};
use utoipa::openapi::{Required, Schema, Object, SchemaType, RefOr, Ref};
use utoipa::openapi::request_body::RequestBodyBuilder;
use utoipa::openapi::response::{ResponseBuilder, ResponsesBuilder};
use utoipa::openapi::content::ContentBuilder;
use std::collections::{HashMap, BTreeMap};
pub struct PathOperation {
pub path: &'static str,
pub method: &'static str,
pub operation_id: &'static str,
pub summary: &'static str,
pub request_schema: Option<&'static str>,
}
pub struct SchemaInfo {
pub name: &'static str,
}
inventory::collect!(PathOperation);
inventory::collect!(SchemaInfo);
pub fn generate_openapi() -> OpenApi {
let mut openapi = OpenApiBuilder::new()
.info(InfoBuilder::new()
.title("Bloom")
.version("1.0.0")
.build())
.build();
let mut paths_map = HashMap::new();
for path_op in inventory::iter::<PathOperation> {
let path_params = extract_path_parameters(path_op.path);
let mut parameters = vec![];
for param_name in path_params {
parameters.push(
ParameterBuilder::new()
.name(param_name)
.parameter_in(ParameterIn::Path)
.required(Required::True)
.schema(Some(RefOr::T(
Schema::Object(
Object::with_type(SchemaType::String)
)
)))
.build()
);
}
let mut operation = OperationBuilder::new()
.operation_id(Some(path_op.operation_id.to_string()))
.summary(Some(path_op.summary.to_string()));
if !parameters.is_empty() {
operation = operation.parameters(Some(parameters));
}
if matches!(path_op.method, "post" | "put" | "patch") {
let schema = if let Some(schema_name) = path_op.request_schema {
RefOr::Ref(Ref::from_schema_name(schema_name))
} else {
RefOr::T(Schema::Object(Object::with_type(SchemaType::Object)))
};
let request_body = RequestBodyBuilder::new()
.content("application/json", ContentBuilder::new()
.schema(schema)
.build())
.build();
operation = operation.request_body(Some(request_body));
}
let (status_code, description) = match path_op.method {
"get" => ("200", "Successfully retrieved data"),
"post" => ("201", "Successfully created resource"),
"put" => ("200", "Successfully updated resource"),
"delete" => ("204", "Successfully deleted resource"),
"patch" => ("200", "Successfully patched resource"),
_ => ("200", "Success"),
};
let mut response_builder = ResponseBuilder::new()
.description(description);
if path_op.method != "delete" {
let content = ContentBuilder::new()
.schema(RefOr::T(Schema::Object(Object::with_type(SchemaType::Object))))
.build();
response_builder = response_builder.content("application/json", content);
}
let responses = ResponsesBuilder::new()
.response(status_code, response_builder.build())
.build();
operation = operation.responses(responses);
let path_item = paths_map.entry(path_op.path.to_string())
.or_insert_with(|| PathItemBuilder::new().build());
match path_op.method {
"get" => path_item.operations.insert(PathItemType::Get, operation.build()),
"post" => path_item.operations.insert(PathItemType::Post, operation.build()),
"put" => path_item.operations.insert(PathItemType::Put, operation.build()),
"delete" => path_item.operations.insert(PathItemType::Delete, operation.build()),
"patch" => path_item.operations.insert(PathItemType::Patch, operation.build()),
_ => None,
};
}
let mut paths_builder = PathsBuilder::new();
for (path_str, path_item) in paths_map {
paths_builder = paths_builder.path(path_str, path_item);
}
openapi.paths = paths_builder.build();
let mut schemas = HashMap::new();
for schema_info in inventory::iter::<SchemaInfo> {
let schema = generate_schema_for_type(schema_info.name);
schemas.insert(schema_info.name.to_string(), RefOr::T(schema));
}
if !schemas.is_empty() {
openapi.components = Some(
ComponentsBuilder::new()
.schemas_from_iter(schemas)
.build()
);
}
openapi
}
fn generate_schema_for_type(type_name: &str) -> Schema {
match type_name {
"CreateUser" => {
let mut properties = BTreeMap::new();
properties.insert("username".to_string(), RefOr::T(
Schema::Object(Object::with_type(SchemaType::String))
));
properties.insert("email".to_string(), RefOr::T(
Schema::Object(Object::with_type(SchemaType::String))
));
let mut object = Object::with_type(SchemaType::Object);
object.properties = properties;
object.required = vec!["username".to_string(), "email".to_string()];
Schema::Object(object)
},
"UpdateUser" => {
let mut properties = BTreeMap::new();
properties.insert("username".to_string(), RefOr::T(
Schema::Object(Object::with_type(SchemaType::String))
));
properties.insert("email".to_string(), RefOr::T(
Schema::Object(Object::with_type(SchemaType::String))
));
let mut object = Object::with_type(SchemaType::Object);
object.properties = properties;
object.required = vec!["username".to_string(), "email".to_string()];
Schema::Object(object)
},
"UserDto" => {
let mut properties = BTreeMap::new();
properties.insert("id".to_string(), RefOr::T(
Schema::Object(Object::with_type(SchemaType::Integer))
));
properties.insert("username".to_string(), RefOr::T(
Schema::Object(Object::with_type(SchemaType::String))
));
properties.insert("email".to_string(), RefOr::T(
Schema::Object(Object::with_type(SchemaType::String))
));
let mut object = Object::with_type(SchemaType::Object);
object.properties = properties;
object.required = vec!["id".to_string(), "username".to_string(), "email".to_string()];
Schema::Object(object)
},
_ => {
Schema::Object(Object::with_type(SchemaType::Object))
}
}
}
fn extract_path_parameters(path: &str) -> Vec<String> {
let mut params = Vec::new();
let mut chars = path.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '{' {
let mut param_name = String::new();
while let Some(ch) = chars.peek() {
if *ch == '}' {
chars.next();
break;
}
param_name.push(chars.next().unwrap());
}
if !param_name.is_empty() {
params.push(param_name);
}
}
}
params
}