From bc3469fa8871e8bec1509db9d7085f4c726483bf Mon Sep 17 00:00:00 2001 From: roaming97 Date: Thu, 17 Apr 2025 17:30:04 -0600 Subject: [PATCH] Write OpenAPI documentation --- Cargo.lock | 209 +++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 3 +- TODO.md | 7 +- src/channel.rs | 3 +- src/comment.rs | 3 +- src/doc.rs | 45 +++++++++ src/main.rs | 15 ++- src/routes/channel.rs | 35 ++++++- src/routes/comment.rs | 4 +- src/routes/mod.rs | 4 +- src/routes/video.rs | 35 +++---- src/video.rs | 3 +- 12 files changed, 323 insertions(+), 43 deletions(-) create mode 100755 src/doc.rs diff --git a/Cargo.lock b/Cargo.lock index 3f2b734..419bd2b 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -49,7 +49,17 @@ dependencies = [ "tracing", "tracing-subscriber", "url", - "uuid", + "utoipa", + "utoipa-swagger-ui", +] + +[[package]] +name = "arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +dependencies = [ + "derive_arbitrary", ] [[package]] @@ -178,6 +188,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + [[package]] name = "byteorder" version = "1.5.0" @@ -244,6 +260,15 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-queue" version = "0.3.12" @@ -280,6 +305,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "digest" version = "0.10.7" @@ -362,6 +398,16 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "flate2" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "flume" version = "0.11.1" @@ -792,6 +838,7 @@ checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", "hashbrown", + "serde", ] [[package]] @@ -863,6 +910,12 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "lockfree-object-pool" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" + [[package]] name = "log" version = "0.4.27" @@ -897,6 +950,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "miniz_oxide" version = "0.8.8" @@ -1199,6 +1262,40 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rust-embed" +version = "8.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5fbc0ee50fcb99af7cebb442e5df7b5b45e9460ffa3f8f549cd26b862bec49d" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bf418c9a2e3f6663ca38b8a7134cc2c2167c9d69688860e8961e3faa731702e" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d55b95147fe01265d06b3955db798bdaed52e60e2211c41137701b3aba8e21" +dependencies = [ + "sha2", + "walkdir", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -1230,6 +1327,15 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[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" @@ -1365,6 +1471,12 @@ dependencies = [ "rand_core", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "slab" version = "0.4.9" @@ -1886,6 +1998,12 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + [[package]] name = "unicode-bidi" version = "0.3.18" @@ -1937,12 +2055,45 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] -name = "uuid" -version = "1.16.0" +name = "utoipa" +version = "5.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" +checksum = "435c6f69ef38c9017b4b4eea965dfb91e71e53d869e896db40d1cf2441dd75c0" dependencies = [ - "getrandom 0.3.2", + "indexmap", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-gen" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77d306bc75294fd52f3e99b13ece67c02c1a2789190a6f31d32f736624326f7" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn", +] + +[[package]] +name = "utoipa-swagger-ui" +version = "9.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29519b3c485df6b13f4478ac909a491387e9ef70204487c3b64b53749aec0be" +dependencies = [ + "axum", + "base64", + "mime_guess", + "regex", + "rust-embed", + "serde", + "serde_json", + "url", + "utoipa", + "zip", ] [[package]] @@ -1963,6 +2114,16 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[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" @@ -2010,6 +2171,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -2286,3 +2456,32 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zip" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dcb24d0152526ae49b9b96c1dcf71850ca1e0b882e4e28ed898a93c41334744" +dependencies = [ + "arbitrary", + "crc32fast", + "crossbeam-utils", + "flate2", + "indexmap", + "memchr", + "zopfli", +] + +[[package]] +name = "zopfli" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" +dependencies = [ + "bumpalo", + "crc32fast", + "lockfree-object-pool", + "log", + "once_cell", + "simd-adler32", +] diff --git a/Cargo.toml b/Cargo.toml index f04b459..9c74628 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,4 +22,5 @@ toml = "0.8.20" tracing = "0.1.41" tracing-subscriber = "0.3.19" url = "2.5.4" -uuid = { version = "1.16.0", features = ["v4"] } +utoipa = { version = "5.3.1", features = ["axum_extras"] } +utoipa-swagger-ui = { version = "9.0.1", features = ["axum"] } diff --git a/TODO.md b/TODO.md index 55a515e..176d1fc 100755 --- a/TODO.md +++ b/TODO.md @@ -4,5 +4,10 @@ Tasks that have been completed in commits before this one have been omitted from - [x] /video PUT route - [x] /channel PUT route -- [ ] OpenAPI documentation +- [x] OpenAPI documentation + +## Planned features + - [ ] Video versioning +- [ ] Subtitles support +- [ ] Cookies support diff --git a/src/channel.rs b/src/channel.rs index c824379..f8a538e 100755 --- a/src/channel.rs +++ b/src/channel.rs @@ -6,6 +6,7 @@ use thiserror::Error; use tokio::{fs, process::Command}; use tracing::{error, info, warn}; use url::Url; +use utoipa::ToSchema; use crate::string::ToUnquotedString; @@ -29,7 +30,7 @@ pub enum ChannelError { JsonKey(String), } -#[derive(Serialize, Deserialize, Clone)] +#[derive(Serialize, Deserialize, Clone, ToSchema)] pub struct Channel { pub id: i64, pub url: String, diff --git a/src/comment.rs b/src/comment.rs index 9e7a299..4cf341d 100755 --- a/src/comment.rs +++ b/src/comment.rs @@ -5,6 +5,7 @@ use serde_json::Value; use thiserror::Error; use tokio::fs; use tracing::{error, warn}; +use utoipa::ToSchema; use crate::string::ToUnquotedString; @@ -19,7 +20,7 @@ pub enum CommentsError { } #[allow(clippy::struct_excessive_bools)] -#[derive(Serialize, Deserialize, Clone)] +#[derive(Serialize, Deserialize, Clone, ToSchema)] pub struct Comment { pub id: String, pub video_id: String, diff --git a/src/doc.rs b/src/doc.rs new file mode 100755 index 0000000..a11ea55 --- /dev/null +++ b/src/doc.rs @@ -0,0 +1,45 @@ +use utoipa::OpenApi; +use utoipa_swagger_ui::SwaggerUi; + +// Because apparently the OpenApi macro can't import these on its own +// if the routes are not in the same location as the router +use crate::routes::{ + __path_index, + channel::{ + __path_delete_channel, __path_get_channel, __path_get_channel_videos, __path_list_channels, + __path_update_channel, __path_upload_channel, + }, + comment::__path_get_video_comments, + video::{ + __path_delete_video, __path_get_video, __path_list_videos, __path_update_video, + __path_upload_video, + }, +}; + +#[derive(OpenApi)] +#[openapi( + paths( + index, + list_channels, + list_videos, + upload_video, + upload_channel, + update_channel, + update_video, + delete_channel, + delete_video, + get_video, + get_channel, + get_video_comments, + get_channel_videos + ), + info( + title = "Almond API", + description = "Interface to archive YouTube videos." + ) +)] +struct ApiDoc; + +pub fn docs_router() -> SwaggerUi { + SwaggerUi::new("/docs").url("/api-doc/openapi.json", ApiDoc::openapi()) +} diff --git a/src/main.rs b/src/main.rs index b5f8aa5..6f14214 100755 --- a/src/main.rs +++ b/src/main.rs @@ -4,13 +4,18 @@ use axum::{ Router, routing::{delete, get, post, put}, }; +use doc::docs_router; use instance::Instance; + use routes::{ - channel::{delete_channel, get_channel, list_channels, update_channel, upload_channel}, + channel::{ + delete_channel, get_channel, get_channel_videos, list_channels, update_channel, + upload_channel, + }, comment::get_video_comments, index, middleware::auth, - video::{delete_video, get_channel_videos, get_video, list_videos, update_video, upload_video}, + video::{delete_video, get_video, list_videos, update_video, upload_video}, }; use tokio::signal; use tracing::info; @@ -18,6 +23,7 @@ use tracing::info; mod channel; mod comment; mod config; +mod doc; mod instance; mod routes; mod string; @@ -60,7 +66,10 @@ async fn main() -> Result<(), Box> { .route("/channel/{id}", delete(delete_channel)) .route_layer(axum::middleware::from_fn_with_state(instance.clone(), auth)); - let almond = router.merge(protected).with_state(instance); + let almond = router + .merge(protected) + .merge(docs_router()) + .with_state(instance); let listener = tokio::net::TcpListener::bind(address).await?; diff --git a/src/routes/channel.rs b/src/routes/channel.rs index 48b6208..c928268 100755 --- a/src/routes/channel.rs +++ b/src/routes/channel.rs @@ -6,19 +6,21 @@ use axum::{ use serde::{Deserialize, Serialize}; use tracing::{error, info}; use url::Url; +use utoipa::ToSchema; use crate::{ channel::{Channel, ChannelError}, instance::Instance, url::is_youtube_url, + video::Video, }; -#[derive(Deserialize)] +#[derive(Deserialize, ToSchema)] pub struct ListChannelsQuery { page: Option, } -#[derive(Serialize)] +#[derive(Serialize, ToSchema)] pub struct ListChannelsResponse { channels: Vec, page: usize, @@ -28,6 +30,7 @@ pub struct ListChannelsResponse { } /// Retrieve video list as JSON (paged) +#[utoipa::path(get, path = "/channel", request_body = ListChannelsQuery, params(("page" = Option, Query)), tags = ["channel"], responses((status = 200, body = ListChannelsResponse), (status = 500, description = "Error fetching channels from database")))] pub async fn list_channels( State(state): State, Query(query): Query, @@ -64,6 +67,7 @@ pub async fn list_channels( } /// Get a single channel from the database by its ID +#[utoipa::path(get, path = "/channel/{id}", tags = ["channel"], responses((status = 200, body = Channel), (status = 404, description = "Not found"), (status = 500, description = "Failed to get channel from database")))] pub async fn get_channel( State(state): State, Path(id): Path, @@ -76,12 +80,31 @@ pub async fn get_channel( }) } -#[derive(Deserialize)] +/// Get all videos belonging to a channel ID from the database +#[utoipa::path(get, path = "/channel/{id}/videos", tags = ["channel"], responses((status = 200, body = Vec