From 0a058ba2064d323451462a79c71580dea7d8ec8c Mon Sep 17 00:00:00 2001 From: FxQnLr Date: Mon, 4 Mar 2024 21:37:55 +0100 Subject: Closes #19. Added OpenApi through `utoipa` --- src/db.rs | 11 +++++++ src/error.rs | 3 +- src/main.rs | 89 +++++++++++++++++++++++++++++++++++++++----------- src/routes.rs | 2 +- src/routes/device.rs | 91 +++++++++++++++++++++++++++++++++++++++++++--------- src/routes/start.rs | 16 +++++++-- 6 files changed, 172 insertions(+), 40 deletions(-) (limited to 'src') diff --git a/src/db.rs b/src/db.rs index 47e907d..a2b2009 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1,6 +1,7 @@ use serde::Serialize; use sqlx::{PgPool, postgres::PgPoolOptions, types::{ipnetwork::IpNetwork, mac_address::MacAddress}}; use tracing::{debug, info}; +use utoipa::ToSchema; #[derive(Serialize, Debug)] pub struct Device { @@ -11,6 +12,16 @@ pub struct Device { pub times: Option> } +#[derive(ToSchema)] +#[schema(as = Device)] +pub struct DeviceSchema { + pub id: String, + pub mac: String, + pub broadcast_addr: String, + pub ip: String, + pub times: Option> +} + pub async fn init_db_pool(db_url: &str) -> PgPool { debug!("attempt to connect dbPool to '{}'", db_url); diff --git a/src/error.rs b/src/error.rs index 513b51b..006fcdb 100644 --- a/src/error.rs +++ b/src/error.rs @@ -5,10 +5,11 @@ use axum::response::{IntoResponse, Response}; use axum::Json; use mac_address::MacParseError; use serde_json::json; +use utoipa::ToSchema; use std::io; use tracing::error; -#[derive(Debug, thiserror::Error)] +#[derive(Debug, thiserror::Error, ToSchema)] pub enum Error { #[error("db: {source}")] Db { diff --git a/src/main.rs b/src/main.rs index d17984f..8978e58 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,21 +1,29 @@ -use crate::config::Config; -use crate::db::init_db_pool; -use crate::routes::device; -use crate::routes::start::start; -use crate::routes::status::status; -use crate::services::ping::StatusMap; -use axum::middleware::from_fn_with_state; -use axum::routing::{get, put}; -use axum::{routing::post, Router}; +use crate::{ + config::Config, + db::init_db_pool, + routes::{device, start, status}, + services::ping::{BroadcastCommand, StatusMap}, +}; +use axum::{ + middleware::from_fn_with_state, + routing::{get, post}, + Router, +}; use dashmap::DashMap; -use services::ping::BroadcastCommand; use sqlx::PgPool; -use std::env; -use std::sync::Arc; +use std::{env, sync::Arc}; use tokio::sync::broadcast::{channel, Sender}; use tracing::{info, level_filters::LevelFilter}; -use tracing_subscriber::fmt::time::UtcTime; -use tracing_subscriber::{fmt, prelude::*, EnvFilter}; +use tracing_subscriber::{ + fmt::{self, time::UtcTime}, + prelude::*, + EnvFilter, +}; +use utoipa::{ + openapi::security::{ApiKey, ApiKeyValue, SecurityScheme}, + Modify, OpenApi, +}; +use utoipa_swagger_ui::SwaggerUi; mod config; mod db; @@ -25,7 +33,47 @@ mod routes; mod services; mod wol; +#[derive(OpenApi)] +#[openapi( + paths( + start::start, + device::get, + device::get_path, + device::post, + device::put, + ), + components( + schemas( + start::Payload, + start::Response, + device::PutDevicePayload, + device::GetDevicePayload, + device::PostDevicePayload, + db::DeviceSchema, + ) + ), + modifiers(&SecurityAddon), + tags( + (name = "Webol", description = "Webol API") + ) +)] +struct ApiDoc; + +struct SecurityAddon; + +impl Modify for SecurityAddon { + fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) { + if let Some(components) = openapi.components.as_mut() { + components.add_security_scheme( + "api_key", + SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("Authorization"))), + ); + } + } +} + #[tokio::main] +#[allow(deprecated)] async fn main() -> color_eyre::eyre::Result<()> { color_eyre::install()?; @@ -67,12 +115,15 @@ async fn main() -> color_eyre::eyre::Result<()> { }; let app = Router::new() - .route("/start", post(start)) - .route("/device", get(device::get)) - .route("/device", put(device::put)) - .route("/device", post(device::post)) - .route("/status", get(status)) + .route("/start", post(start::start)) + .route( + "/device", + post(device::post).get(device::get).put(device::put), + ) + .route("/device/:id", get(device::get_path)) + .route("/status", get(status::status)) .route_layer(from_fn_with_state(shared_state.clone(), extractors::auth)) + .merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", ApiDoc::openapi())) .with_state(Arc::new(shared_state)); let addr = config.serveraddr; diff --git a/src/routes.rs b/src/routes.rs index d5ab0d6..a72f27b 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -1,3 +1,3 @@ pub mod start; pub mod device; -pub mod status; \ No newline at end of file +pub mod status; diff --git a/src/routes/device.rs b/src/routes/device.rs index d39d98e..d01d9f0 100644 --- a/src/routes/device.rs +++ b/src/routes/device.rs @@ -1,14 +1,25 @@ use crate::db::Device; use crate::error::Error; -use axum::extract::State; +use axum::extract::{Path, State}; use axum::Json; use mac_address::MacAddress; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; use serde_json::{json, Value}; use sqlx::types::ipnetwork::IpNetwork; -use std::{sync::Arc, str::FromStr}; +use std::{str::FromStr, sync::Arc}; use tracing::{debug, info}; +use utoipa::ToSchema; +#[utoipa::path( + get, + path = "/device", + request_body = GetDevicePayload, + responses( + (status = 200, description = "Get `Device` information", body = [Device]) + ), + security(("api_key" = [])) +)] +#[deprecated] pub async fn get( State(state): State>, Json(payload): Json, @@ -31,11 +42,53 @@ pub async fn get( Ok(Json(json!(device))) } -#[derive(Deserialize)] +#[utoipa::path( + get, + path = "/device/{id}", + responses( + (status = 200, description = "Get `Device` information", body = [Device]) + ), + params( + ("id" = String, Path, description = "Device id") + ), + security(("api_key" = [])) +)] +pub async fn get_path( + State(state): State>, + Path(path): Path, +) -> Result, Error> { + info!("get device from path {}", path); + let device = sqlx::query_as!( + Device, + r#" + SELECT id, mac, broadcast_addr, ip, times + FROM devices + WHERE id = $1; + "#, + path + ) + .fetch_one(&state.db) + .await?; + + debug!("got device {:?}", device); + + Ok(Json(json!(device))) +} + +#[derive(Deserialize, ToSchema)] pub struct GetDevicePayload { id: String, } +#[utoipa::path( + put, + path = "/device", + request_body = PutDevicePayload, + responses( + (status = 200, description = "List matching todos by query", body = [DeviceSchema]) + ), + security(("api_key" = [])) +)] pub async fn put( State(state): State>, Json(payload): Json, @@ -44,26 +97,28 @@ pub async fn put( "add device {} ({}, {}, {})", payload.id, payload.mac, payload.broadcast_addr, payload.ip ); - + let ip = IpNetwork::from_str(&payload.ip)?; let mac = MacAddress::from_str(&payload.mac)?; - sqlx::query!( + let device = sqlx::query_as!( + Device, r#" INSERT INTO devices (id, mac, broadcast_addr, ip) - VALUES ($1, $2, $3, $4); + VALUES ($1, $2, $3, $4) + RETURNING id, mac, broadcast_addr, ip, times; "#, payload.id, mac, payload.broadcast_addr, ip ) - .execute(&state.db) + .fetch_one(&state.db) .await?; - Ok(Json(json!(PutDeviceResponse { success: true }))) + Ok(Json(json!(device))) } -#[derive(Deserialize)] +#[derive(Deserialize, ToSchema)] pub struct PutDevicePayload { id: String, mac: String, @@ -71,11 +126,15 @@ pub struct PutDevicePayload { ip: String, } -#[derive(Serialize)] -pub struct PutDeviceResponse { - success: bool, -} - +#[utoipa::path( + post, + path = "/device", + request_body = PostDevicePayload, + responses( + (status = 200, description = "List matching todos by query", body = [DeviceSchema]) + ), + security(("api_key" = [])) +)] pub async fn post( State(state): State>, Json(payload): Json, @@ -104,7 +163,7 @@ pub async fn post( Ok(Json(json!(device))) } -#[derive(Deserialize)] +#[derive(Deserialize, ToSchema)] pub struct PostDevicePayload { id: String, mac: String, diff --git a/src/routes/start.rs b/src/routes/start.rs index d4c0802..ef6e8f2 100644 --- a/src/routes/start.rs +++ b/src/routes/start.rs @@ -6,10 +6,20 @@ use axum::extract::State; use axum::Json; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; +use utoipa::ToSchema; use std::sync::Arc; use tracing::{debug, info}; use uuid::Uuid; +#[utoipa::path( + post, + path = "/start", + request_body = Payload, + responses( + (status = 200, description = "List matching todos by query", body = [Response]) + ), + security(("api_key" = [])) +)] pub async fn start( State(state): State>, Json(payload): Json, @@ -88,14 +98,14 @@ fn setup_ping(state: Arc, device: Device) -> String { uuid_ret } -#[derive(Deserialize)] +#[derive(Deserialize, ToSchema)] pub struct Payload { id: String, ping: Option, } -#[derive(Serialize)] -struct Response { +#[derive(Serialize, ToSchema)] +pub struct Response { id: String, boot: bool, uuid: Option, -- cgit v1.2.3