use std::sync::Arc;
pub use serde;
pub use axum;
pub use inventory;
pub use tracing;
pub use utoipa;
pub use cinderblock_json_api_macros::__resource_extension;
pub trait FieldSchema {
fn field_schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>;
}
#[macro_export]
macro_rules! impl_field_schema {
($ty:ty) => {
impl $crate::FieldSchema for $ty {
fn field_schema(
) -> $crate::utoipa::openapi::RefOr<$crate::utoipa::openapi::schema::Schema> {
<$ty as $crate::utoipa::PartialSchema>::schema()
}
}
};
}
macro_rules! impl_field_schema_string {
($($ty:ty),*) => {
$(
impl FieldSchema for $ty {
fn field_schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
use utoipa::openapi::schema::{ObjectBuilder, SchemaType, Type};
ObjectBuilder::new()
.schema_type(SchemaType::new(Type::String))
.into()
}
}
)*
};
}
macro_rules! impl_field_schema_integer {
($($ty:ty => $format:expr),*) => {
$(
impl FieldSchema for $ty {
fn field_schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
use utoipa::openapi::schema::{ObjectBuilder, SchemaType, SchemaFormat, Type};
ObjectBuilder::new()
.schema_type(SchemaType::new(Type::Integer))
.format(Some(SchemaFormat::KnownFormat($format)))
.into()
}
}
)*
};
}
macro_rules! impl_field_schema_number {
($($ty:ty => $format:expr),*) => {
$(
impl FieldSchema for $ty {
fn field_schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
use utoipa::openapi::schema::{ObjectBuilder, SchemaType, SchemaFormat, Type};
ObjectBuilder::new()
.schema_type(SchemaType::new(Type::Number))
.format(Some(SchemaFormat::KnownFormat($format)))
.into()
}
}
)*
};
}
impl_field_schema_string!(String);
impl FieldSchema for bool {
fn field_schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
use utoipa::openapi::schema::{ObjectBuilder, SchemaType, Type};
ObjectBuilder::new()
.schema_type(SchemaType::new(Type::Boolean))
.into()
}
}
impl_field_schema_integer!(
i8 => utoipa::openapi::KnownFormat::Int32,
i16 => utoipa::openapi::KnownFormat::Int32,
i32 => utoipa::openapi::KnownFormat::Int32,
i64 => utoipa::openapi::KnownFormat::Int64,
u8 => utoipa::openapi::KnownFormat::Int32,
u16 => utoipa::openapi::KnownFormat::Int32,
u32 => utoipa::openapi::KnownFormat::Int32,
u64 => utoipa::openapi::KnownFormat::Int64,
isize => utoipa::openapi::KnownFormat::Int64,
usize => utoipa::openapi::KnownFormat::Int64
);
impl_field_schema_number!(
f32 => utoipa::openapi::KnownFormat::Float,
f64 => utoipa::openapi::KnownFormat::Double
);
impl FieldSchema for uuid::Uuid {
fn field_schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
use utoipa::openapi::schema::{ObjectBuilder, SchemaFormat, SchemaType, Type};
ObjectBuilder::new()
.schema_type(SchemaType::new(Type::String))
.format(Some(SchemaFormat::KnownFormat(
utoipa::openapi::KnownFormat::Uuid,
)))
.into()
}
}
#[derive(Debug, serde::Serialize)]
pub struct Response<T: serde::Serialize> {
pub data: T,
}
#[derive(Debug, serde::Serialize)]
pub struct PaginatedResponse<T: serde::Serialize> {
pub data: Vec<T>,
pub meta: PaginationMeta,
}
#[derive(Debug, serde::Serialize)]
pub struct PaginationMeta {
pub page: u32,
pub per_page: u32,
pub total: u64,
pub total_pages: u32,
}
impl<T> utoipa::PartialSchema for Response<T>
where
T: serde::Serialize + utoipa::PartialSchema,
{
fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
use utoipa::openapi::schema::{ObjectBuilder, SchemaType, Type};
ObjectBuilder::new()
.schema_type(SchemaType::new(Type::Object))
.property("data", T::schema())
.required("data")
.into()
}
}
impl<T> utoipa::ToSchema for Response<T>
where
T: serde::Serialize + utoipa::PartialSchema,
{
fn name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("Response")
}
}
impl<T> utoipa::PartialSchema for PaginatedResponse<T>
where
T: serde::Serialize + utoipa::PartialSchema,
{
fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
use utoipa::openapi::schema::{
ArrayBuilder, ObjectBuilder, SchemaFormat, SchemaType, Type,
};
let meta_schema = ObjectBuilder::new()
.schema_type(SchemaType::new(Type::Object))
.property(
"page",
ObjectBuilder::new()
.schema_type(SchemaType::new(Type::Integer))
.format(Some(SchemaFormat::KnownFormat(
utoipa::openapi::KnownFormat::Int32,
))),
)
.required("page")
.property(
"per_page",
ObjectBuilder::new()
.schema_type(SchemaType::new(Type::Integer))
.format(Some(SchemaFormat::KnownFormat(
utoipa::openapi::KnownFormat::Int32,
))),
)
.required("per_page")
.property(
"total",
ObjectBuilder::new()
.schema_type(SchemaType::new(Type::Integer))
.format(Some(SchemaFormat::KnownFormat(
utoipa::openapi::KnownFormat::Int64,
))),
)
.required("total")
.property(
"total_pages",
ObjectBuilder::new()
.schema_type(SchemaType::new(Type::Integer))
.format(Some(SchemaFormat::KnownFormat(
utoipa::openapi::KnownFormat::Int32,
))),
)
.required("total_pages");
ObjectBuilder::new()
.schema_type(SchemaType::new(Type::Object))
.property("data", ArrayBuilder::new().items(T::schema()))
.required("data")
.property("meta", meta_schema)
.required("meta")
.into()
}
}
impl<T> utoipa::ToSchema for PaginatedResponse<T>
where
T: serde::Serialize + utoipa::PartialSchema,
{
fn name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("PaginatedResponse")
}
}
pub struct ResourceEndpoint {
pub register: fn(axum::Router, Arc<cinderblock_core::Context>) -> axum::Router,
pub openapi: Option<fn() -> utoipa::openapi::OpenApi>,
}
inventory::collect!(ResourceEndpoint);
pub struct RouterConfig {
ctx: Arc<cinderblock_core::Context>,
swagger_ui: bool,
}
impl RouterConfig {
pub fn new(ctx: impl Into<Arc<cinderblock_core::Context>>) -> Self {
Self {
ctx: ctx.into(),
swagger_ui: true,
}
}
pub fn swagger_ui(mut self, enabled: bool) -> Self {
self.swagger_ui = enabled;
self
}
pub fn build(self) -> axum::Router {
let mut router = axum::Router::new();
let mut openapi_specs: Vec<utoipa::openapi::OpenApi> = Vec::new();
for endpoint in inventory::iter::<ResourceEndpoint> {
router = (endpoint.register)(router, self.ctx.clone());
if let Some(openapi_fn) = endpoint.openapi {
openapi_specs.push(openapi_fn());
}
}
if !openapi_specs.is_empty() {
let mut merged = utoipa::openapi::OpenApiBuilder::new()
.info(
utoipa::openapi::InfoBuilder::new()
.title("Cinderblock JSON API")
.version("0.1.0")
.build(),
)
.build();
for spec in openapi_specs {
merged.merge(spec);
}
#[cfg(feature = "swagger-ui")]
if self.swagger_ui {
router = router.merge(
utoipa_swagger_ui::SwaggerUi::new("/swagger-ui").url("/openapi.json", merged),
);
}
#[cfg(not(feature = "swagger-ui"))]
let _ = self.swagger_ui;
}
router
}
}
pub fn router(ctx: impl Into<Arc<cinderblock_core::Context>>) -> axum::Router {
RouterConfig::new(ctx).build()
}