diff --git a/Cargo.toml b/Cargo.toml index 3c96b1f..01211a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ homepage = { workspace = true } axum = { version = "0.7", optional = true, features = ["multipart"] } tower = { version = "0.5", optional = true } tower-http = { version = "0.5", optional = true, features = ["trace", "cors", "normalize-path"] } -mime = { version = "0.3.17", optional = true } +mime = { version = "0.3", optional = true } # Async tokio = { version = "1.39", optional = true, features = ["fs"] } tokio-util = { version = "0.7", optional = true, features = ["io"] } diff --git a/src/axum/app.rs b/src/axum/app.rs index 30e8633..97c5356 100644 --- a/src/axum/app.rs +++ b/src/axum/app.rs @@ -40,10 +40,13 @@ pub struct AppBuilder { } impl AppBuilder { + /// Creates a new app builder with default options. pub fn new() -> Self { Self::default() } + /// Creates the builder from the given router. + /// Only the routes and layers will be used. pub fn from_router(router: Router) -> Self { Self { router, @@ -51,11 +54,13 @@ impl AppBuilder { } } + /// Adds a route to the previously added routes pub fn route(mut self, route: Router) -> Self { self.router = self.router.merge(route); self } + /// Adds multiple routes to the previously added routes pub fn routes(mut self, routes: impl IntoIterator) -> Self { self.router = routes.into_iter().fold(self.router, Router::merge); self @@ -74,12 +79,14 @@ impl AppBuilder { self } + /// Sets the socket for the server. pub fn socket>(mut self, socket: impl Into<(IP, u16)>) -> Self { let (ip, port) = socket.into(); self.socket = Some((ip.into(), port)); self } + /// Sets the port for the server. pub fn port(mut self, port: u16) -> Self { self.socket = if let Some((ip, _)) = self.socket { Some((ip, port)) @@ -89,6 +96,7 @@ impl AppBuilder { self } + /// Sets the fallback handler. pub fn fallback(mut self, fallback: H) -> Self where H: Handler, @@ -98,16 +106,19 @@ impl AppBuilder { self } + /// Sets the cors layer. pub fn cors(mut self, cors: CorsLayer) -> Self { self.cors = Some(cors); self } + /// Sets the normalize path option. Default is true. pub fn normalize_path(mut self, normalize_path: bool) -> Self { self.normalize_path = Some(normalize_path); self } + /// Sets the trace layer. pub fn tracing(mut self, tracing: TraceLayer) -> Self { self.tracing = Some(tracing); self @@ -168,44 +179,37 @@ fn fmt_trace() -> Result<(), String> { #[cfg(test)] mod tests { - use axum::Router; - use super::*; + use axum::Router; + use std::time::Duration; + use tokio::time::sleep; - mod tokio_tests { - use std::time::Duration; + #[tokio::test] + async fn test_app_builder_serve() { + let handler = tokio::spawn(async { + AppBuilder::new().serve().await.unwrap(); + }); + sleep(Duration::from_millis(250)).await; + handler.abort(); + } - use tokio::time::sleep; - - use super::*; - - #[tokio::test] - async fn test_app_builder_serve() { - let handler = tokio::spawn(async { - AppBuilder::new().serve().await.unwrap(); - }); - sleep(Duration::from_millis(250)).await; - handler.abort(); - } - - #[tokio::test] - async fn test_app_builder_all() { - let handler = tokio::spawn(async { - AppBuilder::new() - .socket((Ipv4Addr::LOCALHOST, 8080)) - .routes([Router::new()]) - .fallback(|| async { "Fallback" }) - .cors(CorsLayer::new()) - .normalize_path(true) - .tracing(TraceLayer::new_for_http()) - .layer(TraceLayer::new_for_http()) - .serve() - .await - .unwrap(); - }); - sleep(Duration::from_millis(250)).await; - handler.abort(); - } + #[tokio::test] + async fn test_app_builder_all() { + let handler = tokio::spawn(async { + AppBuilder::new() + .socket((Ipv4Addr::LOCALHOST, 8080)) + .routes([Router::new()]) + .fallback(|| async { "Fallback" }) + .cors(CorsLayer::new()) + .normalize_path(true) + .tracing(TraceLayer::new_for_http()) + .layer(TraceLayer::new_for_http()) + .serve() + .await + .unwrap(); + }); + sleep(Duration::from_millis(250)).await; + handler.abort(); } #[test] diff --git a/src/axum/extractor.rs b/src/axum/extractor.rs index 271d827..6f2e8aa 100644 --- a/src/axum/extractor.rs +++ b/src/axum/extractor.rs @@ -10,6 +10,7 @@ use mime::Mime; use std::str::FromStr; use thiserror::Error; +/// A file extracted from a multipart request. #[derive(Debug, Clone, PartialEq)] pub struct File { pub filename: String, @@ -18,6 +19,7 @@ pub struct File { } impl File { + /// Creates a new file with the given filename, bytes and content type. pub fn new( filename: impl Into, bytes: impl Into>, @@ -30,7 +32,8 @@ impl File { } } - async fn from_field(field: Field<'_>) -> Result { + /// Creates a new file from a field in a multipart request. + pub async fn from_field(field: Field<'_>) -> Result { let filename = field .file_name() .ok_or(MultipartFileRejection::MissingFilename)? @@ -54,6 +57,7 @@ pub struct MultipartFile(pub File); #[derive(Debug, Clone, PartialEq)] pub struct MultipartFiles(pub Vec); +/// Rejection type for multipart file extractors. #[derive(Debug, Error)] pub enum MultipartFileRejection { #[error(transparent)] @@ -113,6 +117,19 @@ where { type Rejection = MultipartFileRejection; + /// Extracts a single file from a multipart request. + /// Expects exactly one file. A file must have a name, bytes and optionally a content type. + /// This extractor consumes the request and must ble placed last in the handler. + /// # Example + /// ``` + /// use std::str::from_utf8; + /// use axum::response::Html; + /// use lib::axum::extractor::MultipartFile; + /// + /// async fn upload_file(MultipartFile(file): MultipartFile) -> Html { + /// Html(String::from_utf8(file.bytes).unwrap()) + /// } + /// ``` async fn from_request(req: Request, state: &S) -> Result { let multipart = Multipart::from_request(req, state).await?; let files = get_files(multipart).await?; @@ -132,6 +149,24 @@ where { type Rejection = MultipartFileRejection; + /// Extracts multiple files from a multipart request. + /// Expects at least one file. A file must have a name, bytes and optionally a content type. + /// This extractor consumes the request and must ble placed last in the handler. + /// # Example + /// ``` + /// use axum::response::Html; + /// use lib::axum::extractor::MultipartFiles; + /// use std::str::from_utf8; + /// + /// async fn upload_files(MultipartFiles(files): MultipartFiles) -> Html { + /// let content = files + /// .iter() + /// .map(|file| String::from_utf8(file.bytes.clone()).unwrap()) + /// .collect::>() + /// .join("
"); + /// Html(content) + /// } + /// ``` async fn from_request(req: Request, state: &S) -> Result { let multipart = Multipart::from_request(req, state).await?; let files = get_files(multipart).await?; diff --git a/src/axum/response.rs b/src/axum/response.rs index aedd1c8..a197c9e 100644 --- a/src/axum/response.rs +++ b/src/axum/response.rs @@ -18,6 +18,7 @@ mod tests { use axum::http::header::CONTENT_TYPE; use axum::http::{HeaderValue, StatusCode}; use axum::response::IntoResponse; + use mime::APPLICATION_JSON; use serde::Serialize; use crate::serde::response::BaseResponse; @@ -39,7 +40,7 @@ mod tests { assert_eq!(json_response.status(), StatusCode::OK); assert_eq!( json_response.headers().get(CONTENT_TYPE), - Some(&HeaderValue::from_static("application/json")) + Some(&HeaderValue::from_static(APPLICATION_JSON.as_ref())) ); } diff --git a/src/axum/router.rs b/src/axum/router.rs index d45cfb0..fc80a4d 100644 --- a/src/axum/router.rs +++ b/src/axum/router.rs @@ -76,6 +76,7 @@ macro_rules! routes { }; } +/// Merges the given routers into a single router. #[macro_export] macro_rules! join_routes { ($($route:expr),* $(,)?) => { diff --git a/src/axum/wrappers.rs b/src/axum/wrappers.rs index 141bfc1..7fa9fa5 100644 --- a/src/axum/wrappers.rs +++ b/src/axum/wrappers.rs @@ -3,11 +3,13 @@ use derive_more::{Constructor, From}; use into_response_derive::IntoResponse; use serde::Serialize; +/// Wrapper for a vector of items. #[derive(Debug, Clone, PartialEq, Default, Serialize, From, Constructor)] pub struct Array { pub data: Vec, } +/// Wrapper for a count. #[derive( Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, IntoResponse, From, Constructor, )]