Skip to content

Commit

Permalink
feat(aide): added wasm support
Browse files Browse the repository at this point in the history
* adds axum-wasm feature

* adds axum worker example

---------

Co-authored-by: Dominik Lenz <dominikalexanerlenz@gmail.com>
  • Loading branch information
domlen2003 and Dominik Lenz committed Apr 12, 2024
1 parent 8bd148a commit 42d1b7c
Show file tree
Hide file tree
Showing 16 changed files with 482 additions and 4 deletions.
3 changes: 2 additions & 1 deletion crates/aide/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ aide-macros = { version = "0.7.0", path = "../aide-macros", optional = true }
bytes = { version = "1", optional = true }
http = { version = "1.0.0", optional = true }

axum = { version = "0.7.1", optional = true }
axum = { version = "0.7.1", optional = true, default-features = false }
axum-extra = { version = "0.9.0", optional = true }
tower-layer = { version = "0.3", optional = true }
tower-service = { version = "0.3", optional = true }
Expand All @@ -47,6 +47,7 @@ axum-extra-cookie = ["axum", "axum-extra", "axum-extra/cookie"]
axum-extra-cookie-private = ["axum", "axum-extra", "axum-extra/cookie-private"]
axum-extra-form = ["axum", "axum-extra", "axum-extra/form"]
axum-extra-query = ["axum", "axum-extra", "axum-extra/query"]
axum-wasm = ["axum"]


serde_qs = ["dep:serde_qs"]
Expand Down
8 changes: 7 additions & 1 deletion crates/aide/src/axum/inputs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@ use crate::{
use axum::{
body::Body,
extract::{
ConnectInfo, Extension, Form, Host, Json, MatchedPath, OriginalUri, Path, Query, RawQuery,
Extension, Form, Host, Json, MatchedPath, OriginalUri, Path, Query, RawQuery,
State,
},
};

#[cfg(not(feature = "axum-wasm"))]
use axum::extract::ConnectInfo;

use indexmap::IndexMap;
use schemars::{
schema::{ArrayValidation, InstanceType, Schema, SingleOrVec},
Expand All @@ -27,6 +31,8 @@ use crate::{

impl<T> OperationInput for Extension<T> {}
impl<T> OperationInput for State<T> {}

#[cfg(not(feature = "axum-wasm"))]
impl<T> OperationInput for ConnectInfo<T> {}
impl OperationInput for MatchedPath {}
impl OperationInput for OriginalUri {}
Expand Down
6 changes: 4 additions & 2 deletions crates/aide/src/axum/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,20 +172,21 @@ use std::{convert::Infallible, future::Future, mem, pin::Pin};

use crate::{
gen::{self, in_context},
openapi::{Components, OpenApi, PathItem, ReferenceOr, SchemaObject},
openapi::{OpenApi, PathItem, ReferenceOr, SchemaObject},
operation::OperationHandler,
util::merge_paths,
OperationInput, OperationOutput,
};
use axum::{
body::Body,
extract::connect_info::IntoMakeServiceWithConnectInfo,
handler::Handler,
http::Request,
response::IntoResponse,
routing::{IntoMakeService, Route, RouterAsService, RouterIntoService},
Router,
};
#[cfg(not(feature = "axum-wasm"))]
use axum::extract::connect_info::IntoMakeServiceWithConnectInfo;
use indexmap::map::Entry;
use indexmap::IndexMap;
use tower_layer::Layer;
Expand Down Expand Up @@ -598,6 +599,7 @@ impl ApiRouter<()> {
/// See [`axum::Router::into_make_service_with_connect_info`] for details.
#[tracing::instrument(skip_all)]
#[must_use]
#[cfg(not(feature = "axum-wasm"))]
pub fn into_make_service_with_connect_info<C>(
self,
) -> IntoMakeServiceWithConnectInfo<Router<()>, C> {
Expand Down
3 changes: 3 additions & 0 deletions examples/example-axum-worker/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
build/
.wrangler/
32 changes: 32 additions & 0 deletions examples/example-axum-worker/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
[package]
name = "example-axum-worker"
version = "0.1.0"
edition = "2021"
publish = false

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
aide = { path = "../../crates/aide", features = [
"redoc",
"scalar",
"axum",
"axum-extra",
"macros",
"axum-wasm"
] }
tower-service = "0.3.2"
async-trait = "0.1.57"
worker = { version = "0.1.0", features = ["http", "axum"] }
console_error_panic_hook = "0.1.7"
axum = { version = "0.7.1", default-features = false, features = ["macros", "form", "matched-path", "query", "original-uri"] }
axum-extra = "0.9.0"
axum-jsonschema = { path = "../../crates/axum-jsonschema", features = [
"aide",
] }
axum-macros = "0.4.0"
schemars = { version = "0.8.10", features = ["uuid1"] }
serde = { version = "1.0.144", features = ["derive", "rc"] }
serde_json = "1.0.85"
uuid = { version = "1.1.2", features = ["serde", "v4"] }
5 changes: 5 additions & 0 deletions examples/example-axum-worker/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Aide axum

A minimal to-do axum cloudflare worker documented with aide.

You can run it with `npm run dev`, and then visit the documentation at `http://localhost:3000`.
11 changes: 11 additions & 0 deletions examples/example-axum-worker/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "example-axum-worker",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "wrangler dev --env dev"
},
"devDependencies": {
"wrangler": "^3.49.0"
}
}
6 changes: 6 additions & 0 deletions examples/example-axum-worker/src/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Todo API

A very simple Todo server with documentation.

The purpose is to showcase the documentation workflow of Aide rather
than a correct implementation.
58 changes: 58 additions & 0 deletions examples/example-axum-worker/src/docs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
use std::sync::Arc;

use aide::{
axum::{
routing::{get, get_with},
ApiRouter, IntoApiResponse,
},
openapi::OpenApi,
redoc::Redoc,
scalar::Scalar,
};
use axum::{response::IntoResponse, Extension};

use crate::{extractors::Json, state::AppState};

pub fn docs_routes(state: AppState) -> ApiRouter {
// We infer the return types for these routes
// as an example.
//
// As a result, the `serve_redoc` route will
// have the `text/html` content-type correctly set
// with a 200 status.
aide::gen::infer_responses(true);

let router: ApiRouter = ApiRouter::new()
.api_route_with(
"/",
get_with(
Scalar::new("/docs/private/api.json")
.with_title("Aide Axum")
.axum_handler(),
|op| op.description("This documentation page."),
),
|p| p.security_requirement("ApiKey"),
)
.api_route_with(
"/redoc",
get_with(
Redoc::new("/docs/private/api.json")
.with_title("Aide Axum")
.axum_handler(),
|op| op.description("This documentation page."),
),
|p| p.security_requirement("ApiKey"),
)
.route("/private/api.json", get(serve_docs))
.with_state(state);

// Afterwards we disable response inference because
// it might be incorrect for other routes.
aide::gen::infer_responses(false);

router
}

async fn serve_docs(Extension(api): Extension<Arc<OpenApi>>) -> impl IntoApiResponse {
Json(api).into_response()
}
49 changes: 49 additions & 0 deletions examples/example-axum-worker/src/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
use axum::{http::StatusCode, response::IntoResponse};
use schemars::JsonSchema;
use serde::Serialize;
use serde_json::Value;
use uuid::Uuid;

/// A default error response for most API errors.
#[derive(Debug, Serialize, JsonSchema)]
pub struct AppError {
/// An error message.
pub error: String,
/// A unique error ID.
pub error_id: Uuid,
#[serde(skip)]
pub status: StatusCode,
/// Optional Additional error details.
#[serde(skip_serializing_if = "Option::is_none")]
pub error_details: Option<Value>,
}

impl AppError {
pub fn new(error: &str) -> Self {
Self {
error: error.to_string(),
error_id: Uuid::new_v4(),
status: StatusCode::BAD_REQUEST,
error_details: None,
}
}

pub fn with_status(mut self, status: StatusCode) -> Self {
self.status = status;
self
}

pub fn with_details(mut self, details: Value) -> Self {
self.error_details = Some(details);
self
}
}

impl IntoResponse for AppError {
fn into_response(self) -> axum::response::Response {
let status = self.status;
let mut res = axum::Json(self).into_response();
*res.status_mut() = status;
res
}
}
38 changes: 38 additions & 0 deletions examples/example-axum-worker/src/extractors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use aide::operation::OperationIo;
use axum::response::IntoResponse;
use axum_jsonschema::JsonSchemaRejection;
use axum_macros::FromRequest;
use serde::Serialize;
use serde_json::json;

use crate::errors::AppError;

#[derive(FromRequest, OperationIo)]
#[from_request(via(axum_jsonschema::Json), rejection(AppError))]
#[aide(
input_with = "axum_jsonschema::Json<T>",
output_with = "axum_jsonschema::Json<T>",
json_schema
)]
pub struct Json<T>(pub T);

impl<T> IntoResponse for Json<T>
where
T: Serialize,
{
fn into_response(self) -> axum::response::Response {
axum::Json(self.0).into_response()
}
}

impl From<JsonSchemaRejection> for AppError {
fn from(rejection: JsonSchemaRejection) -> Self {
match rejection {
JsonSchemaRejection::Json(j) => Self::new(&j.to_string()),
JsonSchemaRejection::Serde(_) => Self::new("invalid request"),
JsonSchemaRejection::Schema(s) => {
Self::new("invalid request").with_details(json!({ "schema_validation": s }))
}
}
}
}
84 changes: 84 additions & 0 deletions examples/example-axum-worker/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
use std::sync::Arc;

use aide::{
axum::ApiRouter,
openapi::{OpenApi, Tag},
transform::TransformOpenApi,
};
use axum::{http::StatusCode, Extension, http};
use tower_service::Service;
use docs::docs_routes;
use errors::AppError;
use extractors::Json;
use state::AppState;
use todos::routes::todo_routes;
use uuid::Uuid;
use worker::{console_log, Context, Env, event, HttpRequest};

pub mod docs;
pub mod errors;
pub mod extractors;
pub mod state;
pub mod todos;

#[event(start)]
fn start(){
console_log!("Example docs are accessible at http://127.0.0.1:3000/docs");
}

#[event(fetch)]
async fn fetch(
req: HttpRequest,
_env: Env,
_ctx: Context,
) -> worker::Result<http::Response<axum::body::Body>> {
console_error_panic_hook::set_once();
aide::gen::on_error(|error| {
println!("{error}");
});

aide::gen::extract_schemas(true);


let state = AppState::default();

let mut api = OpenApi::default();

let mut app = ApiRouter::new()
.nest_api_service("/todo", todo_routes(state.clone()))
.nest_api_service("/docs", docs_routes(state.clone()))
.finish_api_with(&mut api, api_docs)
.layer(Extension(Arc::new(api))) // Arc is very important here or you will face massive memory and performance issues
.with_state(state);

Ok(app.call(req).await?)
}

fn api_docs(api: TransformOpenApi) -> TransformOpenApi {
api.title("Aide axum Open API")
.summary("An example Todo application")
.description(include_str!("README.md"))
.tag(Tag {
name: "todo".into(),
description: Some("Todo Management".into()),
..Default::default()
})
.security_scheme(
"ApiKey",
aide::openapi::SecurityScheme::ApiKey {
location: aide::openapi::ApiKeyLocation::Header,
name: "X-Auth-Key".into(),
description: Some("A key that is ignored.".into()),
extensions: Default::default(),
},
)
.default_response_with::<Json<AppError>, _>(|res| {
res.example(AppError {
error: "some error happened".to_string(),
error_details: None,
error_id: Uuid::nil(),
// This is not visible.
status: StatusCode::IM_A_TEAPOT,
})
})
}
13 changes: 13 additions & 0 deletions examples/example-axum-worker/src/state.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
use std::{
collections::HashMap,
sync::{Arc, Mutex},
};

use uuid::Uuid;

use crate::todos::TodoItem;

#[derive(Debug, Clone, Default)]
pub struct AppState {
pub todos: Arc<Mutex<HashMap<Uuid, TodoItem>>>,
}
15 changes: 15 additions & 0 deletions examples/example-axum-worker/src/todos/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use uuid::Uuid;

pub mod routes;

/// A single Todo item.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct TodoItem {
pub id: Uuid,
/// The description of the item.
pub description: String,
/// Whether the item was completed.
pub complete: bool,
}
Loading

0 comments on commit 42d1b7c

Please sign in to comment.