diff --git a/Cargo.lock b/Cargo.lock index a5355d6..8b6ca2a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -735,6 +735,8 @@ dependencies = [ "serde_json", "sqlx", "tokio", + "utoipa", + "utoipa-swagger-ui", "uuid", ] @@ -1156,6 +1158,7 @@ checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" dependencies = [ "equivalent", "hashbrown 0.14.2", + "serde", ] [[package]] @@ -1318,6 +1321,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1593,6 +1606,30 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.69" @@ -1710,6 +1747,40 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rust-embed" +version = "8.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb78f46d0066053d16d4ca7b898e9343bc3530f71c61d5ad84cd404ada068745" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91ac2a3c6c0520a3fb3dd89321177c3c692937c4eb21893378219da10c44fc8" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn 2.0.38", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f69089032567ffff4eada41c573fc43ff466c7db7c5688b2e7969584345581" +dependencies = [ + "sha2", + "walkdir", +] + [[package]] name = "rust-ini" version = "0.18.0" @@ -1754,6 +1825,15 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -2379,6 +2459,15 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.13" @@ -2429,6 +2518,46 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utoipa" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "272ebdfbc99111033031d2f10e018836056e4d2c8e2acda76450ec7974269fa7" +dependencies = [ + "indexmap 2.0.2", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-gen" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3c9f4d08338c1bfa70dde39412a040a884c6f318b3d09aaaf3437a1e52027fc" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "utoipa-swagger-ui" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b39868d43c011961e04b41623e050aedf2cc93652562ff7935ce0f819aaf2da" +dependencies = [ + "actix-web", + "mime_guess", + "regex", + "rust-embed", + "serde", + "serde_json", + "utoipa", + "zip", +] + [[package]] name = "uuid" version = "1.6.1" @@ -2451,6 +2580,16 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -2517,6 +2656,37 @@ version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-core" version = "0.51.1" @@ -2627,6 +2797,18 @@ version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "byteorder", + "crc32fast", + "crossbeam-utils", + "flate2", +] + [[package]] name = "zstd" version = "0.12.4" diff --git a/emgauwa-core/Cargo.toml b/emgauwa-core/Cargo.toml index 6400d42..1bb4ef4 100644 --- a/emgauwa-core/Cargo.toml +++ b/emgauwa-core/Cargo.toml @@ -12,6 +12,9 @@ actix-web = "4.4" actix-web-actors = "4.2" actix-cors = "0.7" +utoipa = "4.2" +utoipa-swagger-ui = { version = "6.0", features = ["actix-web", "debug-embed"] } + log = "0.4" chrono = { version = "0.4", features = ["serde"] } diff --git a/emgauwa-core/build.rs b/emgauwa-core/build.rs index 41015be..443df4c 100644 --- a/emgauwa-core/build.rs +++ b/emgauwa-core/build.rs @@ -1,3 +1,17 @@ +use std::process::{exit, Command}; + fn main() { - println!("cargo:rustc-env=DATABASE_URL=sqlite://emgauwa-core.sqlite") + println!("cargo:rustc-env=DATABASE_URL=sqlite://emgauwa-core.sqlite"); + + println!("cargo:rerun-if-changed=../api.v1.yaml"); + let output = Command::new("sh") + .arg("-c") + .arg("yq . < ../api.v1.yaml > $OUT_DIR/api.v1.json") + .output() + .expect("Failed to convert api documentation to json"); + + if !output.status.success() { + eprintln!("Error: {}", String::from_utf8_lossy(&output.stderr)); + exit(1); + } } diff --git a/emgauwa-core/src/handlers/v1/controllers.rs b/emgauwa-core/src/handlers/v1/controllers.rs index 1fdd6cd..eb1b870 100644 --- a/emgauwa-core/src/handlers/v1/controllers.rs +++ b/emgauwa-core/src/handlers/v1/controllers.rs @@ -9,7 +9,7 @@ use sqlx::{Pool, Sqlite}; use crate::app_state; use crate::app_state::AppState; -#[get("/api/v1/controllers")] +#[get("/controllers")] pub async fn index(pool: web::Data<Pool<Sqlite>>) -> Result<HttpResponse, EmgauwaError> { let mut pool_conn = pool.acquire().await?; @@ -20,7 +20,7 @@ pub async fn index(pool: web::Data<Pool<Sqlite>>) -> Result<HttpResponse, Emgauw Ok(HttpResponse::Ok().json(controllers)) } -#[get("/api/v1/controllers/{controller_id}")] +#[get("/controllers/{controller_id}")] pub async fn show( pool: web::Data<Pool<Sqlite>>, path: web::Path<(String,)>, @@ -38,7 +38,7 @@ pub async fn show( Ok(HttpResponse::Ok().json(return_controller)) } -#[put("/api/v1/controllers/{controller_id}")] +#[put("/controllers/{controller_id}")] pub async fn update( pool: web::Data<Pool<Sqlite>>, app_state: web::Data<Addr<AppState>>, @@ -70,7 +70,7 @@ pub async fn update( Ok(HttpResponse::Ok().json(return_controller)) } -#[delete("/api/v1/controllers/{controller_id}")] +#[delete("/controllers/{controller_id}")] pub async fn delete( pool: web::Data<Pool<Sqlite>>, app_state: web::Data<Addr<AppState>>, diff --git a/emgauwa-core/src/handlers/v1/relays.rs b/emgauwa-core/src/handlers/v1/relays.rs index 6f2ddfa..2d519d4 100644 --- a/emgauwa-core/src/handlers/v1/relays.rs +++ b/emgauwa-core/src/handlers/v1/relays.rs @@ -10,7 +10,7 @@ use sqlx::{Pool, Sqlite}; use crate::app_state; use crate::app_state::AppState; -#[get("/api/v1/relays")] +#[get("/relays")] pub async fn index(pool: web::Data<Pool<Sqlite>>) -> Result<HttpResponse, EmgauwaError> { let mut pool_conn = pool.acquire().await?; @@ -21,7 +21,7 @@ pub async fn index(pool: web::Data<Pool<Sqlite>>) -> Result<HttpResponse, Emgauw Ok(HttpResponse::Ok().json(relays)) } -#[get("/api/v1/relays/tag/{tag}")] +#[get("/relays/tag/{tag}")] pub async fn tagged( pool: web::Data<Pool<Sqlite>>, path: web::Path<(String,)>, @@ -39,7 +39,7 @@ pub async fn tagged( Ok(HttpResponse::Ok().json(relays)) } -#[get("/api/v1/controllers/{controller_id}/relays")] +#[get("/controllers/{controller_id}/relays")] pub async fn index_for_controller( pool: web::Data<Pool<Sqlite>>, path: web::Path<(String,)>, @@ -59,7 +59,7 @@ pub async fn index_for_controller( Ok(HttpResponse::Ok().json(relays)) } -#[get("/api/v1/controllers/{controller_id}/relays/{relay_num}")] +#[get("/controllers/{controller_id}/relays/{relay_num}")] pub async fn show_for_controller( pool: web::Data<Pool<Sqlite>>, path: web::Path<(String, i64)>, @@ -81,7 +81,7 @@ pub async fn show_for_controller( Ok(HttpResponse::Ok().json(return_relay)) } -#[put("/api/v1/controllers/{controller_id}/relays/{relay_num}")] +#[put("/controllers/{controller_id}/relays/{relay_num}")] pub async fn update_for_controller( pool: web::Data<Pool<Sqlite>>, app_state: web::Data<Addr<AppState>>, diff --git a/emgauwa-core/src/handlers/v1/schedules.rs b/emgauwa-core/src/handlers/v1/schedules.rs index a1d1583..1130e6b 100644 --- a/emgauwa-core/src/handlers/v1/schedules.rs +++ b/emgauwa-core/src/handlers/v1/schedules.rs @@ -6,7 +6,7 @@ use emgauwa_lib::types::{RequestCreateSchedule, RequestUpdateSchedule, ScheduleU use sqlx::pool::PoolConnection; use sqlx::{Pool, Sqlite}; -#[get("/api/v1/schedules")] +#[get("/schedules")] pub async fn index(pool: web::Data<Pool<Sqlite>>) -> Result<HttpResponse, EmgauwaError> { let mut pool_conn = pool.acquire().await?; @@ -16,7 +16,7 @@ pub async fn index(pool: web::Data<Pool<Sqlite>>) -> Result<HttpResponse, Emgauw Ok(HttpResponse::Ok().json(schedules)) } -#[get("/api/v1/schedules/tag/{tag}")] +#[get("/schedules/tag/{tag}")] pub async fn tagged( pool: web::Data<Pool<Sqlite>>, path: web::Path<(String,)>, @@ -34,7 +34,7 @@ pub async fn tagged( Ok(HttpResponse::Ok().json(schedules)) } -#[get("/api/v1/schedules/{schedule_id}")] +#[get("/schedules/{schedule_id}")] pub async fn show( pool: web::Data<Pool<Sqlite>>, path: web::Path<(String,)>, @@ -52,7 +52,7 @@ pub async fn show( Ok(HttpResponse::Ok().json(return_schedule)) } -#[post("/api/v1/schedules")] +#[post("/schedules")] pub async fn add( pool: web::Data<Pool<Sqlite>>, data: web::Json<RequestCreateSchedule>, @@ -96,7 +96,7 @@ async fn add_list_single( Ok(new_schedule) } -#[post("/api/v1/schedules/list")] +#[post("/schedules/list")] pub async fn add_list( pool: web::Data<Pool<Sqlite>>, data: web::Json<Vec<RequestCreateSchedule>>, @@ -113,7 +113,7 @@ pub async fn add_list( Ok(HttpResponse::Created().json(schedules)) } -#[put("/api/v1/schedules/{schedule_id}")] +#[put("/schedules/{schedule_id}")] pub async fn update( pool: web::Data<Pool<Sqlite>>, path: web::Path<(String,)>, @@ -148,7 +148,7 @@ pub async fn update( Ok(HttpResponse::Ok().json(return_schedule)) } -#[delete("/api/v1/schedules/{schedule_id}")] +#[delete("/schedules/{schedule_id}")] pub async fn delete( pool: web::Data<Pool<Sqlite>>, path: web::Path<(String,)>, diff --git a/emgauwa-core/src/handlers/v1/tags.rs b/emgauwa-core/src/handlers/v1/tags.rs index 851bf98..5a42715 100644 --- a/emgauwa-core/src/handlers/v1/tags.rs +++ b/emgauwa-core/src/handlers/v1/tags.rs @@ -5,7 +5,7 @@ use emgauwa_lib::models::{FromDbModel, Tag}; use emgauwa_lib::types::RequestCreateTag; use sqlx::{Pool, Sqlite}; -#[get("/api/v1/tags")] +#[get("/tags")] pub async fn index(pool: web::Data<Pool<Sqlite>>) -> Result<HttpResponse, EmgauwaError> { let mut pool_conn = pool.acquire().await?; @@ -16,7 +16,7 @@ pub async fn index(pool: web::Data<Pool<Sqlite>>) -> Result<HttpResponse, Emgauw Ok(HttpResponse::Ok().json(tags)) } -#[get("/api/v1/tags/{tag_name}")] +#[get("/tags/{tag_name}")] pub async fn show( pool: web::Data<Pool<Sqlite>>, path: web::Path<(String,)>, @@ -33,7 +33,7 @@ pub async fn show( Ok(HttpResponse::Ok().json(return_tag)) } -#[delete("/api/v1/tags/{tag_name}")] +#[delete("/tags/{tag_name}")] pub async fn delete( pool: web::Data<Pool<Sqlite>>, path: web::Path<(String,)>, @@ -46,7 +46,7 @@ pub async fn delete( Ok(HttpResponse::Ok().json("tag got deleted")) } -#[post("/api/v1/tags")] +#[post("/tags")] pub async fn add( pool: web::Data<Pool<Sqlite>>, data: web::Json<RequestCreateTag>, diff --git a/emgauwa-core/src/handlers/v1/ws/mod.rs b/emgauwa-core/src/handlers/v1/ws/mod.rs index 23a545a..cda5a47 100644 --- a/emgauwa-core/src/handlers/v1/ws/mod.rs +++ b/emgauwa-core/src/handlers/v1/ws/mod.rs @@ -11,7 +11,7 @@ use crate::handlers::v1::ws::controllers::ControllerWs; pub mod controllers; -#[get("/api/v1/ws/controllers")] +#[get("/ws/controllers")] pub async fn ws_controllers( pool: web::Data<Pool<Sqlite>>, app_state: web::Data<Addr<AppState>>, diff --git a/emgauwa-core/src/main.rs b/emgauwa-core/src/main.rs index 1191da6..6bd47a3 100644 --- a/emgauwa-core/src/main.rs +++ b/emgauwa-core/src/main.rs @@ -7,6 +7,8 @@ use actix_web::{middleware, web, App, HttpServer}; use emgauwa_lib::db::DbController; use emgauwa_lib::errors::EmgauwaError; use emgauwa_lib::utils::{drop_privileges, init_logging}; +use serde_json::json; +use utoipa_swagger_ui::SwaggerUi; use crate::app_state::AppState; @@ -61,34 +63,52 @@ async fn main() -> Result<(), std::io::Error> { }), }; + let api_default = json!({ + "openapi": "3.0.0", + "info": { + "version": "0.0.0", + "title": "Failed to load API documentation", + } + }); + let api_v1_json = + serde_json::from_str(include_str!(concat!(env!("OUT_DIR"), "/api.v1.json"))) + .unwrap_or(api_default.clone()); + App::new() .wrap(cors) .wrap(middleware::Logger::default()) - .wrap(middleware::NormalizePath::new(TrailingSlash::Trim)) .app_data(web::JsonConfig::default().error_handler(handlers::json_error_handler)) .app_data(web::Data::new(pool.clone())) .app_data(web::Data::new(app_state.clone())) - .service(handlers::v1::controllers::index) - .service(handlers::v1::controllers::show) - .service(handlers::v1::controllers::update) - .service(handlers::v1::controllers::delete) - .service(handlers::v1::relays::index) - .service(handlers::v1::relays::tagged) - .service(handlers::v1::relays::index_for_controller) - .service(handlers::v1::relays::show_for_controller) - .service(handlers::v1::relays::update_for_controller) - .service(handlers::v1::schedules::index) - .service(handlers::v1::schedules::tagged) - .service(handlers::v1::schedules::show) - .service(handlers::v1::schedules::add) - .service(handlers::v1::schedules::add_list) - .service(handlers::v1::schedules::update) - .service(handlers::v1::schedules::delete) - .service(handlers::v1::tags::index) - .service(handlers::v1::tags::show) - .service(handlers::v1::tags::delete) - .service(handlers::v1::tags::add) - .service(handlers::v1::ws::ws_controllers) + .service( + SwaggerUi::new("/api/docs/{_:.*}") + .external_urls_from_iter_unchecked([("/api/v1.json", api_v1_json)]), + ) + .service( + web::scope("/api/v1") + .wrap(middleware::NormalizePath::new(TrailingSlash::Trim)) + .service(handlers::v1::controllers::index) + .service(handlers::v1::controllers::show) + .service(handlers::v1::controllers::update) + .service(handlers::v1::controllers::delete) + .service(handlers::v1::relays::index) + .service(handlers::v1::relays::tagged) + .service(handlers::v1::relays::index_for_controller) + .service(handlers::v1::relays::show_for_controller) + .service(handlers::v1::relays::update_for_controller) + .service(handlers::v1::schedules::index) + .service(handlers::v1::schedules::tagged) + .service(handlers::v1::schedules::show) + .service(handlers::v1::schedules::add) + .service(handlers::v1::schedules::add_list) + .service(handlers::v1::schedules::update) + .service(handlers::v1::schedules::delete) + .service(handlers::v1::tags::index) + .service(handlers::v1::tags::show) + .service(handlers::v1::tags::delete) + .service(handlers::v1::tags::add) + .service(handlers::v1::ws::ws_controllers), + ) }) .listen(listener)? .run()