Skip to content

Commit

Permalink
start OpenAPI docs with utoipa and Scalar
Browse files Browse the repository at this point in the history
  • Loading branch information
RubberDuckShobe committed Nov 2, 2024
1 parent 9bf705b commit 9d27f55
Show file tree
Hide file tree
Showing 8 changed files with 114 additions and 31 deletions.
66 changes: 59 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
[package]
name = "wavebreaker"
authors = ["m1nt_ (Rubber Duck Shobe)"]
description = "A custom, open-source server for Audiosurf 1"
version = "0.1.0"
license = "MIT"
repository = "https://github.com/AudiosurfResearch/wavebreaker-rs"
edition = "2021"

Expand Down Expand Up @@ -29,7 +31,7 @@ tower-http = { version = "0.6", features = ["fs", "trace"] }
toml = "0.8"
validator = { version = "0.18", features = ["derive"] }
axum-valid = "0.20.0"
axum-extra = { version = "0.9.4", features = ["cookie", "form", "typed-header"] }
axum-extra = { version = "0.9.4", features = ["form", "typed-header"] }
regex = "1.10"
musicbrainz_rs = "0.5.0"
clap = { version = "4.5", features = ["derive"] }
Expand All @@ -45,3 +47,6 @@ fred = { version = "9.3.0", features = ["i-sorted-sets"] }
tower-sessions = "0.13.0"
rmp-serde = "1.3.0"
thiserror = "1.0"
utoipa = { version = "5.1.3", features = ["axum_extras", "time"] }
utoipa-axum = "0.1.2"
utoipa-scalar = { version = "0.2.0", features = ["axum"] }
12 changes: 6 additions & 6 deletions src/api/auth.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
use anyhow::anyhow;
use axum::{
extract::{RawQuery, State},
http::{response, StatusCode},
http::StatusCode,
response::Redirect,
routing::get,
Json, Router,
Json,
};
use axum_extra::{extract::CookieJar, headers::Cookie};
use diesel_async::RunQueryDsl;
use jsonwebtoken::{encode, Header};
use tracing::info;
use utoipa_axum::router::OpenApiRouter;

use crate::{
models::players::Player,
Expand All @@ -20,8 +20,8 @@ use crate::{
AppState,
};

pub fn routes() -> Router<AppState> {
Router::new()
pub fn routes() -> OpenApiRouter<AppState> {
OpenApiRouter::new()
.route("/return", get(auth_return))
.route("/login", get(auth_login))
}
Expand All @@ -33,7 +33,7 @@ async fn auth_login(State(state): State<AppState>) -> Result<Redirect, RouteErro
async fn auth_return(
State(state): State<AppState>,
RawQuery(query): RawQuery,
) -> Result<(Json<AuthBody>), RouteError> {
) -> Result<Json<AuthBody>, RouteError> {
let steamid64 = state
.steam_openid
.verify(&query.ok_or_else(|| anyhow!("No query string to verify!"))?)
Expand Down
28 changes: 23 additions & 5 deletions src/api/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use axum::{routing::get, Json, Router};
use axum::{Json, Router};
use serde::Serialize;
use utoipa::{openapi::OpenApi, OpenApi as OpenApiTrait, ToSchema};
use utoipa_axum::{router::OpenApiRouter, routes};

use crate::{
util::{errors::RouteError, radio::get_radio_songs},
Expand All @@ -11,22 +13,38 @@ mod players;
mod rivals;
mod songs;

pub fn routes() -> Router<AppState> {
Router::new()
.route("/healthCheck", get(health_check))
#[derive(OpenApiTrait)]
#[openapi(servers((url = "/api")), security(
(),
("token_jwt" = [])
))]
pub struct ApiDoc;

pub fn routes() -> (Router<AppState>, OpenApi) {
OpenApiRouter::with_openapi(ApiDoc::openapi())
.routes(routes!(health_check))
.nest("/songs", songs::routes())
.nest("/players", players::routes())
.nest("/auth", auth::routes())
.nest("/rivals", rivals::routes())
.split_for_parts()
}

#[derive(Serialize)]
#[derive(Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
struct HealthCheck {
status: &'static str,
radio_status: String,
}

/// Get health of the API.
#[utoipa::path(
method(get),
path = "/healthCheck",
responses(
(status = OK, description = "Success", body = HealthCheck, content_type = "application/json")
)
)]
async fn health_check() -> Result<Json<HealthCheck>, RouteError> {
let radio_status: String = get_radio_songs().map_or_else(
|_| "error".to_owned(),
Expand Down
7 changes: 4 additions & 3 deletions src/api/players.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
use axum::{
extract::{Path, State},
routing::get,
Json, Router,
Json,
};
use diesel::prelude::*;
use diesel_async::RunQueryDsl;
use serde::Serialize;
use utoipa_axum::router::OpenApiRouter;

use crate::{
models::players::{Player, PlayerPublic},
util::{errors::RouteError, jwt::Claims},
AppState,
};

pub fn routes() -> Router<AppState> {
Router::new()
pub fn routes() -> OpenApiRouter<AppState> {
OpenApiRouter::new()
.route("/:id", get(get_player))
.route("/self", get(get_self))
}
Expand Down
7 changes: 4 additions & 3 deletions src/api/rivals.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
use axum::{extract::State, routing::get, Json, Router};
use axum::{extract::State, routing::get, Json};
use diesel::prelude::*;
use diesel_async::RunQueryDsl;
use serde::Serialize;
use utoipa_axum::router::OpenApiRouter;

use crate::{
models::{players::Player, rivalries::RivalryView},
util::{errors::RouteError, jwt::Claims},
AppState,
};

pub fn routes() -> Router<AppState> {
Router::new().route("/own", get(get_own_rivals))
pub fn routes() -> OpenApiRouter<AppState> {
OpenApiRouter::new().route("/own", get(get_own_rivals))
}

#[derive(Serialize)]
Expand Down
7 changes: 4 additions & 3 deletions src/api/songs.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
use axum::{
extract::{Path, Query, State},
routing::get,
Json, Router,
Json,
};
use diesel::prelude::*;
use diesel_async::RunQueryDsl;
use serde::{Deserialize, Serialize};
use utoipa_axum::router::OpenApiRouter;

use crate::{
models::{extra_song_info::ExtraSongInfo, songs::Song},
util::errors::RouteError,
AppState,
};

pub fn routes() -> Router<AppState> {
Router::new().route("/:id", get(get_song))
pub fn routes() -> OpenApiRouter<AppState> {
OpenApiRouter::new().route("/:id", get(get_song))
}

#[derive(Serialize)]
Expand Down
11 changes: 8 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ use std::{io::stdout, sync::Arc};
use anyhow::{anyhow, Context};
use axum::{
extract::{MatchedPath, Request},
Router,
routing::get,
Json, Router,
};
use clap::Parser;
use diesel::pg::Pg;
Expand All @@ -49,7 +50,8 @@ use tracing_appender::rolling::{RollingFileAppender, Rotation};
use tracing_subscriber::{
fmt::writer::MakeWriterExt, layer::SubscriberExt, util::SubscriberInitExt,
};
use util::session_store::RedisStore;
use util::{errors::RouteError, session_store::RedisStore};
use utoipa_scalar::{Scalar, Servable};

use crate::{
api::routes,
Expand Down Expand Up @@ -179,6 +181,8 @@ async fn init_state() -> anyhow::Result<AppState> {
}

fn make_router(state: AppState) -> Router {
let (api_router, openapi) = api::routes();

let session_store = RedisStore::new((*state.redis).clone());
//TODO: Make with_secure configurable
let session_layer = SessionManagerLayer::new(session_store)
Expand All @@ -189,7 +193,8 @@ fn make_router(state: AppState) -> Router {
.nest("/as_steamlogin", routes_steam())
.nest("//as_steamlogin", routes_steam_doubleslash()) // for that one edge case
.nest("/as", routes_as(&state.config.radio.cgr_location))
.nest("/api", routes())
.nest("/api", api_router)
.merge(Scalar::with_url("/scalar", openapi))
.layer(session_layer)
.layer(
// TAKEN FROM: https://github.com/tokio-rs/axum/blob/d1fb14ead1063efe31ae3202e947ffd569875c0b/examples/error-handling/src/main.rs#L60-L77
Expand Down

0 comments on commit 9d27f55

Please sign in to comment.