mirror of
https://github.com/tokio-rs/axum.git
synced 2026-09-01 00:00:14 +02:00
wip
This commit is contained in:
@@ -7,3 +7,10 @@ edition = "2021"
|
||||
axum = { path = "../axum", version = "0.4" }
|
||||
okapi = "0.7.0-rc.1"
|
||||
schemars = "0.8.8"
|
||||
mime = "0.3.16"
|
||||
|
||||
serde_json = "1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
assert-json-diff = "2.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
use axum::{
|
||||
body::HttpBody,
|
||||
extract::FromRequest,
|
||||
handler::Handler,
|
||||
http::{Request, StatusCode},
|
||||
response::{IntoResponse, Response},
|
||||
routing::{self, MethodRouter},
|
||||
Json, Router,
|
||||
};
|
||||
use okapi::openapi3::{
|
||||
self, Components, Info, MediaType, OpenApi, Operation, Parameter, RefOr, RequestBody,
|
||||
};
|
||||
use schemars::{
|
||||
schema::{RootSchema, Schema},
|
||||
JsonSchema,
|
||||
};
|
||||
use std::{
|
||||
collections::BTreeMap, convert::Infallible, future::Future, marker::PhantomData, sync::Arc,
|
||||
};
|
||||
|
||||
pub trait DescribeRequest {
|
||||
fn describe(operation: &mut Operation, components: &mut Components);
|
||||
}
|
||||
|
||||
impl DescribeRequest for () {
|
||||
fn describe(_: &mut Operation, _: &mut Components) {}
|
||||
}
|
||||
|
||||
macro_rules! impl_tuples {
|
||||
( $($ty:ident),* $(,)? ) => {
|
||||
impl<$($ty,)*> DescribeRequest for ($($ty,)*)
|
||||
where
|
||||
$($ty: DescribeRequest,)*
|
||||
{
|
||||
fn describe(operation: &mut Operation, components: &mut Components) {
|
||||
$( $ty::describe(operation, components); )*
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
all_the_tuples!(impl_tuples);
|
||||
|
||||
impl<T> DescribeRequest for Json<T>
|
||||
where
|
||||
T: JsonSchema,
|
||||
{
|
||||
fn describe(operation: &mut Operation, components: &mut Components) {
|
||||
let RootSchema {
|
||||
mut schema,
|
||||
definitions,
|
||||
meta_schema: _,
|
||||
} = schemars::schema_for!(T);
|
||||
|
||||
components.schemas.extend(
|
||||
definitions
|
||||
.into_iter()
|
||||
.filter_map(|(k, schema)| match schema {
|
||||
Schema::Bool(_) => None,
|
||||
Schema::Object(obj) => Some((k, obj)),
|
||||
}),
|
||||
);
|
||||
|
||||
schema.object().properties = std::mem::take(&mut schema.object().properties)
|
||||
.into_iter()
|
||||
.map(|(key, schema)| match schema {
|
||||
Schema::Bool(_) => (key, schema),
|
||||
Schema::Object(mut obj) => {
|
||||
if let Some(reference) = &mut obj.reference {
|
||||
*reference = reference.replace("/definitions/", "/components/schemas/");
|
||||
}
|
||||
(key, Schema::Object(obj))
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let request_body = RequestBody {
|
||||
content: schemars::Map::from_iter([(
|
||||
mime::APPLICATION_JSON.to_string(),
|
||||
MediaType {
|
||||
schema: Some(schema),
|
||||
..Default::default()
|
||||
},
|
||||
)]),
|
||||
required: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
operation.request_body = Some(RefOr::Object(request_body));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
use axum::{
|
||||
body::HttpBody,
|
||||
extract::FromRequest,
|
||||
handler::Handler,
|
||||
http::{Request, StatusCode},
|
||||
response::{IntoResponse, Response},
|
||||
routing::{self, MethodRouter},
|
||||
Json, Router,
|
||||
};
|
||||
use okapi::openapi3::{
|
||||
self, Components, Info, MediaType, OpenApi, Operation, Parameter, RefOr, RequestBody,
|
||||
};
|
||||
use schemars::{
|
||||
schema::{RootSchema, Schema},
|
||||
JsonSchema,
|
||||
};
|
||||
use std::{
|
||||
collections::BTreeMap, convert::Infallible, future::Future, marker::PhantomData, sync::Arc,
|
||||
};
|
||||
|
||||
pub trait DescribeResponse {
|
||||
fn describe(operation: &mut Operation, components: &mut Components);
|
||||
}
|
||||
|
||||
impl DescribeResponse for () {
|
||||
fn describe(operation: &mut Operation, components: &mut Components) {
|
||||
Ok::describe(operation, components)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_tuples {
|
||||
( $($ty:ident),* $(,)? ) => {
|
||||
impl<$($ty,)*> DescribeResponse for ($($ty,)*)
|
||||
where
|
||||
$($ty: DescribeResponse,)*
|
||||
{
|
||||
fn describe(operation: &mut Operation, components: &mut Components) {
|
||||
$( $ty::describe(operation, components); )*
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
all_the_tuples!(impl_tuples);
|
||||
|
||||
macro_rules! status {
|
||||
(
|
||||
$name:ident, $variant:ident
|
||||
) => {
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct $name;
|
||||
|
||||
impl IntoResponse for $name {
|
||||
fn into_response(self) -> Response {
|
||||
StatusCode::$variant.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
impl DescribeResponse for $name {
|
||||
fn describe(operation: &mut Operation, _: &mut Components) {
|
||||
operation.responses.responses.insert(
|
||||
StatusCode::$variant.as_u16().to_string(),
|
||||
RefOr::Object(openapi3::Response {
|
||||
description: "Successful response".to_owned(),
|
||||
..Default::default()
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
status!(Ok, OK);
|
||||
status!(Created, CREATED);
|
||||
+291
-103
@@ -1,16 +1,57 @@
|
||||
#![allow(missing_debug_implementations)]
|
||||
#![allow(missing_debug_implementations, dead_code, unused_imports)]
|
||||
#![deny(unreachable_pub)]
|
||||
|
||||
use axum::{
|
||||
async_trait,
|
||||
body::HttpBody,
|
||||
extract::FromRequest,
|
||||
handler::Handler,
|
||||
http::Request,
|
||||
http::{Request, StatusCode},
|
||||
response::{IntoResponse, Response},
|
||||
routing::{get, post},
|
||||
Router,
|
||||
routing::{self, MethodRouter},
|
||||
Json, Router,
|
||||
};
|
||||
use okapi::openapi3::{
|
||||
self, Components, Info, MediaType, OpenApi, Operation, Parameter, RefOr, RequestBody,
|
||||
};
|
||||
use schemars::{
|
||||
schema::{RootSchema, Schema},
|
||||
JsonSchema,
|
||||
};
|
||||
use std::{
|
||||
collections::BTreeMap, convert::Infallible, future::Future, marker::PhantomData, sync::Arc,
|
||||
};
|
||||
|
||||
#[macro_use]
|
||||
mod macros {
|
||||
macro_rules! all_the_tuples {
|
||||
($name:ident) => {
|
||||
$name!(T1);
|
||||
$name!(T1, T2);
|
||||
$name!(T1, T2, T3);
|
||||
$name!(T1, T2, T3, T4);
|
||||
$name!(T1, T2, T3, T4, T5);
|
||||
$name!(T1, T2, T3, T4, T5, T6);
|
||||
$name!(T1, T2, T3, T4, T5, T6, T7);
|
||||
$name!(T1, T2, T3, T4, T5, T6, T7, T8);
|
||||
$name!(T1, T2, T3, T4, T5, T6, T7, T8, T9);
|
||||
$name!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10);
|
||||
$name!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11);
|
||||
$name!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12);
|
||||
$name!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13);
|
||||
$name!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14);
|
||||
$name!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15);
|
||||
$name!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
mod describe_request;
|
||||
mod describe_response;
|
||||
|
||||
pub use self::{
|
||||
describe_request::DescribeRequest,
|
||||
describe_response::{Created, DescribeResponse, Ok},
|
||||
};
|
||||
use okapi::openapi3::{Info, OpenApi, Operation};
|
||||
use std::{future::Future, marker::PhantomData, sync::Arc};
|
||||
|
||||
pub struct OpenApiRouter<B> {
|
||||
router: Router<B>,
|
||||
@@ -26,129 +67,276 @@ where
|
||||
router: Default::default(),
|
||||
schema: OpenApi {
|
||||
info,
|
||||
openapi: "3.0.0".to_owned(),
|
||||
components: Some(Components::default()),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get<H, T>(mut self, path: &str, handler: H) -> Self
|
||||
where
|
||||
H: OpenApiHandler<T, B>,
|
||||
T: 'static,
|
||||
{
|
||||
let mut operation = Operation::default();
|
||||
handler.clone().to_operation(&mut operation);
|
||||
self.schema.paths.entry(path.to_owned()).or_default().get = Some(operation);
|
||||
self.router = self.router.route(path, get(handler));
|
||||
self
|
||||
pub fn into_parts(self) -> (Router<B>, OpenApi) {
|
||||
(self.router, self.schema)
|
||||
}
|
||||
|
||||
pub fn post<H, T>(mut self, path: &str, handler: H) -> Self
|
||||
where
|
||||
H: OpenApiHandler<T, B>,
|
||||
T: 'static,
|
||||
{
|
||||
let mut operation = Operation::default();
|
||||
handler.clone().to_operation(&mut operation);
|
||||
self.schema.paths.entry(path.to_owned()).or_default().post = Some(operation);
|
||||
self.router = self.router.route(path, post(handler));
|
||||
pub fn route(mut self, path: &str, handler: OpenApiHandler<B>) -> Self {
|
||||
let OpenApiHandler {
|
||||
svc,
|
||||
method,
|
||||
operation,
|
||||
components,
|
||||
} = handler;
|
||||
|
||||
self.router = self.router.route(path, svc);
|
||||
|
||||
extend_components(self.schema.components.as_mut().unwrap(), components);
|
||||
|
||||
let path = path
|
||||
.split('/')
|
||||
.map(|segment| {
|
||||
if let Some(param) = segment.strip_prefix(':') {
|
||||
format!("{{{}}}", param)
|
||||
} else {
|
||||
// TODO(david): wildcards
|
||||
segment.to_owned()
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("/");
|
||||
|
||||
let path_item = self.schema.paths.entry(path).or_default();
|
||||
match method {
|
||||
Method::Get => path_item.get = Some(operation),
|
||||
Method::Post => path_item.post = Some(operation),
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub trait OpenApiHandler<T, B>: Handler<T, B> {
|
||||
fn to_operation(self, operation: &mut Operation);
|
||||
fn extend_components(current: &mut Components, new: Components) {
|
||||
let Components {
|
||||
schemas,
|
||||
responses,
|
||||
parameters,
|
||||
examples,
|
||||
request_bodies,
|
||||
headers,
|
||||
security_schemes,
|
||||
links,
|
||||
callbacks,
|
||||
extensions,
|
||||
} = current;
|
||||
|
||||
fn map_operation<F>(self, f: F) -> MapOperation<Self, T, B, F>
|
||||
schemas.extend(new.schemas);
|
||||
responses.extend(new.responses);
|
||||
parameters.extend(new.parameters);
|
||||
examples.extend(new.examples);
|
||||
request_bodies.extend(new.request_bodies);
|
||||
headers.extend(new.headers);
|
||||
security_schemes.extend(new.security_schemes);
|
||||
links.extend(new.links);
|
||||
callbacks.extend(new.callbacks);
|
||||
extensions.extend(new.extensions);
|
||||
}
|
||||
|
||||
macro_rules! method {
|
||||
($fn_name:ident, $method:ident) => {
|
||||
pub fn $fn_name<H, T, B>(handler: H) -> OpenApiHandler<B>
|
||||
where
|
||||
H: Handler<T, B> + HandlerResponse<T>,
|
||||
T: DescribeRequest + 'static,
|
||||
H::Response: DescribeResponse,
|
||||
B: Send + 'static,
|
||||
{
|
||||
let mut operation = Operation::default();
|
||||
let mut components = Components::default();
|
||||
T::describe(&mut operation, &mut components);
|
||||
H::Response::describe(&mut operation, &mut components);
|
||||
OpenApiHandler {
|
||||
svc: routing::$fn_name(handler),
|
||||
method: Method::$method,
|
||||
operation,
|
||||
components,
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
method!(get, Get);
|
||||
method!(post, Post);
|
||||
|
||||
// TODO(david): the remaining methods
|
||||
enum Method {
|
||||
Get,
|
||||
Post,
|
||||
}
|
||||
|
||||
pub struct OpenApiHandler<B> {
|
||||
svc: MethodRouter<B, Infallible>,
|
||||
method: Method,
|
||||
operation: Operation,
|
||||
components: Components,
|
||||
}
|
||||
|
||||
impl<B> OpenApiHandler<B> {
|
||||
pub fn operation_id<S>(self, id: S) -> Self
|
||||
where
|
||||
F: FnOnce(&mut Operation),
|
||||
S: Into<String>,
|
||||
{
|
||||
MapOperation {
|
||||
handler: self,
|
||||
f,
|
||||
_marker: PhantomData,
|
||||
self.map_operation(|op, _| {
|
||||
op.operation_id = Some(id.into());
|
||||
})
|
||||
}
|
||||
|
||||
pub fn summary<S>(self, summary: S) -> Self
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
self.map_operation(|op, _| {
|
||||
op.summary = Some(summary.into());
|
||||
})
|
||||
}
|
||||
|
||||
pub fn description<S>(self, description: S) -> Self
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
self.map_operation(|op, _| {
|
||||
op.description = Some(description.into());
|
||||
})
|
||||
}
|
||||
|
||||
pub fn map_operation<F>(mut self, f: F) -> Self
|
||||
where
|
||||
F: FnOnce(&mut Operation, &mut Components),
|
||||
{
|
||||
f(&mut self.operation, &mut self.components);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub trait HandlerResponse<T> {
|
||||
type Response;
|
||||
}
|
||||
|
||||
impl<F, Fut> HandlerResponse<()> for F
|
||||
where
|
||||
F: FnOnce() -> Fut,
|
||||
Fut: Future,
|
||||
{
|
||||
type Response = Fut::Output;
|
||||
}
|
||||
|
||||
macro_rules! impl_tuples {
|
||||
( $($ty:ident),* $(,)? ) => {
|
||||
impl<F, Fut, $($ty,)*> HandlerResponse<($($ty,)*)> for F
|
||||
where
|
||||
F: FnOnce($($ty,)*) -> Fut,
|
||||
Fut: Future,
|
||||
{
|
||||
type Response = Fut::Output;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub struct MapOperation<H, T, B, F> {
|
||||
handler: H,
|
||||
f: F,
|
||||
_marker: PhantomData<(T, B)>,
|
||||
}
|
||||
|
||||
impl<H, T, B, F> Clone for MapOperation<H, T, B, F>
|
||||
where
|
||||
H: Clone,
|
||||
F: Clone,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
handler: self.handler.clone(),
|
||||
f: self.f.clone(),
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<H, T, B, F> OpenApiHandler<T, B> for MapOperation<H, T, B, F>
|
||||
where
|
||||
H: OpenApiHandler<T, B> + Handler<T, B>,
|
||||
F: FnOnce(&mut Operation) + Clone + Send + 'static,
|
||||
T: Send + 'static,
|
||||
B: Send + 'static,
|
||||
{
|
||||
fn to_operation(self, operation: &mut Operation) {
|
||||
(self.f)(operation);
|
||||
}
|
||||
}
|
||||
|
||||
impl<H, T, B, F> Handler<T, B> for MapOperation<H, T, B, F>
|
||||
where
|
||||
H: Handler<T, B>,
|
||||
F: Clone + Send + 'static,
|
||||
T: Send + 'static,
|
||||
B: Send + 'static,
|
||||
{
|
||||
type Future = H::Future;
|
||||
|
||||
fn call(self, req: Request<B>) -> Self::Future {
|
||||
self.handler.call(req)
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, Fut, Res, B> OpenApiHandler<(), B> for F
|
||||
where
|
||||
F: FnOnce() -> Fut + Clone + Send + 'static,
|
||||
Fut: Future<Output = Res> + Send,
|
||||
Res: IntoResponse,
|
||||
B: Send + 'static,
|
||||
{
|
||||
fn to_operation(self, operation: &mut Operation) {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
all_the_tuples!(impl_tuples);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use assert_json_diff::assert_json_eq;
|
||||
use axum::body::Body;
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn test_something() {
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
struct UsersCreate {
|
||||
account: Account,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
struct Account {
|
||||
username: String,
|
||||
}
|
||||
|
||||
async fn users_show() {}
|
||||
|
||||
async fn users_create() {}
|
||||
async fn users_create(Json(_): Json<UsersCreate>) -> Created {
|
||||
Created
|
||||
}
|
||||
|
||||
let _router: OpenApiRouter<Body> = OpenApiRouter::new(Info::default())
|
||||
.post(
|
||||
"/users",
|
||||
users_create
|
||||
.map_operation(|_| {})
|
||||
.map_operation(|mut operation| {
|
||||
operation.summary = Some("Create a new user".to_owned());
|
||||
}),
|
||||
)
|
||||
.get("/users/:id", users_show.map_operation(|_operation| {}));
|
||||
let (router, schema) = OpenApiRouter::<Body>::new(Info::default())
|
||||
.route("/users/:id", get(users_show).operation_id("users_show"))
|
||||
.route("/users", post(users_create).operation_id("users_create"))
|
||||
.into_parts();
|
||||
|
||||
assert_json_eq!(
|
||||
schema,
|
||||
json!({
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"title": "",
|
||||
"version": ""
|
||||
},
|
||||
"paths": {
|
||||
"/users": {
|
||||
"post": {
|
||||
"operationId": "users_create",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"title": "UsersCreate",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"account",
|
||||
],
|
||||
"properties": {
|
||||
"account": {
|
||||
"$ref": "#/components/schemas/Account"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Successful response",
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/{id}": {
|
||||
"get": {
|
||||
"operationId": "users_show",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Account": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"username"
|
||||
],
|
||||
"properties": {
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user