Skip to content

Commit

Permalink
OpenAPI Schema (#12)
Browse files Browse the repository at this point in the history
* Automatically generate OpenAPI schema

* Properly document fields

* Expose API docs
  • Loading branch information
m1guelpf authored Mar 28, 2024
1 parent 78ad17e commit cd6861f
Show file tree
Hide file tree
Showing 9 changed files with 803 additions and 70 deletions.
754 changes: 715 additions & 39 deletions Cargo.lock

Large diffs are not rendered by default.

9 changes: 6 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,19 @@ repository = "https://github.com/worldcoin/wallet-bridge"
description = "A bridge between the World ID SDK and the World App"

[dependencies]
axum = "0.6.20"
axum = "0.7.4"
tower = "0.4.13"
dotenvy = "0.15.7"
serde_json = "1.0.107"
tracing = { version = "0.1", features = ["log"] }
tokio = { version = "1.31.0", features = ["full"] }
serde = { version = "1.0.183", features = ["derive"] }
tower-http = { version = "0.4.3", features = ["cors"] }
schemars = { version = "0.8.16", features = ["uuid1"] }
tower-http = { version = "0.5.2", features = ["cors"] }
uuid = { version = "1.4.1", features = ["v4", "serde"] }
redis = { version = "0.23.0", features = ["tokio-comp", "connection-manager", "tokio-native-tls-comp"] }
aide = { version = "0.13.2", features = ["axum", "scalar"] }
axum-jsonschema = { version = "0.8.0", features = ["aide"] }
redis = { version = "0.24.0", features = ["tokio-comp", "connection-manager", "tokio-native-tls-comp"] }
tracing-subscriber = { version = "0.3", default-features = false, features = [
"fmt","json"
] }
Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#![warn(clippy::all, clippy::pedantic, clippy::nursery)]

use dotenvy::dotenv;
use redis::aio::ConnectionManager;
use std::env;
Expand Down
6 changes: 3 additions & 3 deletions src/routes/mod.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use axum::Router;
use aide::axum::ApiRouter;

mod request;
mod response;
mod system;

pub fn handler() -> Router {
Router::new()
pub fn handler() -> ApiRouter {
ApiRouter::new()
.merge(system::handler())
.merge(request::handler())
.merge(response::handler())
Expand Down
27 changes: 17 additions & 10 deletions src/routes/request.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
use aide::axum::{
routing::{head, post},
ApiRouter,
};
use axum::{
extract::Path,
http::{Method, StatusCode},
routing::{head, post},
Extension, Json, Router,
Extension,
};
use axum_jsonschema::Json;
use redis::{aio::ConnectionManager, AsyncCommands};
use schemars::JsonSchema;
use tower_http::cors::{AllowHeaders, Any, CorsLayer};
use uuid::Uuid;

Expand All @@ -14,21 +19,22 @@ use crate::utils::{

const REQ_PREFIX: &str = "req:";

#[derive(Debug, serde::Serialize)]
struct CustomResponse {
#[derive(Debug, serde::Serialize, JsonSchema)]
struct RequestCreatedPayload {
/// The unique identifier for the request
request_id: Uuid,
}

pub fn handler() -> Router {
pub fn handler() -> ApiRouter {
let cors = CorsLayer::new()
.allow_origin(Any)
.allow_headers(AllowHeaders::any())
.allow_methods([Method::POST, Method::HEAD]);

// You must chain the routes to the same Router instance
Router::new()
.route("/request", post(insert_request))
.route("/request/:request_id", head(has_request).get(get_request))
ApiRouter::new()
.api_route("/request", post(insert_request))
.api_route("/request/:request_id", head(has_request).get(get_request))
.layer(cors) // Apply the CORS layer to all routes
}

Expand Down Expand Up @@ -79,10 +85,11 @@ async fn get_request(
})
}

/// Create a new request
async fn insert_request(
Extension(mut redis): Extension<ConnectionManager>,
Json(request): Json<RequestPayload>,
) -> Result<Json<CustomResponse>, StatusCode> {
) -> Result<Json<RequestCreatedPayload>, StatusCode> {
let request_id = Uuid::new_v4();

tracing::info!("{}", format!("Processing /request: {request_id}"));
Expand Down Expand Up @@ -112,5 +119,5 @@ async fn insert_request(
format!("Successfully processed /request: {request_id}")
);

Ok(Json(CustomResponse { request_id }))
Ok(Json(RequestCreatedPayload { request_id }))
}
12 changes: 7 additions & 5 deletions src/routes/response.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
use std::str::FromStr;

use aide::axum::{routing::get, ApiRouter};
use axum::{
extract::Path,
http::{Method, StatusCode},
routing::get,
Extension, Json, Router,
Extension,
};
use axum_jsonschema::Json;
use redis::{aio::ConnectionManager, AsyncCommands};
use schemars::JsonSchema;
use std::str;
use tower_http::cors::{AllowHeaders, Any, CorsLayer};
use uuid::Uuid;
Expand All @@ -17,19 +19,19 @@ use crate::utils::{

const RES_PREFIX: &str = "res:";

#[derive(Debug, serde::Deserialize, serde::Serialize)]
#[derive(Debug, serde::Deserialize, serde::Serialize, JsonSchema)]
struct Response {
status: RequestStatus,
response: Option<RequestPayload>,
}

pub fn handler() -> Router {
pub fn handler() -> ApiRouter {
let cors = CorsLayer::new()
.allow_origin(Any)
.allow_headers(AllowHeaders::any())
.allow_methods([Method::GET, Method::PUT]); //TODO: PUT is required by the simulator but should not be included

Router::new().route(
ApiRouter::new().api_route(
"/response/:request_id",
get(get_response).put(insert_response).layer(cors),
)
Expand Down
21 changes: 18 additions & 3 deletions src/routes/system.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
use axum::{routing::get, Json, Router};
use aide::{axum::ApiRouter, openapi::OpenApi, scalar::Scalar};
use axum::{routing::get, Extension};
use axum_jsonschema::Json;

pub fn handler() -> Router {
Router::new().route("/", get(get_info))
pub fn handler() -> ApiRouter {
let scalar = Scalar::new("/openapi.json").with_title("Wallet Bridge Docs");

ApiRouter::new()
.route("/", get(get_info))
.route("/openapi.json", get(api_schema))
.route("/docs", scalar.axum_route())
}

#[derive(Debug, serde::Serialize)]
Expand All @@ -17,11 +24,14 @@ pub struct RootResponse {
pub repo_url: String,
/// Application version
pub version: AppVersion,
/// Documentation URL
pub docs_url: String,
}

#[allow(clippy::unused_async)]
async fn get_info() -> Json<RootResponse> {
Json(RootResponse {
docs_url: "/docs".to_string(),
repo_url: "https://github.com/worldcoin/wallet-bridge".to_string(),
version: AppVersion {
semver: env!("CARGO_PKG_VERSION").to_string(),
Expand All @@ -30,3 +40,8 @@ async fn get_info() -> Json<RootResponse> {
},
})
}

#[allow(clippy::unused_async)]
async fn api_schema(Extension(openapi): Extension<OpenApi>) -> Json<OpenApi> {
Json(openapi)
}
31 changes: 27 additions & 4 deletions src/server.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,44 @@
use std::{env, net::SocketAddr};

use axum::{Extension, Server};
use aide::openapi::{Info, License, OpenApi};
use axum::Extension;
use redis::aio::ConnectionManager;
use tokio::net::TcpListener;

use crate::routes;

pub async fn start(redis: ConnectionManager) {
let app = routes::handler().layer(Extension(redis));
let mut openapi = OpenApi {
info: Info {
title: "Wallet Bridge".to_string(),
summary: Some(
"An end-to-end encrypted bridge for communicating with World App.".to_string(),
),
license: Some(License {
name: "MIT".to_string(),
identifier: Some("MIT".to_string()),
..Default::default()
}),
..Default::default()
},
..Default::default()
};

let app = routes::handler()
.finish_api(&mut openapi)
.layer(Extension(redis))
.layer(Extension(openapi));

let address = SocketAddr::from((
[0, 0, 0, 0],
env::var("PORT").map_or(8000, |p| p.parse().unwrap()),
));
let listener = TcpListener::bind(&address)
.await
.expect("Failed to bind address");

println!("🪩 World Bridge started on http://{address}");
Server::bind(&address)
.serve(app.into_make_service())
axum::serve(listener, app.into_make_service())
.await
.expect("Failed to start server");
}
12 changes: 9 additions & 3 deletions src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@ use std::{fmt::Display, str::FromStr};

use axum::http::StatusCode;
use redis::RedisError;
use schemars::JsonSchema;

pub const EXPIRE_AFTER_SECONDS: usize = 180;
pub const EXPIRE_AFTER_SECONDS: u64 = 180;
pub const REQ_STATUS_PREFIX: &str = "req:status:";

#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum RequestStatus {
/// The request has been initiated by the client
Initialized,
/// The request has been retrieved by World App
Retrieved,
/// The request has received a response from World App
Completed,
}

Expand All @@ -37,9 +41,11 @@ impl FromStr for RequestStatus {
}
}

#[derive(Debug, serde::Deserialize, serde::Serialize)]
#[derive(Debug, serde::Deserialize, serde::Serialize, JsonSchema)]
pub struct RequestPayload {
/// The initialization vector for the encrypted payload
iv: String,
/// The encrypted payload
payload: String,
}

Expand Down

0 comments on commit cd6861f

Please sign in to comment.