use crate::api::api_dto;
use axum::{Router, handler::Handler, routing::MethodRouter};
use http::Method;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::marker::PhantomData;
use toolkit_canonical_errors::problem;
use toolkit_gts::gts_id;
#[must_use]
pub fn normalize_to_axum_path(path: &str) -> String {
path.to_owned()
}
#[must_use]
pub fn axum_to_openapi_path(path: &str) -> String {
path.replace("{*", "{")
}
pub const CORE_GLOBAL_BASE_LICENSE_FEATURE: &str =
gts_id!("cf.core.lic.feat.v1~cf.core.global.base.v1");
pub mod state {
#[derive(Debug, Clone, Copy)]
pub struct Missing;
#[derive(Debug, Clone, Copy)]
pub struct Present;
#[derive(Debug, Clone, Copy)]
pub struct AuthNotSet;
#[derive(Debug, Clone, Copy)]
pub struct AuthSet;
#[derive(Debug, Clone, Copy)]
pub struct LicenseNotSet;
#[derive(Debug, Clone, Copy)]
pub struct LicenseSet;
}
mod sealed {
pub trait Sealed {}
pub trait SealedAuth {}
pub trait SealedLicenseReq {}
}
pub trait HandlerSlot<S>: sealed::Sealed {
type Slot;
}
pub trait AuthState: sealed::SealedAuth {}
impl sealed::Sealed for Missing {}
impl sealed::Sealed for Present {}
impl sealed::SealedAuth for state::AuthNotSet {}
impl sealed::SealedAuth for state::AuthSet {}
impl AuthState for state::AuthNotSet {}
impl AuthState for state::AuthSet {}
pub trait LicenseState: sealed::SealedLicenseReq {}
impl sealed::SealedLicenseReq for state::LicenseNotSet {}
impl sealed::SealedLicenseReq for state::LicenseSet {}
impl LicenseState for state::LicenseNotSet {}
impl LicenseState for state::LicenseSet {}
impl<S> HandlerSlot<S> for Missing {
type Slot = ();
}
impl<S> HandlerSlot<S> for Present {
type Slot = MethodRouter<S>;
}
pub use state::{AuthNotSet, AuthSet, LicenseNotSet, LicenseSet, Missing, Present};
#[derive(Clone, Debug)]
pub struct ParamSpec {
pub name: String,
pub location: ParamLocation,
pub required: bool,
pub description: Option<String>,
pub param_type: String, pub array: bool,
}
impl ParamSpec {
fn scalar(
name: String,
location: ParamLocation,
required: bool,
description: Option<String>,
param_type: String,
) -> Self {
Self {
name,
location,
required,
description,
param_type,
array: false,
}
}
}
pub trait LicenseFeature: AsRef<str> {}
impl<T: LicenseFeature + ?Sized> LicenseFeature for &T {}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ParamLocation {
Path,
Query,
Header,
Cookie,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RequestBodySchema {
Ref { schema_name: String },
MultipartFile { field_name: String },
Binary,
InlineObject,
}
#[derive(Clone, Debug)]
pub struct RequestBodySpec {
pub content_type: &'static str,
pub description: Option<String>,
pub schema: RequestBodySchema,
pub required: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ResponseSchema {
Ref { schema_name: String },
Array { items_schema_name: String },
}
impl ResponseSchema {
#[must_use]
pub fn schema_name(&self) -> &str {
match self {
Self::Ref { schema_name } => schema_name,
Self::Array { items_schema_name } => items_schema_name,
}
}
}
#[derive(Clone, Debug)]
pub struct ResponseSpec {
pub status: u16,
pub content_type: &'static str,
pub description: String,
pub schema: Option<ResponseSchema>,
}
impl ResponseSpec {
#[must_use]
pub fn schema_name(&self) -> Option<&str> {
self.schema.as_ref().map(ResponseSchema::schema_name)
}
}
#[derive(Clone, Debug)]
pub struct LicenseReqSpec {
pub license_names: Vec<String>,
}
#[derive(Clone, Debug)]
pub struct OperationSpec {
pub method: Method,
pub path: String,
pub operation_id: Option<String>,
pub summary: Option<String>,
pub description: Option<String>,
pub tags: Vec<String>,
pub params: Vec<ParamSpec>,
pub request_body: Option<RequestBodySpec>,
pub responses: Vec<ResponseSpec>,
pub handler_id: String,
pub authenticated: bool,
pub exposed: bool,
pub rate_limit: Option<RateLimitSpec>,
pub allowed_request_content_types: Option<Vec<&'static str>>,
pub vendor_extensions: VendorExtensions,
pub license_requirement: Option<LicenseReqSpec>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct VendorExtensions {
#[serde(rename = "x-odata-filter", skip_serializing_if = "Option::is_none")]
pub x_odata_filter: Option<ODataPagination<BTreeMap<String, Vec<String>>>>,
#[serde(rename = "x-odata-orderby", skip_serializing_if = "Option::is_none")]
pub x_odata_orderby: Option<ODataPagination<Vec<String>>>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct ODataPagination<T> {
#[serde(rename = "allowedFields")]
pub allowed_fields: T,
}
#[derive(Clone, Debug, Default)]
pub struct RateLimitSpec {
pub rps: u32,
pub burst: u32,
pub in_flight: u32,
}
#[derive(Clone, Debug, Deserialize, Serialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct XPagination {
pub filter_fields: BTreeMap<String, Vec<String>>,
pub order_by: Vec<String>,
}
pub trait OperationBuilderODataExt<S, H, R> {
#[must_use]
fn with_odata_filter<T>(self) -> Self
where
T: toolkit_odata::filter::FilterField;
#[must_use]
fn with_odata_select(self) -> Self;
#[must_use]
fn with_odata_orderby<T>(self) -> Self
where
T: toolkit_odata::filter::FilterField;
}
impl<S, H, R, A, L> OperationBuilderODataExt<S, H, R> for OperationBuilder<H, R, S, A, L>
where
H: HandlerSlot<S>,
A: AuthState,
L: LicenseState,
{
fn with_odata_filter<T>(mut self) -> Self
where
T: toolkit_odata::filter::FilterField,
{
use std::fmt::Write as _;
use toolkit_odata::filter::FieldKind;
let mut filter = self
.spec
.vendor_extensions
.x_odata_filter
.unwrap_or_default();
let mut description = "OData v4 filter expression".to_owned();
for field in T::FIELDS {
let name = field.name().to_owned();
let kind = field.kind();
let ops: Vec<String> = match kind {
FieldKind::String => vec!["eq", "ne", "contains", "startswith", "endswith", "in"],
FieldKind::Uuid => vec!["eq", "ne", "in"],
FieldKind::Bool => vec!["eq", "ne"],
FieldKind::I64
| FieldKind::F64
| FieldKind::Decimal
| FieldKind::DateTimeUtc
| FieldKind::Date
| FieldKind::Time => {
vec!["eq", "ne", "gt", "ge", "lt", "le", "in"]
}
}
.into_iter()
.map(String::from)
.collect();
_ = write!(description, "\n- {}: {}", name, ops.join("|"));
filter.allowed_fields.insert(name.clone(), ops);
}
self.spec.params.push(ParamSpec::scalar(
"$filter".to_owned(),
ParamLocation::Query,
false,
Some(description),
"string".to_owned(),
));
self.spec.vendor_extensions.x_odata_filter = Some(filter);
self
}
fn with_odata_select(mut self) -> Self {
self.spec.params.push(ParamSpec::scalar(
"$select".to_owned(),
ParamLocation::Query,
false,
Some("OData v4 select expression".to_owned()),
"string".to_owned(),
));
self
}
fn with_odata_orderby<T>(mut self) -> Self
where
T: toolkit_odata::filter::FilterField,
{
use std::fmt::Write as _;
let mut order_by = self
.spec
.vendor_extensions
.x_odata_orderby
.unwrap_or_default();
let mut description = "OData v4 orderby expression".to_owned();
for field in T::FIELDS {
let name = field.name().to_owned();
let asc = format!("{name} asc");
let desc = format!("{name} desc");
_ = write!(description, "\n- {asc}\n- {desc}");
if !order_by.allowed_fields.contains(&asc) {
order_by.allowed_fields.push(asc);
}
if !order_by.allowed_fields.contains(&desc) {
order_by.allowed_fields.push(desc);
}
}
self.spec.params.push(ParamSpec::scalar(
"$orderby".to_owned(),
ParamLocation::Query,
false,
Some(description),
"string".to_owned(),
));
self.spec.vendor_extensions.x_odata_orderby = Some(order_by);
self
}
}
pub use crate::api::openapi_registry::{OpenApiRegistry, ensure_schema};
#[must_use]
pub struct OperationBuilder<H = Missing, R = Missing, S = (), A = AuthNotSet, L = LicenseNotSet>
where
H: HandlerSlot<S>,
A: AuthState,
L: LicenseState,
{
spec: OperationSpec,
method_router: <H as HandlerSlot<S>>::Slot,
_has_handler: PhantomData<H>,
_has_response: PhantomData<R>,
#[allow(clippy::type_complexity)]
_state: PhantomData<fn() -> S>, _auth_state: PhantomData<A>,
_license_state: PhantomData<L>,
}
impl<S> OperationBuilder<Missing, Missing, S, AuthNotSet> {
pub fn new(method: Method, path: impl Into<String>) -> Self {
let path_str = path.into();
let handler_id = format!(
"{}:{}",
method.as_str().to_lowercase(),
path_str.replace(['/', '{', '}'], "_")
);
Self {
spec: OperationSpec {
method,
path: path_str,
operation_id: None,
summary: None,
description: None,
tags: Vec::new(),
params: Vec::new(),
request_body: None,
responses: Vec::new(),
handler_id,
authenticated: false,
exposed: false,
rate_limit: None,
allowed_request_content_types: None,
vendor_extensions: VendorExtensions::default(),
license_requirement: None,
},
method_router: (), _has_handler: PhantomData,
_has_response: PhantomData,
_state: PhantomData,
_auth_state: PhantomData,
_license_state: PhantomData,
}
}
pub fn get(path: impl Into<String>) -> Self {
let path_str = path.into();
Self::new(Method::GET, normalize_to_axum_path(&path_str))
}
pub fn post(path: impl Into<String>) -> Self {
let path_str = path.into();
Self::new(Method::POST, normalize_to_axum_path(&path_str))
}
pub fn put(path: impl Into<String>) -> Self {
let path_str = path.into();
Self::new(Method::PUT, normalize_to_axum_path(&path_str))
}
pub fn delete(path: impl Into<String>) -> Self {
let path_str = path.into();
Self::new(Method::DELETE, normalize_to_axum_path(&path_str))
}
pub fn patch(path: impl Into<String>) -> Self {
let path_str = path.into();
Self::new(Method::PATCH, normalize_to_axum_path(&path_str))
}
}
impl<H, R, S, A, L> OperationBuilder<H, R, S, A, L>
where
H: HandlerSlot<S>,
A: AuthState,
L: LicenseState,
{
pub fn spec(&self) -> &OperationSpec {
&self.spec
}
pub fn operation_id(mut self, id: impl Into<String>) -> Self {
self.spec.operation_id = Some(id.into());
self
}
pub fn require_rate_limit(&mut self, rps: u32, burst: u32, in_flight: u32) -> &mut Self {
self.spec.rate_limit = Some(RateLimitSpec {
rps,
burst,
in_flight,
});
self
}
pub fn summary(mut self, text: impl Into<String>) -> Self {
self.spec.summary = Some(text.into());
self
}
pub fn description(mut self, text: impl Into<String>) -> Self {
self.spec.description = Some(text.into());
self
}
pub fn tag(mut self, tag: impl Into<String>) -> Self {
self.spec.tags.push(tag.into());
self
}
pub fn param(mut self, param: ParamSpec) -> Self {
self.spec.params.push(param);
self
}
pub fn path_param(mut self, name: impl Into<String>, description: impl Into<String>) -> Self {
self.spec.params.push(ParamSpec::scalar(
name.into(),
ParamLocation::Path,
true,
Some(description.into()),
"string".to_owned(),
));
self
}
pub fn query_param(
mut self,
name: impl Into<String>,
required: bool,
description: impl Into<String>,
) -> Self {
self.spec.params.push(ParamSpec::scalar(
name.into(),
ParamLocation::Query,
required,
Some(description.into()),
"string".to_owned(),
));
self
}
pub fn query_param_typed(
mut self,
name: impl Into<String>,
required: bool,
description: impl Into<String>,
param_type: impl Into<String>,
) -> Self {
self.spec.params.push(ParamSpec::scalar(
name.into(),
ParamLocation::Query,
required,
Some(description.into()),
param_type.into(),
));
self
}
pub fn query_params_from<T: toolkit_contract::query::QueryParams>(mut self) -> Self {
for p in T::openapi_params() {
self.spec.params.push(ParamSpec {
name: p.name.to_owned(),
location: ParamLocation::Query,
required: p.required,
description: None,
param_type: p.openapi_type.to_owned(),
array: p.array,
});
}
self
}
pub fn query_param_array(
mut self,
name: impl Into<String>,
required: bool,
description: impl Into<String>,
item_type: impl Into<String>,
) -> Self {
self.spec.params.push(ParamSpec {
name: name.into(),
location: ParamLocation::Query,
required,
description: Some(description.into()),
param_type: item_type.into(),
array: true,
});
self
}
pub fn json_request_schema(
mut self,
schema_name: impl Into<String>,
desc: impl Into<String>,
) -> Self {
self.spec.request_body = Some(RequestBodySpec {
content_type: "application/json",
description: Some(desc.into()),
schema: RequestBodySchema::Ref {
schema_name: schema_name.into(),
},
required: true,
});
self
}
pub fn json_request_schema_no_desc(mut self, schema_name: impl Into<String>) -> Self {
self.spec.request_body = Some(RequestBodySpec {
content_type: "application/json",
description: None,
schema: RequestBodySchema::Ref {
schema_name: schema_name.into(),
},
required: true,
});
self
}
pub fn json_request<T>(
mut self,
registry: &dyn OpenApiRegistry,
desc: impl Into<String>,
) -> Self
where
T: utoipa::ToSchema + utoipa::PartialSchema + api_dto::RequestApiDto + 'static,
{
let name = ensure_schema::<T>(registry);
self.spec.request_body = Some(RequestBodySpec {
content_type: "application/json",
description: Some(desc.into()),
schema: RequestBodySchema::Ref { schema_name: name },
required: true,
});
self
}
pub fn json_request_no_desc<T>(mut self, registry: &dyn OpenApiRegistry) -> Self
where
T: utoipa::ToSchema + utoipa::PartialSchema + api_dto::RequestApiDto + 'static,
{
let name = ensure_schema::<T>(registry);
self.spec.request_body = Some(RequestBodySpec {
content_type: "application/json",
description: None,
schema: RequestBodySchema::Ref { schema_name: name },
required: true,
});
self
}
pub fn request_optional(mut self) -> Self {
if let Some(rb) = &mut self.spec.request_body {
rb.required = false;
}
self
}
pub fn multipart_file_request(mut self, field_name: &str, description: Option<&str>) -> Self {
self.spec.request_body = Some(RequestBodySpec {
content_type: "multipart/form-data",
description: description
.map(|s| format!("{s} (expects field '{field_name}' with file data)")),
schema: RequestBodySchema::MultipartFile {
field_name: field_name.to_owned(),
},
required: true,
});
self.spec.allowed_request_content_types = Some(vec!["multipart/form-data"]);
self
}
pub fn octet_stream_request(mut self, description: Option<&str>) -> Self {
self.spec.request_body = Some(RequestBodySpec {
content_type: "application/octet-stream",
description: description.map(str::to_owned),
schema: RequestBodySchema::Binary,
required: true,
});
self.spec.allowed_request_content_types = Some(vec!["application/octet-stream"]);
self
}
pub fn allow_content_types(mut self, types: &[&'static str]) -> Self {
self.spec.allowed_request_content_types = Some(types.to_vec());
self
}
pub fn exposed(mut self) -> Self {
self.spec.exposed = true;
self
}
}
impl<H, R, S> OperationBuilder<H, R, S, AuthSet, LicenseNotSet>
where
H: HandlerSlot<S>,
{
pub fn require_license_features<F>(
mut self,
licenses: impl IntoIterator<Item = F>,
) -> OperationBuilder<H, R, S, AuthSet, LicenseSet>
where
F: LicenseFeature,
{
let license_names: Vec<String> = licenses
.into_iter()
.map(|l| l.as_ref().to_owned())
.collect();
self.spec.license_requirement =
(!license_names.is_empty()).then_some(LicenseReqSpec { license_names });
OperationBuilder {
spec: self.spec,
method_router: self.method_router,
_has_handler: self._has_handler,
_has_response: self._has_response,
_state: self._state,
_auth_state: self._auth_state,
_license_state: PhantomData,
}
}
pub fn no_license_required(self) -> OperationBuilder<H, R, S, AuthSet, LicenseSet> {
OperationBuilder {
spec: self.spec,
method_router: self.method_router,
_has_handler: self._has_handler,
_has_response: self._has_response,
_state: self._state,
_auth_state: self._auth_state,
_license_state: PhantomData,
}
}
}
impl<H, R, S, L> OperationBuilder<H, R, S, AuthNotSet, L>
where
H: HandlerSlot<S>,
L: LicenseState,
{
pub fn authenticated(mut self) -> OperationBuilder<H, R, S, AuthSet, L> {
self.spec.authenticated = true;
OperationBuilder {
spec: self.spec,
method_router: self.method_router,
_has_handler: self._has_handler,
_has_response: self._has_response,
_state: self._state,
_auth_state: PhantomData,
_license_state: self._license_state,
}
}
pub fn anonymous(mut self) -> OperationBuilder<H, R, S, AuthSet, LicenseSet> {
self.spec.authenticated = false;
OperationBuilder {
spec: self.spec,
method_router: self.method_router,
_has_handler: self._has_handler,
_has_response: self._has_response,
_state: self._state,
_auth_state: PhantomData,
_license_state: PhantomData,
}
}
#[deprecated(
since = "0.6.21",
note = "`.public()` split into two axes; use `.anonymous().exposed()` \
(this alias forwards to exactly that)"
)]
pub fn public(self) -> OperationBuilder<H, R, S, AuthSet, LicenseSet> {
self.anonymous().exposed()
}
}
impl<R, S, A, L> OperationBuilder<Missing, R, S, A, L>
where
S: Clone + Send + Sync + 'static,
A: AuthState,
L: LicenseState,
{
pub fn handler<F, T>(self, h: F) -> OperationBuilder<Present, R, S, A, L>
where
F: Handler<T, S> + Clone + Send + 'static,
T: 'static,
{
let method_router = match self.spec.method {
Method::GET => axum::routing::get(h),
Method::POST => axum::routing::post(h),
Method::PUT => axum::routing::put(h),
Method::DELETE => axum::routing::delete(h),
Method::PATCH => axum::routing::patch(h),
_ => axum::routing::any(|| async { axum::http::StatusCode::METHOD_NOT_ALLOWED }),
};
OperationBuilder {
spec: self.spec,
method_router, _has_handler: PhantomData::<Present>,
_has_response: self._has_response,
_state: self._state,
_auth_state: self._auth_state,
_license_state: self._license_state,
}
}
pub fn method_router(self, mr: MethodRouter<S>) -> OperationBuilder<Present, R, S, A, L> {
OperationBuilder {
spec: self.spec,
method_router: mr, _has_handler: PhantomData::<Present>,
_has_response: self._has_response,
_state: self._state,
_auth_state: self._auth_state,
_license_state: self._license_state,
}
}
}
impl<H, S, A, L> OperationBuilder<H, Missing, S, A, L>
where
H: HandlerSlot<S>,
A: AuthState,
L: LicenseState,
{
pub fn response(mut self, resp: ResponseSpec) -> OperationBuilder<H, Present, S, A, L> {
self.spec.responses.push(resp);
OperationBuilder {
spec: self.spec,
method_router: self.method_router,
_has_handler: self._has_handler,
_has_response: PhantomData::<Present>,
_state: self._state,
_auth_state: self._auth_state,
_license_state: self._license_state,
}
}
pub fn json_response(
mut self,
status: http::StatusCode,
description: impl Into<String>,
) -> OperationBuilder<H, Present, S, A, L> {
self.spec.responses.push(ResponseSpec {
status: status.as_u16(),
content_type: "application/json",
description: description.into(),
schema: None,
});
OperationBuilder {
spec: self.spec,
method_router: self.method_router,
_has_handler: self._has_handler,
_has_response: PhantomData::<Present>,
_state: self._state,
_auth_state: self._auth_state,
_license_state: self._license_state,
}
}
pub fn no_content_response(
mut self,
status: http::StatusCode,
description: impl Into<String>,
) -> OperationBuilder<H, Present, S, A, L> {
self.spec.responses.push(ResponseSpec {
status: status.as_u16(),
content_type: "",
description: description.into(),
schema: None,
});
OperationBuilder {
spec: self.spec,
method_router: self.method_router,
_has_handler: self._has_handler,
_has_response: PhantomData::<Present>,
_state: self._state,
_auth_state: self._auth_state,
_license_state: self._license_state,
}
}
pub fn json_response_with_schema<T>(
mut self,
registry: &dyn OpenApiRegistry,
status: http::StatusCode,
description: impl Into<String>,
) -> OperationBuilder<H, Present, S, A, L>
where
T: utoipa::ToSchema + utoipa::PartialSchema + api_dto::ResponseApiDto + 'static,
{
let name = ensure_schema::<T>(registry);
self.spec.responses.push(ResponseSpec {
status: status.as_u16(),
content_type: "application/json",
description: description.into(),
schema: Some(ResponseSchema::Ref { schema_name: name }),
});
OperationBuilder {
spec: self.spec,
method_router: self.method_router,
_has_handler: self._has_handler,
_has_response: PhantomData::<Present>,
_state: self._state,
_auth_state: self._auth_state,
_license_state: self._license_state,
}
}
pub fn json_array_response_with_schema<T>(
mut self,
registry: &dyn OpenApiRegistry,
status: http::StatusCode,
description: impl Into<String>,
) -> OperationBuilder<H, Present, S, A, L>
where
T: utoipa::ToSchema + utoipa::PartialSchema + api_dto::ResponseApiDto + 'static,
{
let items_schema_name = ensure_schema::<T>(registry);
self.spec.responses.push(ResponseSpec {
status: status.as_u16(),
content_type: "application/json",
description: description.into(),
schema: Some(ResponseSchema::Array { items_schema_name }),
});
OperationBuilder {
spec: self.spec,
method_router: self.method_router,
_has_handler: self._has_handler,
_has_response: PhantomData::<Present>,
_state: self._state,
_auth_state: self._auth_state,
_license_state: self._license_state,
}
}
pub fn text_response(
mut self,
status: http::StatusCode,
description: impl Into<String>,
content_type: &'static str,
) -> OperationBuilder<H, Present, S, A, L> {
self.spec.responses.push(ResponseSpec {
status: status.as_u16(),
content_type,
description: description.into(),
schema: None,
});
OperationBuilder {
spec: self.spec,
method_router: self.method_router,
_has_handler: self._has_handler,
_has_response: PhantomData::<Present>,
_state: self._state,
_auth_state: self._auth_state,
_license_state: self._license_state,
}
}
pub fn html_response(
mut self,
status: http::StatusCode,
description: impl Into<String>,
) -> OperationBuilder<H, Present, S, A, L> {
self.spec.responses.push(ResponseSpec {
status: status.as_u16(),
content_type: "text/html",
description: description.into(),
schema: None,
});
OperationBuilder {
spec: self.spec,
method_router: self.method_router,
_has_handler: self._has_handler,
_has_response: PhantomData::<Present>,
_state: self._state,
_auth_state: self._auth_state,
_license_state: self._license_state,
}
}
pub fn problem_response(
mut self,
registry: &dyn OpenApiRegistry,
status: http::StatusCode,
description: impl Into<String>,
) -> OperationBuilder<H, Present, S, A, L> {
let problem_name = ensure_schema::<toolkit_canonical_errors::Problem>(registry);
self.spec.responses.push(ResponseSpec {
status: status.as_u16(),
content_type: problem::APPLICATION_PROBLEM_JSON,
description: description.into(),
schema: Some(ResponseSchema::Ref {
schema_name: problem_name,
}),
});
OperationBuilder {
spec: self.spec,
method_router: self.method_router,
_has_handler: self._has_handler,
_has_response: PhantomData::<Present>,
_state: self._state,
_auth_state: self._auth_state,
_license_state: self._license_state,
}
}
pub fn sse_json<T>(
mut self,
openapi: &dyn OpenApiRegistry,
description: impl Into<String>,
) -> OperationBuilder<H, Present, S, A, L>
where
T: utoipa::ToSchema + utoipa::PartialSchema + api_dto::ResponseApiDto + 'static,
{
let name = ensure_schema::<T>(openapi);
self.spec.responses.push(ResponseSpec {
status: http::StatusCode::OK.as_u16(),
content_type: "text/event-stream",
description: description.into(),
schema: Some(ResponseSchema::Ref { schema_name: name }),
});
OperationBuilder {
spec: self.spec,
method_router: self.method_router,
_has_handler: self._has_handler,
_has_response: PhantomData::<Present>,
_state: self._state,
_auth_state: self._auth_state,
_license_state: self._license_state,
}
}
}
impl<H, S, A, L> OperationBuilder<H, Present, S, A, L>
where
H: HandlerSlot<S>,
A: AuthState,
L: LicenseState,
{
pub fn json_response(
mut self,
status: http::StatusCode,
description: impl Into<String>,
) -> Self {
self.spec.responses.push(ResponseSpec {
status: status.as_u16(),
content_type: "application/json",
description: description.into(),
schema: None,
});
self
}
pub fn no_content_response(
mut self,
status: http::StatusCode,
description: impl Into<String>,
) -> Self {
self.spec.responses.push(ResponseSpec {
status: status.as_u16(),
content_type: "",
description: description.into(),
schema: None,
});
self
}
pub fn json_response_with_schema<T>(
mut self,
registry: &dyn OpenApiRegistry,
status: http::StatusCode,
description: impl Into<String>,
) -> Self
where
T: utoipa::ToSchema + utoipa::PartialSchema + api_dto::ResponseApiDto + 'static,
{
let name = ensure_schema::<T>(registry);
self.spec.responses.push(ResponseSpec {
status: status.as_u16(),
content_type: "application/json",
description: description.into(),
schema: Some(ResponseSchema::Ref { schema_name: name }),
});
self
}
pub fn json_array_response_with_schema<T>(
mut self,
registry: &dyn OpenApiRegistry,
status: http::StatusCode,
description: impl Into<String>,
) -> Self
where
T: utoipa::ToSchema + utoipa::PartialSchema + api_dto::ResponseApiDto + 'static,
{
let items_schema_name = ensure_schema::<T>(registry);
self.spec.responses.push(ResponseSpec {
status: status.as_u16(),
content_type: "application/json",
description: description.into(),
schema: Some(ResponseSchema::Array { items_schema_name }),
});
self
}
pub fn text_response(
mut self,
status: http::StatusCode,
description: impl Into<String>,
content_type: &'static str,
) -> Self {
self.spec.responses.push(ResponseSpec {
status: status.as_u16(),
content_type,
description: description.into(),
schema: None,
});
self
}
pub fn html_response(
mut self,
status: http::StatusCode,
description: impl Into<String>,
) -> Self {
self.spec.responses.push(ResponseSpec {
status: status.as_u16(),
content_type: "text/html",
description: description.into(),
schema: None,
});
self
}
pub fn problem_response(
mut self,
registry: &dyn OpenApiRegistry,
status: http::StatusCode,
description: impl Into<String>,
) -> Self {
let problem_name = ensure_schema::<toolkit_canonical_errors::Problem>(registry);
self.spec.responses.push(ResponseSpec {
status: status.as_u16(),
content_type: problem::APPLICATION_PROBLEM_JSON,
description: description.into(),
schema: Some(ResponseSchema::Ref {
schema_name: problem_name,
}),
});
self
}
pub fn sse_json<T>(
mut self,
openapi: &dyn OpenApiRegistry,
description: impl Into<String>,
) -> Self
where
T: utoipa::ToSchema + utoipa::PartialSchema + api_dto::ResponseApiDto + 'static,
{
let name = ensure_schema::<T>(openapi);
self.spec.responses.push(ResponseSpec {
status: http::StatusCode::OK.as_u16(),
content_type: "text/event-stream",
description: description.into(),
schema: Some(ResponseSchema::Ref { schema_name: name }),
});
self
}
pub fn standard_errors(mut self, registry: &dyn OpenApiRegistry) -> Self {
use http::StatusCode;
let problem_name = ensure_schema::<toolkit_canonical_errors::Problem>(registry);
let standard_errors = [
(StatusCode::BAD_REQUEST, "Bad Request"),
(StatusCode::UNAUTHORIZED, "Unauthorized"),
(StatusCode::FORBIDDEN, "Forbidden"),
(StatusCode::NOT_FOUND, "Not Found"),
(StatusCode::CONFLICT, "Conflict"),
(StatusCode::TOO_MANY_REQUESTS, "Too Many Requests"),
(StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error"),
];
for (status, description) in standard_errors {
self.spec.responses.push(ResponseSpec {
status: status.as_u16(),
content_type: problem::APPLICATION_PROBLEM_JSON,
description: description.to_owned(),
schema: Some(ResponseSchema::Ref {
schema_name: problem_name.clone(),
}),
});
}
self
}
pub fn with_400_validation_error(mut self, registry: &dyn OpenApiRegistry) -> Self {
let problem_name = ensure_schema::<toolkit_canonical_errors::Problem>(registry);
self.spec.responses.push(ResponseSpec {
status: http::StatusCode::BAD_REQUEST.as_u16(),
content_type: problem::APPLICATION_PROBLEM_JSON,
description: "Validation Error".to_owned(),
schema: Some(ResponseSchema::Ref {
schema_name: problem_name,
}),
});
self
}
pub fn error_400(self, registry: &dyn OpenApiRegistry) -> Self {
self.problem_response(registry, http::StatusCode::BAD_REQUEST, "Bad Request")
}
pub fn error_401(self, registry: &dyn OpenApiRegistry) -> Self {
self.problem_response(registry, http::StatusCode::UNAUTHORIZED, "Unauthorized")
}
pub fn error_403(self, registry: &dyn OpenApiRegistry) -> Self {
self.problem_response(registry, http::StatusCode::FORBIDDEN, "Forbidden")
}
pub fn error_404(self, registry: &dyn OpenApiRegistry) -> Self {
self.problem_response(registry, http::StatusCode::NOT_FOUND, "Not Found")
}
pub fn error_409(self, registry: &dyn OpenApiRegistry) -> Self {
self.problem_response(registry, http::StatusCode::CONFLICT, "Conflict")
}
pub fn error_413(self, registry: &dyn OpenApiRegistry) -> Self {
self.problem_response(
registry,
http::StatusCode::PAYLOAD_TOO_LARGE,
"Payload Too Large",
)
}
pub fn error_415(self, registry: &dyn OpenApiRegistry) -> Self {
self.problem_response(
registry,
http::StatusCode::UNSUPPORTED_MEDIA_TYPE,
"Unsupported Media Type",
)
}
pub fn error_422(self, registry: &dyn OpenApiRegistry) -> Self {
self.problem_response(
registry,
http::StatusCode::UNPROCESSABLE_ENTITY,
"Unprocessable Entity",
)
}
pub fn error_429(self, registry: &dyn OpenApiRegistry) -> Self {
self.problem_response(
registry,
http::StatusCode::TOO_MANY_REQUESTS,
"Too Many Requests",
)
}
pub fn error_500(self, registry: &dyn OpenApiRegistry) -> Self {
self.problem_response(
registry,
http::StatusCode::INTERNAL_SERVER_ERROR,
"Internal Server Error",
)
}
pub fn error_502(self, registry: &dyn OpenApiRegistry) -> Self {
self.problem_response(registry, http::StatusCode::BAD_GATEWAY, "Bad Gateway")
}
pub fn error_503(self, registry: &dyn OpenApiRegistry) -> Self {
self.problem_response(
registry,
http::StatusCode::SERVICE_UNAVAILABLE,
"Service Unavailable",
)
}
pub fn error_504(self, registry: &dyn OpenApiRegistry) -> Self {
self.problem_response(
registry,
http::StatusCode::GATEWAY_TIMEOUT,
"Gateway Timeout",
)
}
}
impl<S> OperationBuilder<Present, Present, S, AuthSet, LicenseSet>
where
S: Clone + Send + Sync + 'static,
{
pub fn register(self, router: Router<S>, openapi: &dyn OpenApiRegistry) -> Router<S> {
openapi.register_operation(&self.spec);
router.route(&self.spec.path, self.method_router)
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
#[path = "operation_builder_tests.rs"]
mod tests;