diff --git a/Cargo.lock b/Cargo.lock index 002acd30..45d50776 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -49,7 +49,7 @@ dependencies = [ "schemars", "serde", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", "tracing", ] @@ -661,6 +661,7 @@ dependencies = [ "chrono", "chrono-tz", "clap", + "cot_core", "cot_macros", "criterion", "deadpool-redis", @@ -705,7 +706,7 @@ dependencies = [ "swagger-ui-redist", "sync_wrapper", "tempfile", - "thiserror 2.0.16", + "thiserror 2.0.17", "time", "tokio", "toml", @@ -764,6 +765,40 @@ dependencies = [ "tracing", ] +[[package]] +name = "cot_core" +version = "0.4.0" +dependencies = [ + "aide", + "askama", + "async-stream", + "axum", + "backtrace", + "bytes", + "cot", + "cot_macros", + "derive_more", + "form_urlencoded", + "futures", + "futures-core", + "futures-util", + "http 1.3.1", + "http-body", + "http-body-util", + "indexmap", + "schemars", + "serde", + "serde_html_form", + "serde_json", + "serde_path_to_error", + "sync_wrapper", + "thiserror 2.0.17", + "tokio", + "tower", + "tower-sessions", + "tracing", +] + [[package]] name = "cot_macros" version = "0.4.0" @@ -1977,13 +2012,14 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.11.0" +version = "2.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ "equivalent", "hashbrown 0.15.5", "serde", + "serde_core", ] [[package]] @@ -3103,18 +3139,28 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -3159,12 +3205,13 @@ dependencies = [ [[package]] name = "serde_path_to_error" -version = "0.1.17" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" dependencies = [ "itoa", "serde", + "serde_core", ] [[package]] @@ -3341,7 +3388,7 @@ dependencies = [ "serde_json", "sha2", "smallvec", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tokio-stream", "tracing", @@ -3424,7 +3471,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.16", + "thiserror 2.0.17", "tracing", "whoami", ] @@ -3462,7 +3509,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.16", + "thiserror 2.0.17", "tracing", "whoami", ] @@ -3487,7 +3534,7 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", - "thiserror 2.0.16", + "thiserror 2.0.17", "tracing", "url", ] @@ -3617,11 +3664,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.16", + "thiserror-impl 2.0.17", ] [[package]] @@ -3637,9 +3684,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", @@ -3956,7 +4003,7 @@ dependencies = [ "rand 0.8.5", "serde", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", "time", "tokio", "tracing", diff --git a/Cargo.toml b/Cargo.toml index c20fb3c3..04a3f8aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "cot-cli", "cot-codegen", "cot-macros", + "cot-core", # Examples "examples/admin", "examples/custom-error-pages", @@ -71,6 +72,7 @@ clap_complete = "4" clap_mangen = "0.2.29" cot = { version = "0.4.0", path = "cot" } cot_codegen = { version = "0.4.0", path = "cot-codegen" } +cot_core = { version = "0.4.0", path = "cot-core" } cot_macros = { version = "0.4.0", path = "cot-macros" } criterion = "0.6" darling = "0.21" diff --git a/cot-core/Cargo.toml b/cot-core/Cargo.toml new file mode 100644 index 00000000..c3dad3c4 --- /dev/null +++ b/cot-core/Cargo.toml @@ -0,0 +1,54 @@ +[package] +name = "cot_core" +version = "0.4.0" +description = "The Rust web framework for lazy developers - framework core." +categories = ["web-programming", "web-programming::http-server", "network-programming"] +edition.workspace = true +rust-version.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +keywords.workspace = true +readme.workspace = true +authors.workspace = true + +[lints] +workspace = true + +[dependencies] +aide = { workspace = true, optional = true } +askama = { workspace = true, features = ["derive", "std"] } +axum = { workspace = true, features = ["http1", "tokio"] } +backtrace.workspace = true +bytes.workspace = true +cot_macros.workspace = true +derive_more = { workspace = true, features = ["debug", "deref", "display", "from"] } +form_urlencoded.workspace = true +futures-core.workspace = true +futures-util.workspace = true +http-body-util.workspace = true +http-body.workspace = true +http.workspace = true +indexmap.workspace = true +schemars = { workspace = true, optional = true } +serde = { workspace = true, features = ["derive"] } +serde_html_form = { workspace = true } +serde_json = { workspace = true, optional = true } +serde_path_to_error = { workspace = true } +sync_wrapper.workspace = true +thiserror.workspace = true +tower = { workspace = true, features = ["util"] } +tower-sessions = { workspace = true, features = ["memory-store"] } +tracing = "0.1.41" + +[dev-dependencies] +async-stream.workspace = true +cot = { workspace = true, features = ["test"] } +futures.workspace = true +tokio = { workspace = true, features = ["macros"] } + +[features] +default = [] +json = ["dep:serde_json"] +db = [] +openapi = ["dep:aide", "dep:schemars"] diff --git a/cot/src/body.rs b/cot-core/src/body.rs similarity index 91% rename from cot/src/body.rs rename to cot-core/src/body.rs index 874307c3..952f4b39 100644 --- a/cot/src/body.rs +++ b/cot-core/src/body.rs @@ -9,8 +9,7 @@ use http_body::{Frame, SizeHint}; use http_body_util::combinators::BoxBody; use sync_wrapper::SyncWrapper; -use crate::error::error_impl::impl_into_cot_error; -use crate::{Error, Result}; +use crate::impl_into_cot_error; /// A type that represents an HTTP request or response body. /// @@ -21,21 +20,21 @@ use crate::{Error, Result}; /// # Examples /// /// ``` -/// use cot::Body; +/// use cot_core::Body; /// /// let body = Body::fixed("Hello, world!"); /// let body = Body::streaming(futures::stream::once(async { Ok("Hello, world!".into()) })); /// ``` #[derive(Debug)] pub struct Body { - pub(crate) inner: BodyInner, + pub inner: BodyInner, } -pub(crate) enum BodyInner { +pub enum BodyInner { Fixed(Bytes), - Streaming(SyncWrapper> + Send>>>), + Streaming(SyncWrapper> + Send>>>), Axum(SyncWrapper), - Wrapper(BoxBody), + Wrapper(BoxBody), } impl Debug for BodyInner { @@ -60,7 +59,7 @@ impl Body { /// # Examples /// /// ``` - /// use cot::Body; + /// use cot_core::Body; /// /// let body = Body::empty(); /// ``` @@ -74,7 +73,7 @@ impl Body { /// # Examples /// /// ``` - /// use cot::Body; + /// use cot_core::Body; /// /// let body = Body::fixed("Hello, world!"); /// ``` @@ -89,7 +88,7 @@ impl Body { /// /// ``` /// use async_stream::stream; - /// use cot::Body; + /// use cot_core::Body; /// /// let stream = stream! { /// yield Ok("Hello, ".into()); @@ -98,7 +97,7 @@ impl Body { /// let body = Body::streaming(stream); /// ``` #[must_use] - pub fn streaming> + Send + 'static>(stream: T) -> Self { + pub fn streaming> + Send + 'static>(stream: T) -> Self { Self::new(BodyInner::Streaming(SyncWrapper::new(Box::pin(stream)))) } @@ -116,17 +115,17 @@ impl Body { /// # Examples /// /// ``` - /// use cot::Body; + /// use cot_core::Body; /// /// # #[tokio::main] - /// # async fn main() -> cot::Result<()> { + /// # async fn main() -> cot_core::Result<()> { /// let body = Body::fixed("Hello, world!"); /// let bytes = body.into_bytes().await?; /// assert_eq!(bytes, "Hello, world!".as_bytes()); /// # Ok(()) /// # } /// ``` - pub async fn into_bytes(self) -> Result { + pub async fn into_bytes(self) -> crate::Result { self.into_bytes_limited(usize::MAX).await } @@ -145,17 +144,17 @@ impl Body { /// # Examples /// /// ``` - /// use cot::Body; + /// use cot_core::Body; /// /// # #[tokio::main] - /// # async fn main() -> cot::Result<()> { + /// # async fn main() -> cot_core::Result<()> { /// let body = Body::fixed("Hello, world!"); /// let bytes = body.into_bytes_limited(32).await?; /// assert_eq!(bytes, "Hello, world!".as_bytes()); /// # Ok(()) /// # } /// ``` - pub async fn into_bytes_limited(self, limit: usize) -> Result { + pub async fn into_bytes_limited(self, limit: usize) -> crate::Result { use http_body_util::BodyExt; Ok(http_body_util::Limited::new(self, limit) @@ -166,12 +165,12 @@ impl Body { } #[must_use] - pub(crate) fn axum(inner: axum::body::Body) -> Self { + pub fn axum(inner: axum::body::Body) -> Self { Self::new(BodyInner::Axum(SyncWrapper::new(inner))) } #[must_use] - pub(crate) fn wrapper(inner: BoxBody) -> Self { + pub fn wrapper(inner: BoxBody) -> Self { Self::new(BodyInner::Wrapper(inner)) } } @@ -184,7 +183,7 @@ impl Default for Body { impl http_body::Body for Body { type Data = Bytes; - type Error = Error; + type Error = crate::Error; fn poll_frame( self: Pin<&mut Self>, @@ -290,7 +289,7 @@ mod tests { } } - #[cot::test] + #[cot_macros::test] async fn body_streaming() { let stream = stream::once(async { Ok(Bytes::from("Hello, world!")) }); let body = Body::streaming(stream); @@ -301,7 +300,7 @@ mod tests { } } - #[cot::test] + #[cot_macros::test] async fn http_body_poll_frame_fixed() { let content = "Hello, world!"; let mut body = Body::fixed(content); @@ -320,7 +319,7 @@ mod tests { } } - #[cot::test] + #[cot_macros::test] async fn http_body_poll_frame_streaming() { let content = "Hello, world!"; let mut body = Body::streaming(stream::once(async move { Ok(Bytes::from(content)) })); diff --git a/cot/src/error.rs b/cot-core/src/error.rs similarity index 80% rename from cot/src/error.rs rename to cot-core/src/error.rs index 2e24fa37..72aa75d6 100644 --- a/cot/src/error.rs +++ b/cot-core/src/error.rs @@ -1,5 +1,5 @@ -pub(crate) mod backtrace; -pub(crate) mod error_impl; +pub mod backtrace; +pub mod error_impl; pub mod handler; mod method_not_allowed; mod not_found; diff --git a/cot/src/error/backtrace.rs b/cot-core/src/error/backtrace.rs similarity index 94% rename from cot/src/error/backtrace.rs rename to cot-core/src/error/backtrace.rs index 9bc27cfe..9827b76e 100644 --- a/cot/src/error/backtrace.rs +++ b/cot-core/src/error/backtrace.rs @@ -1,7 +1,7 @@ // inline(never) is added to make sure there is a separate frame for this // function so that it can be used to find the start of the backtrace. #[inline(never)] -pub(crate) fn __cot_create_backtrace() -> Backtrace { +pub fn __cot_create_backtrace() -> Backtrace { let mut backtrace = Vec::new(); let mut start = false; backtrace::trace(|frame| { @@ -21,19 +21,19 @@ pub(crate) fn __cot_create_backtrace() -> Backtrace { } #[derive(Debug, Clone)] -pub(crate) struct Backtrace { +pub struct Backtrace { frames: Vec, } impl Backtrace { #[must_use] - pub(crate) fn frames(&self) -> &[StackFrame] { + pub fn frames(&self) -> &[StackFrame] { &self.frames } } #[derive(Debug, Clone)] -pub(crate) struct StackFrame { +pub struct StackFrame { symbol_name: Option, filename: Option, lineno: Option, @@ -42,7 +42,7 @@ pub(crate) struct StackFrame { impl StackFrame { #[must_use] - pub(crate) fn symbol_name(&self) -> String { + pub fn symbol_name(&self) -> String { self.symbol_name .as_deref() .unwrap_or("") @@ -50,7 +50,7 @@ impl StackFrame { } #[must_use] - pub(crate) fn location(&self) -> String { + pub fn location(&self) -> String { if let Some(filename) = self.filename.as_deref() { let mut s = filename.to_owned(); diff --git a/cot/src/error/error_impl.rs b/cot-core/src/error/error_impl.rs similarity index 99% rename from cot/src/error/error_impl.rs rename to cot-core/src/error/error_impl.rs index 4b3ebdcb..a732d65b 100644 --- a/cot/src/error/error_impl.rs +++ b/cot-core/src/error/error_impl.rs @@ -216,7 +216,7 @@ impl Error { } #[must_use] - pub(crate) fn backtrace(&self) -> &CotBacktrace { + pub fn backtrace(&self) -> &CotBacktrace { &self.repr.backtrace } @@ -319,6 +319,7 @@ impl From for askama::Error { } } +#[macro_export] macro_rules! impl_into_cot_error { ($error_ty:ty) => { impl From<$error_ty> for $crate::Error { @@ -335,7 +336,8 @@ macro_rules! impl_into_cot_error { } }; } -pub(crate) use impl_into_cot_error; + +pub use impl_into_cot_error; #[derive(Debug, thiserror::Error)] #[error("failed to render template: {0}")] diff --git a/cot/src/error/handler.rs b/cot-core/src/error/handler.rs similarity index 94% rename from cot/src/error/handler.rs rename to cot-core/src/error/handler.rs index 8dd03002..b8cae428 100644 --- a/cot/src/error/handler.rs +++ b/cot-core/src/error/handler.rs @@ -9,11 +9,10 @@ use std::task::{Context, Poll}; use derive_more::with_trait::Debug; -use crate::Error; -use crate::handler::handle_all_parameters; use crate::request::extractors::FromRequestHead; use crate::request::{Request, RequestHead}; use crate::response::Response; +use crate::{Error, handle_all_parameters}; /// A trait for handling error pages in Cot applications. /// @@ -26,9 +25,9 @@ use crate::response::Response; /// /// ``` /// use cot::Project; -/// use cot::error::handler::{DynErrorPageHandler, RequestError}; /// use cot::html::Html; /// use cot::response::IntoResponse; +/// use cot_core::error::handler::{DynErrorPageHandler, RequestError}; /// /// struct MyProject; /// impl Project for MyProject { @@ -64,7 +63,7 @@ pub trait ErrorPageHandler { fn handle(&self, head: &RequestHead) -> impl Future> + Send; } -pub(crate) trait BoxErrorPageHandler: Send + Sync { +pub trait BoxErrorPageHandler: Send + Sync { fn handle<'a>( &'a self, head: &'a RequestHead, @@ -87,15 +86,15 @@ impl DynErrorPageHandler { /// /// This method wraps a concrete error page handler in a type-erased /// wrapper, allowing it to be used in - /// [`crate::project::Project::error_handler`]. + /// [`cot::project::Project::error_handler`]. /// /// # Examples /// /// ``` /// use cot::Project; - /// use cot::error::handler::{DynErrorPageHandler, RequestError}; /// use cot::html::Html; /// use cot::response::IntoResponse; + /// use cot_core::error::handler::{DynErrorPageHandler, RequestError}; /// /// struct MyProject; /// impl Project for MyProject { @@ -120,7 +119,7 @@ impl DynErrorPageHandler { fn handle<'a>( &'a self, head: &'a RequestHead, - ) -> Pin> + Send + 'a>> { + ) -> Pin> + Send + 'a>> { Box::pin(self.0.handle(head)) } } @@ -134,7 +133,7 @@ impl DynErrorPageHandler { impl tower::Service for DynErrorPageHandler { type Response = Response; type Error = Error; - type Future = Pin> + Send>>; + type Future = Pin> + Send>>; fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { Poll::Ready(Ok(())) @@ -189,7 +188,7 @@ pub struct RequestOuterError(Arc); impl RequestOuterError { #[must_use] - pub(crate) fn new(error: Error) -> Self { + pub fn new(error: Error) -> Self { Self(Arc::new(error)) } } @@ -278,7 +277,7 @@ mod tests { assert_eq!(format!("{request_error}"), "Test error"); } - #[cot::test] + #[cot_macros::test] async fn request_outer_error_from_request_head() { let request = Request::default(); let (mut head, _) = request.into_parts(); @@ -289,7 +288,7 @@ mod tests { assert_eq!(format!("{extracted_error}"), "Test error"); } - #[cot::test] + #[cot_macros::test] async fn request_error_from_request_head() { let request = Request::default(); let (mut head, _) = request.into_parts(); diff --git a/cot/src/error/method_not_allowed.rs b/cot-core/src/error/method_not_allowed.rs similarity index 93% rename from cot/src/error/method_not_allowed.rs rename to cot-core/src/error/method_not_allowed.rs index d1b3205a..9f1c1915 100644 --- a/cot/src/error/method_not_allowed.rs +++ b/cot-core/src/error/method_not_allowed.rs @@ -11,7 +11,7 @@ use crate::error::error_impl::impl_into_cot_error; /// # Examples /// /// ``` -/// use cot::error::MethodNotAllowed; +/// use cot_core::error::MethodNotAllowed; /// /// let error = MethodNotAllowed::new(cot::Method::POST); /// assert_eq!(error.method, &cot::Method::POST); @@ -33,7 +33,7 @@ impl MethodNotAllowed { /// # Examples /// /// ``` - /// use cot::error::MethodNotAllowed; + /// use cot_core::error::MethodNotAllowed; /// /// let error = MethodNotAllowed::new(cot::Method::POST); /// assert_eq!(error.method, cot::Method::POST); diff --git a/cot/src/error/not_found.rs b/cot-core/src/error/not_found.rs similarity index 96% rename from cot/src/error/not_found.rs rename to cot-core/src/error/not_found.rs index f71144ba..ec0f1742 100644 --- a/cot/src/error/not_found.rs +++ b/cot-core/src/error/not_found.rs @@ -14,7 +14,7 @@ use crate::error::error_impl::impl_into_cot_error; /// # Examples /// /// ``` -/// use cot::error::NotFound; +/// use cot_core::error::NotFound; /// /// // Create a basic 404 error /// let error = NotFound::new(); @@ -42,7 +42,7 @@ impl NotFound { /// # Examples /// /// ``` - /// use cot::error::NotFound; + /// use cot_core::error::NotFound; /// /// let error = NotFound::new(); /// ``` @@ -60,7 +60,7 @@ impl NotFound { /// # Examples /// /// ``` - /// use cot::error::NotFound; + /// use cot_core::error::NotFound; /// /// let error = NotFound::with_message("User with ID 123 not found"); /// let page_name = "home"; @@ -72,7 +72,7 @@ impl NotFound { } #[must_use] - pub(crate) fn router() -> Self { + pub fn router() -> Self { Self::with_kind(Kind::FromRouter) } diff --git a/cot/src/error/uncaught_panic.rs b/cot-core/src/error/uncaught_panic.rs similarity index 95% rename from cot/src/error/uncaught_panic.rs rename to cot-core/src/error/uncaught_panic.rs index e4303a04..5c64efe5 100644 --- a/cot/src/error/uncaught_panic.rs +++ b/cot-core/src/error/uncaught_panic.rs @@ -22,7 +22,7 @@ use crate::error::error_impl::impl_into_cot_error; /// # Examples /// /// ``` -/// use cot::error::UncaughtPanic; +/// use cot_core::error::UncaughtPanic; /// /// // This would typically be created internally by Cot when catching panics /// let panic = UncaughtPanic::new(Box::new("something went wrong")); @@ -43,7 +43,7 @@ impl UncaughtPanic { /// # Examples /// /// ``` - /// use cot::error::UncaughtPanic; + /// use cot_core::error::UncaughtPanic; /// /// let panic = UncaughtPanic::new(Box::new("a panic occurred")); /// ``` @@ -66,7 +66,7 @@ impl UncaughtPanic { /// # Examples /// /// ``` - /// use cot::error::UncaughtPanic; + /// use cot_core::error::UncaughtPanic; /// /// let panic = UncaughtPanic::new(Box::new("test panic")); /// let payload = panic.payload(); diff --git a/cot/src/handler.rs b/cot-core/src/handler.rs similarity index 95% rename from cot/src/handler.rs rename to cot-core/src/handler.rs index be8f6423..4f2eb1a6 100644 --- a/cot/src/handler.rs +++ b/cot-core/src/handler.rs @@ -2,19 +2,19 @@ use std::future::Future; use std::marker::PhantomData; use std::pin::Pin; -use tower::util::BoxCloneSyncService; - +use crate::Error; +use crate::Result; use crate::request::Request; use crate::request::extractors::{FromRequest, FromRequestHead}; use crate::response::{IntoResponse, Response}; -use crate::{Error, Result}; +use tower::util::BoxCloneSyncService; /// A function that takes a request and returns a response. /// /// This is the main building block of a Cot app. You shouldn't /// usually need to implement this directly, as it is already /// implemented for closures and functions that take some -/// number of [extractors](crate::request::extractors) as parameters +/// number of [extractors](cot::request::extractors) as parameters /// and return some type that [can be converted into a /// response](IntoResponse). /// @@ -48,14 +48,14 @@ pub trait RequestHandler { fn handle(&self, request: Request) -> impl Future> + Send; } -pub(crate) trait BoxRequestHandler { +pub trait BoxRequestHandler { fn handle( &self, request: Request, ) -> Pin> + Send + '_>>; } -pub(crate) fn into_box_request_handler + Send + Sync>( +pub fn into_box_request_handler + Send + Sync>( handler: H, ) -> impl BoxRequestHandler { struct Inner(H, PhantomData T>); @@ -142,6 +142,7 @@ macro_rules! impl_request_handler_from_request { }; } +#[macro_export] macro_rules! handle_all_parameters { ($name:ident) => { $name!(); @@ -227,18 +228,18 @@ macro_rules! handle_all_parameters_from_request { }; } -pub(crate) use handle_all_parameters; +pub use handle_all_parameters; handle_all_parameters!(impl_request_handler); handle_all_parameters_from_request!(impl_request_handler_from_request); /// A wrapper around a handler that's used in -/// [`Bootstrapper`](cot::Bootstrapper). +/// [`Bootstrapper`](project::Bootstrapper). /// /// It is returned by -/// [`Bootstrapper::into_bootstrapped_project`](cot::Bootstrapper::finish). +/// [`Bootstrapper::into_bootstrapped_project`](project::Bootstrapper::finish). /// Typically, you don't need to interact with this type directly, except for -/// creating it in [`Project::middlewares`](cot::Project::middlewares) through +/// creating it in [`Project::middlewares`](project::Project::middlewares) through /// the [`RootHandlerBuilder::build`](cot::project::RootHandlerBuilder::build) /// method. /// diff --git a/cot-core/src/headers.rs b/cot-core/src/headers.rs new file mode 100644 index 00000000..746790b2 --- /dev/null +++ b/cot-core/src/headers.rs @@ -0,0 +1,7 @@ +pub const HTML_CONTENT_TYPE: &str = "text/html; charset=utf-8"; +pub const MULTIPART_FORM_CONTENT_TYPE: &str = "multipart/form-data"; +pub const URLENCODED_FORM_CONTENT_TYPE: &str = "application/x-www-form-urlencoded"; +#[cfg(feature = "json")] +pub const JSON_CONTENT_TYPE: &str = "application/json"; +pub const PLAIN_TEXT_CONTENT_TYPE: &str = "text/plain; charset=utf-8"; +pub const OCTET_STREAM_CONTENT_TYPE: &str = "application/octet-stream"; diff --git a/cot/src/html.rs b/cot-core/src/html.rs similarity index 96% rename from cot/src/html.rs rename to cot-core/src/html.rs index 00066911..e7249d9f 100644 --- a/cot/src/html.rs +++ b/cot-core/src/html.rs @@ -8,7 +8,7 @@ //! ## Creating and rendering an HTML Tag //! //! ``` -//! use cot::html::HtmlTag; +//! use cot_core::html::HtmlTag; //! //! let tag = HtmlTag::new("br"); //! let html = tag.render(); @@ -18,7 +18,7 @@ //! ## Adding Attributes to an HTML Tag //! //! ``` -//! use cot::html::HtmlTag; +//! use cot_core::html::HtmlTag; //! //! let mut tag = HtmlTag::new("input"); //! tag.attr("type", "text").attr("placeholder", "Enter text"); @@ -32,7 +32,7 @@ //! ## Creating nested HTML elements //! //! ``` -//! use cot::html::{Html, HtmlTag}; +//! use cot_core::html::{Html, HtmlTag}; //! //! let mut div = HtmlTag::new("div"); //! div.attr("class", "container"); @@ -60,7 +60,7 @@ use derive_more::{Deref, Display, From}; /// # Examples /// /// ``` -/// use cot::html::Html; +/// use cot_core::html::Html; /// /// let html = Html::new("
Hello
"); /// assert_eq!(html.as_str(), "
Hello
"); @@ -74,7 +74,7 @@ impl Html { /// # Examples /// /// ``` - /// use cot::html::Html; + /// use cot_core::html::Html; /// /// let html = Html::new("
Hello
"); /// assert_eq!(html.as_str(), "
Hello
"); @@ -89,7 +89,7 @@ impl Html { /// # Examples /// /// ``` - /// use cot::html::Html; + /// use cot_core::html::Html; /// /// let html = Html::new("
Hello
"); /// assert_eq!(html.as_str(), "
Hello
"); @@ -133,7 +133,7 @@ impl HtmlNode { /// # Examples /// /// ``` -/// use cot::html::HtmlTag; +/// use cot_core::html::HtmlTag; /// /// let mut tag = HtmlTag::new("div"); /// tag.attr("class", "container"); @@ -157,7 +157,7 @@ impl HtmlTag { /// # Examples /// /// ``` - /// use cot::html::HtmlTag; + /// use cot_core::html::HtmlTag; /// /// let tag = HtmlTag::new("div"); /// assert_eq!(tag.render().as_str(), "
"); @@ -177,7 +177,7 @@ impl HtmlTag { /// # Examples /// /// ``` - /// use cot::html::HtmlTag; + /// use cot_core::html::HtmlTag; /// /// let input = HtmlTag::input("text"); /// assert_eq!(input.render().as_str(), ""); @@ -203,7 +203,7 @@ impl HtmlTag { /// # Examples /// /// ``` - /// use cot::html::HtmlTag; + /// use cot_core::html::HtmlTag; /// /// let mut tag = HtmlTag::new("input"); /// tag.attr("type", "text").attr("placeholder", "Enter text"); @@ -235,7 +235,7 @@ impl HtmlTag { /// # Examples /// /// ``` - /// use cot::html::HtmlTag; + /// use cot_core::html::HtmlTag; /// /// let mut tag = HtmlTag::new("input"); /// tag.bool_attr("disabled"); @@ -260,7 +260,7 @@ impl HtmlTag { /// # Examples /// /// ``` - /// use cot::html::HtmlTag; + /// use cot_core::html::HtmlTag; /// /// let mut div = HtmlTag::new("div"); /// div.push_str("Hello, world!"); @@ -275,7 +275,7 @@ impl HtmlTag { /// # Examples /// /// ``` - /// use cot::html::HtmlTag; + /// use cot_core::html::HtmlTag; /// /// let mut div = HtmlTag::new("div"); /// let span = HtmlTag::new("span"); @@ -295,7 +295,7 @@ impl HtmlTag { /// # Examples /// /// ``` - /// use cot::html::HtmlTag; + /// use cot_core::html::HtmlTag; /// /// let tag = HtmlTag::new("div"); /// assert_eq!(tag.render().as_str(), "
"); diff --git a/cot-core/src/lib.rs b/cot-core/src/lib.rs new file mode 100644 index 00000000..27d95373 --- /dev/null +++ b/cot-core/src/lib.rs @@ -0,0 +1,29 @@ +pub use crate::error::error_impl::Error; + +pub mod body; +/// Error handling types and utilities for Cot applications. +/// +/// This module provides error types, error handlers, and utilities for +/// handling various types of errors that can occur in Cot applications, +/// including 404 Not Found errors, uncaught panics, and custom error pages. +pub mod error; +pub mod headers; +pub mod request; +pub mod response; +#[macro_use] +pub mod handler; +pub mod html; +pub mod middleware; +pub mod openapi; +pub mod router; + +/// A type alias for a result that can return a [`cot_core::Error`]. +pub type Result = std::result::Result; + +/// A type alias for an HTTP status code. +pub type StatusCode = http::StatusCode; + +/// A type alias for an HTTP method. +pub type Method = http::Method; + +pub use crate::body::Body; diff --git a/cot-core/src/middleware.rs b/cot-core/src/middleware.rs new file mode 100644 index 00000000..cf54c667 --- /dev/null +++ b/cot-core/src/middleware.rs @@ -0,0 +1,238 @@ +use std::task::{Context, Poll}; + +use bytes::Bytes; +use futures_util::TryFutureExt; +use http_body_util::BodyExt; +use http_body_util::combinators::BoxBody; +use tower::Service; + +use crate::Body; +use crate::error::error_impl::Error; +use crate::request::Request; +use crate::response::Response; + +/// Middleware that converts a any [`http::Response`] generic type to a +/// [`crate::response::Response`]. +/// +/// This is useful for converting a response from a middleware that is +/// compatible with the `tower` crate to a response that is compatible with +/// Cot. It's applied automatically by +/// [`RootHandlerBuilder::middleware()`](cot::project::RootHandlerBuilder::middleware()) +/// and is not needed to be added manually. +/// +/// # Examples +/// +/// ``` +/// use cot::Project; +/// use cot::middleware::LiveReloadMiddleware; +/// use cot::project::{MiddlewareContext, RootHandler, RootHandlerBuilder}; +/// +/// struct MyProject; +/// impl Project for MyProject { +/// fn middlewares( +/// &self, +/// handler: RootHandlerBuilder, +/// context: &MiddlewareContext, +/// ) -> RootHandler { +/// handler +/// // IntoCotResponseLayer used internally in middleware() +/// .middleware(LiveReloadMiddleware::from_context(context)) +/// .build() +/// } +/// } +/// ``` +#[derive(Debug, Copy, Clone)] +pub struct IntoCotResponseLayer; + +impl IntoCotResponseLayer { + /// Create a new [`IntoCotResponseLayer`]. + /// + /// # Examples + /// + /// ``` + /// use cot::middleware::IntoCotResponseLayer; + /// + /// let middleware = IntoCotResponseLayer::new(); + /// ``` + #[must_use] + pub fn new() -> Self { + Self {} + } +} + +impl Default for IntoCotResponseLayer { + fn default() -> Self { + Self::new() + } +} + +impl tower::Layer for IntoCotResponseLayer { + type Service = IntoCotResponse; + + fn layer(&self, inner: S) -> Self::Service { + IntoCotResponse { inner } + } +} + +/// Service struct that converts any [`http::Response`] generic type to +/// [`crate::response::Response`]. +/// +/// Used by [`IntoCotResponseLayer`]. +/// +/// # Examples +/// +/// ``` +/// use std::any::TypeId; +/// +/// use cot::middleware::{IntoCotResponse, IntoCotResponseLayer}; +/// +/// assert_eq!( +/// TypeId::of::<>::Service>(), +/// TypeId::of::>() +/// ); +/// ``` +#[derive(Debug, Clone)] +pub struct IntoCotResponse { + inner: S, +} + +impl Service for IntoCotResponse +where + S: Service>, + ResBody: http_body::Body + Send + Sync + 'static, + E: std::error::Error + Send + Sync + 'static, +{ + type Response = Response; + type Error = S::Error; + type Future = futures_util::future::MapOk) -> Response>; + + #[inline] + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx) + } + + #[inline] + fn call(&mut self, request: Request) -> Self::Future { + self.inner.call(request).map_ok(map_response) + } +} + +fn map_response(response: http::response::Response) -> Response +where + ResBody: http_body::Body + Send + Sync + 'static, + E: std::error::Error + Send + Sync + 'static, +{ + response.map(|body| Body::wrapper(BoxBody::new(body.map_err(map_err)))) +} + +/// Middleware that converts any error type to [`cot::Error`]. +/// +/// This is useful for converting a response from a middleware that is +/// compatible with the `tower` crate to a response that is compatible with +/// Cot. It's applied automatically by +/// [`RootHandlerBuilder::middleware()`](cot::project::RootHandlerBuilder::middleware()) +/// and is not needed to be added manually. +/// +/// # Examples +/// +/// ``` +/// use cot::Project; +/// use cot::middleware::LiveReloadMiddleware; +/// use cot::project::{MiddlewareContext, RootHandler, RootHandlerBuilder}; +/// +/// struct MyProject; +/// impl Project for MyProject { +/// fn middlewares( +/// &self, +/// handler: RootHandlerBuilder, +/// context: &MiddlewareContext, +/// ) -> RootHandler { +/// handler +/// // IntoCotErrorLayer used internally in middleware() +/// .middleware(LiveReloadMiddleware::from_context(context)) +/// .build() +/// } +/// } +/// ``` +#[derive(Debug, Copy, Clone)] +pub struct IntoCotErrorLayer; + +impl IntoCotErrorLayer { + /// Create a new [`IntoCotErrorLayer`]. + /// + /// # Examples + /// + /// ``` + /// use cot::middleware::IntoCotErrorLayer; + /// + /// let middleware = IntoCotErrorLayer::new(); + /// ``` + #[must_use] + pub fn new() -> Self { + Self {} + } +} + +impl Default for IntoCotErrorLayer { + fn default() -> Self { + Self::new() + } +} + +impl tower::Layer for IntoCotErrorLayer { + type Service = IntoCotError; + + fn layer(&self, inner: S) -> Self::Service { + IntoCotError { inner } + } +} + +/// Service struct that converts a any error type to a [`cot::Error`]. +/// +/// Used by [`IntoCotErrorLayer`]. +/// +/// # Examples +/// +/// ``` +/// use std::any::TypeId; +/// +/// use cot::middleware::{IntoCotError, IntoCotErrorLayer}; +/// +/// assert_eq!( +/// TypeId::of::<>::Service>(), +/// TypeId::of::>() +/// ); +/// ``` +#[derive(Debug, Clone)] +pub struct IntoCotError { + inner: S, +} + +impl Service for IntoCotError +where + S: Service, + >::Error: std::error::Error + Send + Sync + 'static, +{ + type Response = S::Response; + type Error = Error; + type Future = futures_util::future::MapErr Error>; + + #[inline] + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx).map_err(map_err) + } + + #[inline] + fn call(&mut self, request: Request) -> Self::Future { + self.inner.call(request).map_err(map_err) + } +} + +fn map_err(error: E) -> Error +where + E: std::error::Error + Send + Sync + 'static, +{ + #[expect(trivial_casts)] + let boxed = Box::new(error) as Box; + boxed.downcast::().map_or_else(Error::wrap, |e| *e) +} diff --git a/cot-core/src/openapi.rs b/cot-core/src/openapi.rs new file mode 100644 index 00000000..67bfc1cc --- /dev/null +++ b/cot-core/src/openapi.rs @@ -0,0 +1,754 @@ +//! OpenAPI integration for Cot Core. +//! +//! This module provides core traits and utilities for OpenAPI integration. +//! It contains the minimal types needed by the router to support OpenAPI. +//! Higher-level OpenAPI functionality is implemented in the main `cot` crate. + +pub mod method; + +use std::future::Future; +use std::marker::PhantomData; +use std::pin::Pin; + +use aide::openapi::{ + MediaType, Parameter, ParameterData, ParameterSchemaOrContent, PathStyle, QueryStyle, + ReferenceOr, RequestBody, +}; +use aide::openapi::{Operation, PathItem, StatusCode}; +use indexmap::IndexMap; +use schemars::{JsonSchema, Schema, SchemaGenerator}; +use serde_json::Value; + +use crate::Method; +use crate::handler::{BoxRequestHandler, RequestHandler}; +use crate::request::Request; +use crate::request::extractors::{Path, UrlQuery}; +use crate::response::{Response, WithExtension}; +/// Context for API route generation. +/// +/// `RouteContext` is used to generate OpenAPI paths from routes. It provides +/// information about the route, such as the HTTP method and route parameter +/// names. +#[non_exhaustive] +#[derive(Debug, Clone)] +pub struct RouteContext<'a> { + /// The HTTP method of the route. + pub method: Option, + /// The names of the route parameters. + pub param_names: &'a [&'a str], +} + +impl RouteContext<'_> { + /// Creates a new `RouteContext` with no information about the route. + /// + /// # Examples + /// + /// ``` + /// use cot_core::openapi::RouteContext; + /// + /// let context = RouteContext::new(); + /// ``` + #[must_use] + pub fn new() -> Self { + Self { + method: None, + param_names: &[], + } + } +} + +impl Default for RouteContext<'_> { + fn default() -> Self { + Self::new() + } +} + +/// Returns the OpenAPI path item for the route - a collection of different +/// HTTP operations (GET, POST, etc.) at a given URL. +/// +/// You usually shouldn't need to implement this directly. Instead, it's easiest +/// to use [`ApiMethodRouter`](crate::router::method::method::ApiMethodRouter). +/// You might want to implement this if you want to create a wrapper that +/// modifies the OpenAPI spec or want to create it manually. +/// +/// An object implementing [`AsApiRoute`] can be passed to +/// [`Route::with_api_handler`](crate::router::Route::with_api_handler) to +/// generate the OpenAPI specs. +/// +/// # Examples +/// +/// ``` +/// use aide::openapi::PathItem; +/// use cot_core::openapi::{AsApiRoute, RouteContext}; +/// use schemars::SchemaGenerator; +/// +/// struct RouteWrapper(T); +/// +/// impl AsApiRoute for RouteWrapper { +/// fn as_api_route( +/// &self, +/// route_context: &RouteContext<'_>, +/// schema_generator: &mut SchemaGenerator, +/// ) -> PathItem { +/// let mut spec = self.0.as_api_route(route_context, schema_generator); +/// spec.summary = Some("This route was wrapped with RouteWrapper".to_owned()); +/// spec +/// } +/// } +/// ``` +pub trait AsApiRoute { + /// Returns the OpenAPI path item for the route. + /// + /// # Examples + /// + /// ``` + /// use aide::openapi::PathItem; + /// use cot_core::openapi::{AsApiRoute, RouteContext}; + /// use schemars::SchemaGenerator; + /// + /// struct RouteWrapper(T); + /// + /// impl AsApiRoute for RouteWrapper { + /// fn as_api_route( + /// &self, + /// route_context: &RouteContext<'_>, + /// schema_generator: &mut SchemaGenerator, + /// ) -> PathItem { + /// let mut spec = self.0.as_api_route(route_context, schema_generator); + /// spec.summary = Some("This route was wrapped with RouteWrapper".to_owned()); + /// spec + /// } + /// } + /// ``` + fn as_api_route( + &self, + route_context: &RouteContext<'_>, + schema_generator: &mut SchemaGenerator, + ) -> PathItem; +} + +/// A trait that can be implemented for types that should be taken into +/// account when generating OpenAPI paths. +/// +/// When implementing this trait for a type, you can modify the `Operation` +/// object to add information about the type to the OpenAPI spec. The +/// default implementation of [`ApiOperationPart::modify_api_operation`] +/// does nothing to indicate that the type has no effect on the OpenAPI spec. +/// +/// # Example +/// +/// ``` +/// use cot::aide::openapi::{Operation, MediaType, ReferenceOr, RequestBody}; +/// use cot::openapi::{ApiOperationPart, RouteContext}; +/// use cot::request::Request; +/// use cot::request::extractors::FromRequest; +/// use indexmap::IndexMap; +/// use cot::schemars::SchemaGenerator; +/// use serde::de::DeserializeOwned; +/// +/// pub struct Json(pub D); +/// +/// impl FromRequest for Json { +/// async fn from_request(head: &cot::request::RequestHead, body: cot::Body) -> cot::Result { +/// // parse the request body as JSON +/// # unimplemented!() +/// } +/// } +/// +/// impl ApiOperationPart for Json { +/// fn modify_api_operation( +/// operation: &mut Operation, +/// _route_context: &RouteContext<'_>, +/// schema_generator: &mut SchemaGenerator, +/// ) { +/// operation.request_body = Some(ReferenceOr::Item(RequestBody { +/// content: IndexMap::from([( +/// "application/json".to_owned(), +/// MediaType { +/// schema: Some(aide::openapi::SchemaObject { +/// json_schema: D::json_schema(schema_generator), +/// external_docs: None, +/// example: None, +/// }), +/// ..Default::default() +/// }, +/// )]), +/// ..Default::default() +/// })); +/// } +/// } +/// +/// # let mut operation = Operation::default(); +/// # let route_context = RouteContext::new(); +/// # let mut schema_generator = SchemaGenerator::default(); +/// # Json::::modify_api_operation(&mut operation, &route_context, &mut schema_generator); +/// # assert!(operation.request_body.is_some()); +/// ``` +pub trait ApiOperationPart { + /// Modify the OpenAPI operation object. + /// + /// This function is called by the framework when generating the OpenAPI + /// spec for a route. You can use this function to add custom information + /// to the operation object. + /// + /// The default implementation does nothing. + /// + /// # Examples + /// + /// ``` + /// use aide::openapi::Operation; + /// use cot::openapi::{ApiOperationPart, RouteContext}; + /// use schemars::SchemaGenerator; + /// + /// struct MyExtractor(T); + /// + /// impl ApiOperationPart for MyExtractor { + /// fn modify_api_operation( + /// operation: &mut Operation, + /// _route_context: &RouteContext<'_>, + /// _schema_generator: &mut SchemaGenerator, + /// ) { + /// // Add custom OpenAPI information to the operation + /// } + /// } + /// ``` + #[expect(unused)] + fn modify_api_operation( + operation: &mut Operation, + route_context: &RouteContext<'_>, + schema_generator: &mut SchemaGenerator, + ) { + } +} + +/// A trait that generates OpenAPI response objects for handler return types. +/// +/// This trait is implemented for types that can be returned from request +/// handlers and need to be documented in the OpenAPI specification. It allows +/// you to specify how a type should be represented in the OpenAPI +/// documentation. +/// +/// # Examples +/// +/// ``` +/// use cot::aide::openapi::{MediaType, Operation, Response, StatusCode}; +/// use cot::openapi::{ApiOperationResponse, RouteContext}; +/// use indexmap::IndexMap; +/// use schemars::SchemaGenerator; +/// +/// // A custom response type +/// struct MyResponse(T); +/// +/// impl ApiOperationResponse for MyResponse { +/// fn api_operation_responses( +/// _operation: &mut Operation, +/// _route_context: &RouteContext<'_>, +/// schema_generator: &mut SchemaGenerator, +/// ) -> Vec<(Option, Response)> { +/// vec![( +/// Some(StatusCode::Code(201)), +/// Response { +/// description: "Created".to_string(), +/// content: IndexMap::from([( +/// "application/json".to_string(), +/// MediaType { +/// schema: Some(aide::openapi::SchemaObject { +/// json_schema: T::json_schema(schema_generator), +/// external_docs: None, +/// example: None, +/// }), +/// ..Default::default() +/// }, +/// )]), +/// ..Default::default() +/// }, +/// )] +/// } +/// } +/// ``` +pub trait ApiOperationResponse { + /// Returns a list of OpenAPI response objects for this type. + /// + /// This method is called by the framework when generating the OpenAPI + /// specification for a route. It should return a list of responses + /// that this type can produce, along with their status codes. + /// + /// The status code can be `None` to indicate a default response. + /// + /// # Examples + /// + /// ``` + /// use cot::aide::openapi::{MediaType, Operation, Response, StatusCode}; + /// use cot::openapi::{ApiOperationResponse, RouteContext}; + /// use indexmap::IndexMap; + /// use schemars::SchemaGenerator; + /// + /// // A custom response type that always returns 201 Created + /// struct CreatedResponse(T); + /// + /// impl ApiOperationResponse for CreatedResponse { + /// fn api_operation_responses( + /// _operation: &mut Operation, + /// _route_context: &RouteContext<'_>, + /// schema_generator: &mut SchemaGenerator, + /// ) -> Vec<(Option, Response)> { + /// vec![( + /// Some(StatusCode::Code(201)), + /// Response { + /// description: "Created".to_string(), + /// content: IndexMap::from([( + /// "application/json".to_string(), + /// MediaType { + /// schema: Some(aide::openapi::SchemaObject { + /// json_schema: T::json_schema(schema_generator), + /// external_docs: None, + /// example: None, + /// }), + /// ..Default::default() + /// }, + /// )]), + /// ..Default::default() + /// }, + /// )] + /// } + /// } + /// ``` + #[expect(unused)] + fn api_operation_responses( + operation: &mut Operation, + route_context: &RouteContext<'_>, + schema_generator: &mut SchemaGenerator, + ) -> Vec<(Option, aide::openapi::Response)> { + Vec::new() + } +} + +/// Trait for handlers that can be used in API routes with OpenAPI +/// documentation. +/// +/// This trait combines [`BoxRequestHandler`] and [`AsApiRoute`] to allow +/// handlers to both process requests and provide OpenAPI documentation. +pub trait BoxApiEndpointRequestHandler: BoxRequestHandler + AsApiRoute { + // TODO: consider removing this when Rust trait_upcasting is stabilized and we + // bump the MSRV (lands in Rust 1.86) + fn as_box_request_handler(&self) -> &(dyn BoxRequestHandler + Send + Sync); +} + +/// Wraps a handler into a type-erased [`BoxApiEndpointRequestHandler`]. +/// +/// This function is used internally by the router to convert handlers into +/// trait objects that can be stored and invoked dynamically. +pub fn into_box_api_endpoint_request_handler( + handler: H, +) -> impl BoxApiEndpointRequestHandler +where + H: RequestHandler + AsApiRoute + Send + Sync, +{ + struct Inner(H, PhantomData HandlerParams>); + + impl BoxRequestHandler for Inner + where + H: RequestHandler + AsApiRoute + Send + Sync, + { + fn handle( + &self, + request: Request, + ) -> Pin> + Send + '_>> { + Box::pin(self.0.handle(request)) + } + } + + impl AsApiRoute for Inner + where + H: RequestHandler + AsApiRoute + Send + Sync, + { + fn as_api_route( + &self, + route_context: &RouteContext<'_>, + schema_generator: &mut SchemaGenerator, + ) -> PathItem { + self.0.as_api_route(route_context, schema_generator) + } + } + + impl BoxApiEndpointRequestHandler for Inner + where + H: RequestHandler + AsApiRoute + Send + Sync, + { + fn as_box_request_handler(&self) -> &(dyn BoxRequestHandler + Send + Sync) { + self + } + } + + Inner(handler, PhantomData) +} + +/// Returns the OpenAPI operation for the route - a specific HTTP operation +/// (GET, POST, etc.) at a given URL. +/// +/// You shouldn't typically need to implement this trait yourself. It is +/// implemented automatically for all functions that can be used as request +/// handlers, as long as all the parameters and the return type implement the +/// [`ApiOperationPart`] trait. You might need to implement it yourself if you +/// are creating a wrapper over a [`RequestHandler`] that adds some extra +/// functionality, or you want to modify the OpenAPI specs or create them +/// manually. +/// +/// # Examples +/// +/// ``` +/// use cot::aide::openapi::Operation; +/// use cot::openapi::{AsApiOperation, RouteContext}; +/// use schemars::SchemaGenerator; +/// +/// struct HandlerWrapper(T); +/// +/// impl AsApiOperation for HandlerWrapper { +/// fn as_api_operation( +/// &self, +/// route_context: &RouteContext<'_>, +/// schema_generator: &mut SchemaGenerator, +/// ) -> Option { +/// // a wrapper that hides the operation from OpenAPI spec +/// None +/// } +/// } +/// +/// # assert!(HandlerWrapper::<()>(()).as_api_operation(&RouteContext::new(), &mut SchemaGenerator::default()).is_none()); +/// ``` +pub trait AsApiOperation { + /// Returns the OpenAPI operation for the route. + /// + /// # Examples + /// + /// ``` + /// use cot::aide::openapi::Operation; + /// use cot::openapi::{AsApiOperation, RouteContext}; + /// use schemars::SchemaGenerator; + /// + /// struct HandlerWrapper(T); + /// + /// impl AsApiOperation for HandlerWrapper { + /// fn as_api_operation( + /// &self, + /// route_context: &RouteContext<'_>, + /// schema_generator: &mut SchemaGenerator, + /// ) -> Option { + /// // a wrapper that hides the operation from OpenAPI spec + /// None + /// } + /// } + /// + /// # assert!(HandlerWrapper::<()>(()).as_api_operation(&RouteContext::new(), &mut SchemaGenerator::default()).is_none()); + /// ``` + fn as_api_operation( + &self, + route_context: &RouteContext<'_>, + schema_generator: &mut SchemaGenerator, + ) -> Option; +} + +#[macro_export] +macro_rules! impl_as_openapi_operation { + ($($ty:ident),*) => { + impl AsApiOperation<($($ty,)*)> for T + where + T: Fn($($ty,)*) -> R + Clone + Send + Sync + 'static, + $($ty: ApiOperationPart,)* + R: for<'a> Future + Send, + Response: ApiOperationResponse, + { + #[allow( + clippy::allow_attributes, + non_snake_case, + reason = "for the case where there are no FromRequestHead params" + )] + fn as_api_operation( + &self, + route_context: &RouteContext<'_>, + schema_generator: &mut SchemaGenerator, + ) -> Option { + let mut operation = Operation::default(); + + $( + $ty::modify_api_operation( + &mut operation, + &route_context, + schema_generator + ); + )* + let responses = Response::api_operation_responses( + &mut operation, + &route_context, + schema_generator + ); + let operation_responses = operation.responses.get_or_insert_default(); + for (response_code, response) in responses { + if let Some(response_code) = response_code { + operation_responses.responses.insert( + response_code, + ReferenceOr::Item(response), + ); + } else { + operation_responses.default = Some(ReferenceOr::Item(response)); + } + } + + Some(operation) + } + } + }; +} + +pub(crate) trait BoxApiRequestHandler: BoxRequestHandler + AsApiOperation {} + +pub(crate) fn into_box_api_request_handler( + handler: H, +) -> impl BoxApiRequestHandler +where + H: RequestHandler + AsApiOperation + Send + Sync, +{ + struct Inner( + H, + PhantomData HandlerParams>, + PhantomData ApiParams>, + ); + + impl BoxRequestHandler for Inner + where + H: RequestHandler + AsApiOperation + Send + Sync, + { + fn handle( + &self, + request: Request, + ) -> Pin> + Send + '_>> { + Box::pin(self.0.handle(request)) + } + } + + impl AsApiOperation for Inner + where + H: RequestHandler + AsApiOperation + Send + Sync, + { + fn as_api_operation( + &self, + route_context: &RouteContext<'_>, + schema_generator: &mut SchemaGenerator, + ) -> Option { + self.0.as_api_operation(route_context, schema_generator) + } + } + + impl BoxApiRequestHandler for Inner where + H: RequestHandler + AsApiOperation + Send + Sync + { + } + + Inner(handler, PhantomData, PhantomData) +} + +handle_all_parameters!(impl_as_openapi_operation); + +impl ApiOperationPart for Request {} +impl ApiOperationPart for Method {} +impl ApiOperationPart for Path { + #[track_caller] + fn modify_api_operation( + operation: &mut Operation, + route_context: &RouteContext<'_>, + schema_generator: &mut SchemaGenerator, + ) { + let mut schema = D::json_schema(schema_generator); + let schema_obj = schema.ensure_object(); + + if let Some(items) = schema_obj.get("prefixItems") { + // a tuple of path params, e.g. Path<(i32, String)> + + if let Value::Array(item_list) = items { + assert_eq!( + route_context.param_names.len(), + item_list.len(), + "the number of path parameters in the route URL must match \ + the number of params in the Path type (found path params: {:?})", + route_context.param_names, + ); + + for (¶m_name, item) in route_context.param_names.iter().zip(item_list.iter()) { + let array_item = Schema::try_from(item.clone()) + .expect("schema.items must contain valid schemas"); + + add_path_param(operation, array_item, param_name.to_owned()); + } + } + } else if let Some(properties) = schema_obj.get("properties") { + // a struct of path params, e.g. Path + + if let Value::Object(properties) = properties { + let mut route_context_sorted = route_context.param_names.to_vec(); + route_context_sorted.sort_unstable(); + let mut object_props_sorted = properties.keys().collect::>(); + object_props_sorted.sort(); + + assert_eq!( + route_context_sorted, object_props_sorted, + "Path parameters in the route info must exactly match parameters \ + in the Path type. Make sure that the type you pass to Path contains \ + all the parameters for the route, and that the names match exactly." + ); + + for (key, item) in properties { + let object_item = Schema::try_from(item.clone()) + .expect("schema.properties must contain valid schemas"); + + add_path_param(operation, object_item, key.clone()); + } + } + } else if schema_obj.contains_key("type") { + // single path param, e.g. Path + + assert_eq!( + route_context.param_names.len(), + 1, + "the number of path parameters in the route URL must equal \ + to 1 if a single parameter was passed to the Path type (found path params: {:?})", + route_context.param_names, + ); + + add_path_param(operation, schema, route_context.param_names[0].to_owned()); + } + } +} + +impl ApiOperationPart for UrlQuery { + fn modify_api_operation( + operation: &mut Operation, + _route_context: &RouteContext<'_>, + schema_generator: &mut SchemaGenerator, + ) { + let schema = D::json_schema(schema_generator); + + if let Some(Value::Object(properties)) = schema.get("properties") { + for (key, item) in properties { + let object_item = Schema::try_from(item.clone()) + .expect("schema.properties must contain valid schemas"); + + add_query_param(operation, object_item, key.clone()); + } + } + } +} +impl ApiOperationResponse for WithExtension { + fn api_operation_responses( + operation: &mut Operation, + route_context: &RouteContext<'_>, + schema_generator: &mut SchemaGenerator, + ) -> Vec<(Option, aide::openapi::Response)> { + T::api_operation_responses(operation, route_context, schema_generator) + } +} + +impl ApiOperationResponse for crate::Result { + fn api_operation_responses( + _operation: &mut Operation, + _route_context: &RouteContext<'_>, + _schema_generator: &mut SchemaGenerator, + ) -> Vec<(Option, aide::openapi::Response)> { + vec![( + None, + aide::openapi::Response { + description: "*<unspecified>*".to_string(), + ..Default::default() + }, + )] + } +} + +// we don't require `E: ApiOperationResponse` here because a global error +// handler will typically take care of generating OpenAPI responses for errors +// +// we might want to add a version for `E: ApiOperationResponse` when (if ever) +// specialization lands in Rust: https://github.com/rust-lang/rust/issues/31844 +impl ApiOperationResponse for Result +where + T: ApiOperationResponse, +{ + fn api_operation_responses( + operation: &mut Operation, + route_context: &RouteContext<'_>, + schema_generator: &mut SchemaGenerator, + ) -> Vec<(Option, aide::openapi::Response)> { + let mut responses = Vec::new(); + + let ok_response = T::api_operation_responses(operation, route_context, schema_generator); + for (status_code, response) in ok_response { + responses.push((status_code, response)); + } + + responses + } +} + +fn add_path_param(operation: &mut Operation, mut schema: Schema, param_name: String) { + let required = extract_is_required(&mut schema); + + operation + .parameters + .push(ReferenceOr::Item(Parameter::Path { + parameter_data: param_with_name(param_name, schema, required), + style: PathStyle::default(), + })); +} + +// TODO: remove pub +pub fn add_query_param(operation: &mut Operation, mut schema: Schema, param_name: String) { + let required = extract_is_required(&mut schema); + + operation + .parameters + .push(ReferenceOr::Item(Parameter::Query { + parameter_data: param_with_name(param_name, schema, required), + allow_reserved: false, + style: QueryStyle::default(), + allow_empty_value: None, + })); +} + +fn extract_is_required(object_item: &mut Schema) -> bool { + let object = object_item.ensure_object(); + let obj_type = object.get_mut("type"); + let null_value = Value::String("null".to_string()); + + if let Some(Value::Array(types)) = obj_type { + if types.contains(&null_value) { + // If the type is nullable, we need to remove "null" from the types + // and return false, indicating that the parameter is not required. + types.retain(|t| t != &null_value); + false + } else { + // If "null" is not in the types, we assume it's a required parameter + true + } + } else { + // If the type is a single string (or some other unknown value), we assume it's + // a required parameter + true + } +} + +fn param_with_name(param_name: String, schema: Schema, required: bool) -> ParameterData { + ParameterData { + name: param_name, + description: None, + required, + deprecated: None, + format: ParameterSchemaOrContent::Schema(aide::openapi::SchemaObject { + json_schema: schema, + external_docs: None, + example: None, + }), + example: None, + examples: IndexMap::default(), + explode: None, + extensions: IndexMap::default(), + } +} diff --git a/cot/src/router/method/openapi.rs b/cot-core/src/openapi/method.rs similarity index 92% rename from cot/src/router/method/openapi.rs rename to cot-core/src/openapi/method.rs index 078bc860..3c4daca4 100644 --- a/cot/src/router/method/openapi.rs +++ b/cot-core/src/openapi/method.rs @@ -7,17 +7,15 @@ use std::fmt::{Debug, Formatter}; use aide::openapi::Operation; -use cot::openapi::RouteContext; -use cot::request::Request; -use cot::response::Response; -use cot::router::method::InnerHandler; use schemars::SchemaGenerator; -use crate::RequestHandler; +use crate::handler::RequestHandler; use crate::openapi::{ - AsApiOperation, AsApiRoute, BoxApiRequestHandler, into_box_api_request_handler, + into_box_api_request_handler, AsApiOperation, AsApiRoute, BoxApiRequestHandler, RouteContext, }; -use crate::router::method::InnerMethodRouter; +use crate::request::Request; +use crate::response::Response; +use crate::router::method::{InnerHandler, InnerMethodRouter}; /// A version of [`MethodRouter`](crate::router::method::MethodRouter) that /// supports OpenAPI. @@ -36,7 +34,7 @@ use crate::router::method::InnerMethodRouter; /// [`HEAD`] requests, the router will return the response generated by the /// handler for [`GET`] requests. /// -/// See [`crate::openapi`] module documentation for more details on how to +/// See [`cot::openapi`] module documentation for more details on how to /// generate OpenAPI specs automatically. /// /// [405 Method Not Allowed]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/405 @@ -47,9 +45,9 @@ use crate::router::method::InnerMethodRouter; /// /// ``` /// use cot::json::Json; -/// use cot::router::method::openapi::api_post; -/// use cot::router::{Route, Router}; /// use cot::test::TestRequestBuilder; +/// use cot_core::router::method::method::api_post; +/// use cot_core::router::{Route, Router}; /// use serde::{Deserialize, Serialize}; /// /// #[derive(Serialize, Deserialize, schemars::JsonSchema)] @@ -106,7 +104,7 @@ macro_rules! define_method { /// /// ``` /// use cot::json::Json; - /// use cot::router::method::openapi::ApiMethodRouter; + /// use cot_core::router::method::method::ApiMethodRouter; /// /// async fn test_handler() -> Json<()> { /// Json(()) @@ -169,18 +167,18 @@ impl ApiMethodRouter { /// Create a new [`ApiMethodRouter`]. /// /// You might consider using [`api_get`], [`api_post`], or one of the other - /// functions defined in [`cot::router::method::openapi`] which serve as - /// convenient constructors for a [`ApiMethodRouter`] with a specific + /// functions defined in [`cot_core::router::method::openapi`] which serve + /// as convenient constructors for a [`ApiMethodRouter`] with a specific /// handler. /// /// # Examples /// /// ``` /// use cot::json::Json; - /// use cot::router::method::MethodRouter; - /// use cot::router::method::openapi::ApiMethodRouter; - /// use cot::router::{Route, Router}; /// use cot::test::TestRequestBuilder; + /// use cot_core::router::method::MethodRouter; + /// use cot_core::router::method::method::ApiMethodRouter; + /// use cot_core::router::{Route, Router}; /// /// async fn test_handler() -> Json<()> { /// Json(()) @@ -232,7 +230,7 @@ impl ApiMethodRouter { /// /// ``` /// use cot::json::Json; - /// use cot::router::method::openapi::ApiMethodRouter; + /// use cot_core::router::method::method::ApiMethodRouter; /// /// async fn test_handler() -> Json<()> { /// Json(()) @@ -242,8 +240,8 @@ impl ApiMethodRouter { /// # async fn main() -> cot::Result<()> { /// let method_router = ApiMethodRouter::new().connect(test_handler); /// # - /// # let router = cot::router::Router::with_urls( - /// # [cot::router::Route::with_api_handler("/", method_router)] + /// # let router = cot_core::router::Router::with_urls( + /// # [cot_core::router::Route::with_api_handler("/", method_router)] /// # ); /// # /// # let request = cot::test::TestRequestBuilder::with_method("/", cot::Method::CONNECT) @@ -276,10 +274,10 @@ impl ApiMethodRouter { /// /// ``` /// use cot::html::Html; - /// use cot::router::method::openapi::ApiMethodRouter; - /// use cot::router::{Route, Router}; /// use cot::test::TestRequestBuilder; /// use cot::{Body, StatusCode}; + /// use cot_core::router::method::method::ApiMethodRouter; + /// use cot_core::router::{Route, Router}; /// /// async fn fallback_handler() -> Html { /// Html::new("fallback") @@ -315,7 +313,7 @@ impl ApiMethodRouter { } impl RequestHandler for ApiMethodRouter { - fn handle(&self, request: Request) -> impl Future> + Send { + fn handle(&self, request: Request) -> impl Future> + Send { self.inner.handle(request) } } @@ -330,7 +328,7 @@ impl AsApiRoute for ApiMethodRouter { ($path_item:ident, $method_func:ident, $method:ident) => { if let Some(handler) = &self.inner.$method_func { let mut route_context = route_context.clone(); - route_context.method = Some(cot::Method::$method); + route_context.method = Some(crate::Method::$method); $path_item.$method_func = handler.as_api_operation(&route_context, schema_generator); } @@ -372,7 +370,7 @@ impl Debug for InnerApiHandler { } impl RequestHandler for InnerApiHandler { - fn handle(&self, request: Request) -> impl Future> + Send { + fn handle(&self, request: Request) -> impl Future> + Send { self.0.handle(request) } } @@ -477,8 +475,8 @@ define_method_router!(api_trace, trace => TRACE); /// /// ``` /// use cot::html::Html; -/// use cot::router::method::openapi::api_connect; /// use cot::{Body, StatusCode}; +/// use cot_core::router::method::method::api_connect; /// /// async fn test_handler() -> Html { /// Html::new("test") @@ -488,8 +486,8 @@ define_method_router!(api_trace, trace => TRACE); /// # async fn main() -> cot::Result<()> { /// let method_router = api_connect(test_handler); /// # -/// # let router = cot::router::Router::with_urls( -/// # [cot::router::Route::with_api_handler("/", method_router)] +/// # let router = cot_core::router::Router::with_urls( +/// # [cot_core::router::Route::with_api_handler("/", method_router)] /// # ); /// # /// # let request = cot::test::TestRequestBuilder::with_method("/", cot::Method::CONNECT) @@ -517,16 +515,17 @@ where #[cfg(test)] mod tests { + use cot::html::Html; + use cot::json::Json; + use cot::test::TestRequestBuilder; + use super::*; use crate::error::MethodNotAllowed; - use crate::html::Html; - use crate::json::Json; use crate::request::extractors::Path; use crate::response::{IntoResponse, Response}; - use crate::test::TestRequestBuilder; use crate::{Method, StatusCode}; - async fn test_handler(method: Method) -> cot::Result { + async fn test_handler(method: Method) -> crate::Result { Html::new(method.as_str()).into_response() } @@ -539,7 +538,7 @@ mod tests { assert_eq!(debug_str, "InnerApiHandler(..)"); } - #[cot::test] + #[cot_macros::test] async fn api_method_router_fallback() { let router = ApiMethodRouter::new(); @@ -551,7 +550,7 @@ mod tests { assert!(inner.is::()); } - #[cot::test] + #[cot_macros::test] async fn api_method_router_default_fallback() { let router = ApiMethodRouter::default(); @@ -563,7 +562,7 @@ mod tests { assert!(inner.is::()); } - #[cot::test] + #[cot_macros::test] async fn api_method_router_custom_fallback() { let router = ApiMethodRouter::new().fallback(test_handler); @@ -574,7 +573,7 @@ mod tests { assert_eq!(response.into_body().into_bytes().await.unwrap(), "GET"); } - #[cot::test] + #[cot_macros::test] async fn api_method_router_router_get() { let router = api_get(test_handler); @@ -626,7 +625,7 @@ mod tests { test_api_method_router!(method_api_router_trace, api_trace, TRACE); test_api_method_router!(method_api_router_connect, api_connect, CONNECT); - #[cot::test] + #[cot_macros::test] async fn api_method_router_default_head() { // verify that the default method router doesn't handle HEAD let router = ApiMethodRouter::new(); @@ -647,7 +646,7 @@ mod tests { assert_eq!(response.status(), StatusCode::OK); } - #[cot::test] + #[cot_macros::test] async fn api_method_router_multiple() { let router = ApiMethodRouter::new() .get(test_handler) @@ -685,7 +684,7 @@ mod tests { async fn test_handler_with_params( Path(_): Path, Json(_): Json, - ) -> cot::Result { + ) -> crate::Result { Html::new("").into_response() } diff --git a/cot-core/src/request.rs b/cot-core/src/request.rs new file mode 100644 index 00000000..aaa0ba60 --- /dev/null +++ b/cot-core/src/request.rs @@ -0,0 +1,328 @@ +//! HTTP request type and helper methods. +//! +//! Cot uses the [`Request`](http::Request) type from the [`http`] crate +//! to represent incoming HTTP requests. However, it also provides a +//! [`RequestExt`] trait that contain various helper methods for working with +//! HTTP requests. These methods are used to access the application context, +//! project configuration, path parameters, and more. You probably want to have +//! a `use` statement for [`RequestExt`] in your code most of the time to be +//! able to use these functions: +//! +//! ``` +//! use cot_core::request::RequestExt; +//! ``` + +use indexmap::IndexMap; + +use crate::impl_into_cot_error; +pub mod extractors; +mod path_params_deserializer; + +/// HTTP request type. +pub type Request = http::Request; + +/// HTTP request head type. +pub type RequestHead = http::request::Parts; + +#[repr(transparent)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct RouteName(pub String); + +/// Path parameters extracted from the request URL, and available as a map of +/// strings. +/// +/// This struct is meant to be mainly used by the [`PathParams::parse`] +/// method, which will deserialize the path parameters into a type `T` +/// implementing `serde::DeserializeOwned`. If needed, you can also access the +/// path parameters directly using the [`PathParams::get`] method. +/// +/// # Examples +/// +/// ``` +/// use cot::response::Response; +/// use cot::test::TestRequestBuilder; +/// use cot_core::request::{PathParams, Request, RequestExt}; +/// +/// async fn my_handler(mut request: Request) -> cot_core::Result { +/// let path_params = request.path_params(); +/// let name = path_params.get("name").unwrap(); +/// +/// // using more ergonomic syntax: +/// let name: String = request.path_params().parse()?; +/// +/// let name = println!("Hello, {}!", name); +/// // ... +/// # unimplemented!() +/// } +/// ``` +#[derive(Debug, Clone)] +pub struct PathParams { + params: IndexMap, +} + +impl Default for PathParams { + fn default() -> Self { + Self::new() + } +} + +impl PathParams { + /// Creates a new [`PathParams`] instance. + /// + /// # Examples + /// + /// ``` + /// use cot_core::request::PathParams; + /// + /// let mut path_params = PathParams::new(); + /// path_params.insert("name".into(), "world".into()); + /// assert_eq!(path_params.get("name"), Some("world")); + /// ``` + #[must_use] + pub fn new() -> Self { + Self { + params: IndexMap::new(), + } + } + + /// Inserts a new path parameter. + /// + /// # Examples + /// + /// ``` + /// use cot_core::request::PathParams; + /// + /// let mut path_params = PathParams::new(); + /// path_params.insert("name".into(), "world".into()); + /// assert_eq!(path_params.get("name"), Some("world")); + /// ``` + pub fn insert(&mut self, name: String, value: String) { + self.params.insert(name, value); + } + + /// Iterates over the path parameters. + /// + /// # Examples + /// + /// ``` + /// use cot_core::request::PathParams; + /// + /// let mut path_params = PathParams::new(); + /// path_params.insert("name".into(), "world".into()); + /// for (name, value) in path_params.iter() { + /// println!("{}: {}", name, value); + /// } + /// ``` + pub fn iter(&self) -> impl Iterator { + self.params + .iter() + .map(|(name, value)| (name.as_str(), value.as_str())) + } + + /// Returns the number of path parameters. + /// + /// # Examples + /// + /// ``` + /// use cot_core::request::PathParams; + /// + /// let path_params = PathParams::new(); + /// assert_eq!(path_params.len(), 0); + /// ``` + #[must_use] + pub fn len(&self) -> usize { + self.params.len() + } + + /// Returns `true` if the path parameters are empty. + /// + /// # Examples + /// + /// ``` + /// use cot_core::request::PathParams; + /// + /// let path_params = PathParams::new(); + /// assert!(path_params.is_empty()); + /// ``` + #[must_use] + pub fn is_empty(&self) -> bool { + self.params.is_empty() + } + + /// Returns the value of a path parameter. + /// + /// # Examples + /// + /// ``` + /// use cot_core::request::PathParams; + /// + /// let mut path_params = PathParams::new(); + /// path_params.insert("name".into(), "world".into()); + /// assert_eq!(path_params.get("name"), Some("world")); + /// ``` + #[must_use] + pub fn get(&self, name: &str) -> Option<&str> { + self.params.get(name).map(String::as_str) + } + + /// Returns the value of a path parameter at the given index. + /// + /// # Examples + /// + /// ``` + /// use cot_core::request::PathParams; + /// + /// let mut path_params = PathParams::new(); + /// path_params.insert("name".into(), "world".into()); + /// assert_eq!(path_params.get_index(0), Some("world")); + /// ``` + #[must_use] + pub fn get_index(&self, index: usize) -> Option<&str> { + self.params + .get_index(index) + .map(|(_, value)| value.as_str()) + } + + /// Returns the key of a path parameter at the given index. + /// + /// # Examples + /// + /// ``` + /// use cot_core::request::PathParams; + /// + /// let mut path_params = PathParams::new(); + /// path_params.insert("name".into(), "world".into()); + /// assert_eq!(path_params.key_at_index(0), Some("name")); + /// ``` + #[must_use] + pub fn key_at_index(&self, index: usize) -> Option<&str> { + self.params.get_index(index).map(|(key, _)| key.as_str()) + } + + /// Deserializes the path parameters into a type `T` implementing + /// `serde::DeserializeOwned`. + /// + /// # Errors + /// + /// Throws an error if the path parameters could not be deserialized. + /// + /// # Examples + /// + /// ``` + /// use cot_core::request::PathParams; + /// + /// # fn main() -> Result<(), cot::Error> { + /// let mut path_params = PathParams::new(); + /// path_params.insert("hello".into(), "world".into()); + /// + /// let hello: String = path_params.parse()?; + /// assert_eq!(hello, "world"); + /// # Ok(()) + /// # } + /// ``` + /// + /// ``` + /// use cot_core::request::PathParams; + /// + /// # fn main() -> Result<(), cot::Error> { + /// let mut path_params = PathParams::new(); + /// path_params.insert("hello".into(), "world".into()); + /// path_params.insert("name".into(), "john".into()); + /// + /// let (hello, name): (String, String) = path_params.parse()?; + /// assert_eq!(hello, "world"); + /// assert_eq!(name, "john"); + /// # Ok(()) + /// # } + /// ``` + /// + /// ``` + /// use cot_core::request::PathParams; + /// use serde::Deserialize; + /// + /// # fn main() -> Result<(), cot::Error> { + /// let mut path_params = PathParams::new(); + /// path_params.insert("hello".into(), "world".into()); + /// path_params.insert("name".into(), "john".into()); + /// + /// #[derive(Deserialize)] + /// struct Params { + /// hello: String, + /// name: String, + /// } + /// + /// let params: Params = path_params.parse()?; + /// assert_eq!(params.hello, "world"); + /// assert_eq!(params.name, "john"); + /// # Ok(()) + /// # } + /// ``` + pub fn parse<'de, T: serde::Deserialize<'de>>( + &'de self, + ) -> Result { + let deserializer = path_params_deserializer::PathParamsDeserializer::new(self); + serde_path_to_error::deserialize(deserializer).map_err(PathParamsDeserializerError) + } +} + +/// An error that occurs when deserializing path parameters. +#[derive(Debug, Clone, thiserror::Error)] +#[error("could not parse path parameters: {0}")] +pub struct PathParamsDeserializerError( + // A wrapper over the original deserializer error. The exact error reason + // shouldn't be useful to the user, hence we're not exposing it. + #[source] serde_path_to_error::Error, +); +impl_into_cot_error!(PathParamsDeserializerError, BAD_REQUEST); + +#[repr(transparent)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct AppName(pub String); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn path_params() { + let mut path_params = PathParams::new(); + path_params.insert("name".into(), "world".into()); + + assert_eq!(path_params.get("name"), Some("world")); + assert_eq!(path_params.get("missing"), None); + } + + #[test] + fn path_params_parse() { + #[derive(Debug, PartialEq, Eq, serde::Deserialize)] + struct Params { + hello: String, + foo: String, + } + + let mut path_params = PathParams::new(); + path_params.insert("hello".into(), "world".into()); + path_params.insert("foo".into(), "bar".into()); + + let params: Params = path_params.parse().unwrap(); + assert_eq!( + params, + Params { + hello: "world".to_string(), + foo: "bar".to_string(), + } + ); + } + + #[cot_macros::test] + async fn path_extract_from_head() { + let (mut head, _) = Request::new(Body::empty()).into_parts(); + + let mut params = PathParams::new(); + params.insert("id".to_string(), "42".to_string()); + head.extensions.insert(params); + + let Path(id): Path = head.extract_from_head().await.unwrap(); + assert_eq!(id, "42"); + } +} diff --git a/cot-core/src/request/extractors.rs b/cot-core/src/request/extractors.rs new file mode 100644 index 00000000..8faa79a2 --- /dev/null +++ b/cot-core/src/request/extractors.rs @@ -0,0 +1,389 @@ +//! Extractors for request data. +//! +//! An extractor is a function that extracts data from a request. The main +//! benefit of using an extractor is that it can be used directly as a parameter +//! in a route handler. +//! +//! An extractor implements either [`FromRequest`] or [`FromRequestHead`]. +//! There are two variants because the request body can only be read once, so it +//! needs to be read in the [`FromRequest`] implementation. Therefore, there can +//! only be one extractor that implements [`FromRequest`] per route handler. +//! +//! # Examples +//! +//! For example, the [`Path`] extractor is used to extract path parameters: +//! +//! ``` +//! use cot::html::Html; +//! use cot::router::{Route, Router}; +//! use cot::test::TestRequestBuilder; +//! use cot_core::request::extractors::{FromRequest, Path}; +//! use cot_core::request::{Request, RequestExt}; +//! +//! async fn my_handler(Path(my_param): Path) -> Html { +//! Html::new(format!("Hello {my_param}!")) +//! } +//! +//! # #[tokio::main] +//! # async fn main() -> cot::Result<()> { +//! let router = Router::with_urls([Route::with_handler_and_name( +//! "/{my_param}/", +//! my_handler, +//! "home", +//! )]); +//! let request = TestRequestBuilder::get("/world/") +//! .router(router.clone()) +//! .build(); +//! +//! assert_eq!( +//! router +//! .handle(request) +//! .await? +//! .into_body() +//! .into_bytes() +//! .await?, +//! "Hello world!" +//! ); +//! # Ok(()) +//! # } +//! ``` + +use std::future::Future; + +use serde::de::DeserializeOwned; + +use crate::request::{PathParams, Request, RequestHead}; +use crate::{Body, Method}; + +/// Trait for extractors that consume the request body. +/// +/// Extractors implementing this trait are used in route handlers that consume +/// the request body and therefore can only be used once per request. +/// +/// See [`crate::request::extractors`] documentation for more information about +/// extractors. +pub trait FromRequest: Sized { + /// Extracts data from the request. + /// + /// # Errors + /// + /// Throws an error if the extractor fails to extract the data from the + /// request. + fn from_request( + head: &RequestHead, + body: Body, + ) -> impl Future> + Send; +} + +impl FromRequest for Request { + async fn from_request(head: &RequestHead, body: Body) -> crate::Result { + Ok(Request::from_parts(head.clone(), body)) + } +} + +/// Trait for extractors that don't consume the request body. +/// +/// Extractors implementing this trait are used in route handlers that don't +/// consume the request and therefore can be used multiple times per request. +/// +/// If you need to consume the body of the request, use [`FromRequest`] instead. +/// +/// See [`crate::request::extractors`] documentation for more information about +/// extractors. +pub trait FromRequestHead: Sized { + /// Extracts data from the request head. + /// + /// # Errors + /// + /// Throws an error if the extractor fails to extract the data from the + /// request head. + fn from_request_head(head: &RequestHead) -> impl Future> + Send; +} + +/// An extractor that extracts data from the URL params. +/// +/// The extractor is generic over a type that implements +/// `serde::de::DeserializeOwned`. +/// +/// # Examples +/// +/// ``` +/// use cot::html::Html; +/// use cot::router::{Route, Router}; +/// use cot::test::TestRequestBuilder; +/// use cot_core::request::extractors::{FromRequest, Path}; +/// use cot_core::request::{Request, RequestExt}; +/// +/// async fn my_handler(Path(my_param): Path) -> Html { +/// Html::new(format!("Hello {my_param}!")) +/// } +/// +/// # #[tokio::main] +/// # async fn main() -> cot::Result<()> { +/// let router = Router::with_urls([Route::with_handler_and_name( +/// "/{my_param}/", +/// my_handler, +/// "home", +/// )]); +/// let request = TestRequestBuilder::get("/world/") +/// .router(router.clone()) +/// .build(); +/// +/// assert_eq!( +/// router +/// .handle(request) +/// .await? +/// .into_body() +/// .into_bytes() +/// .await?, +/// "Hello world!" +/// ); +/// # Ok(()) +/// # } +/// ``` +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Path(pub D); + +impl FromRequestHead for Path { + async fn from_request_head(head: &RequestHead) -> crate::Result { + let params = head + .extensions + .get::() + .expect("PathParams extension missing") + .parse()?; + Ok(Self(params)) + } +} + +/// An extractor that extracts data from the URL query parameters. +/// +/// The extractor is generic over a type that implements +/// `serde::de::DeserializeOwned`. +/// +/// # Example +/// +/// ``` +/// use cot::RequestHandler; +/// use cot::html::Html; +/// use cot::router::{Route, Router}; +/// use cot::test::TestRequestBuilder; +/// use cot_core::request::extractors::{FromRequest, UrlQuery}; +/// use serde::Deserialize; +/// +/// #[derive(Deserialize)] +/// struct MyQuery { +/// hello: String, +/// } +/// +/// async fn my_handler(UrlQuery(query): UrlQuery) -> Html { +/// Html::new(format!("Hello {}!", query.hello)) +/// } +/// +/// # #[tokio::main] +/// # async fn main() -> cot::Result<()> { +/// let request = TestRequestBuilder::get("/?hello=world").build(); +/// +/// assert_eq!( +/// my_handler +/// .handle(request) +/// .await? +/// .into_body() +/// .into_bytes() +/// .await?, +/// "Hello world!" +/// ); +/// # Ok(()) +/// # } +/// ``` +#[derive(Debug, Clone, Copy, Default)] +pub struct UrlQuery(pub T); + +impl FromRequestHead for UrlQuery +where + D: DeserializeOwned, +{ + async fn from_request_head(head: &RequestHead) -> crate::Result { + let query = head.uri.query().unwrap_or_default(); + + let deserializer = + serde_html_form::Deserializer::new(form_urlencoded::parse(query.as_bytes())); + + let value = + serde_path_to_error::deserialize(deserializer).map_err(QueryParametersParseError)?; + + Ok(UrlQuery(value)) + } +} + +#[derive(Debug, thiserror::Error)] +#[error("could not parse query parameters: {0}")] +struct QueryParametersParseError(serde_path_to_error::Error); +impl_into_cot_error!(QueryParametersParseError, BAD_REQUEST); + +// extractor impls for existing types +impl FromRequestHead for RequestHead { + async fn from_request_head(head: &RequestHead) -> crate::Result { + Ok(head.clone()) + } +} + +impl FromRequestHead for Method { + async fn from_request_head(head: &RequestHead) -> crate::Result { + Ok(head.method.clone()) + } +} + +/// A derive macro that automatically implements the [`FromRequestHead`] trait +/// for structs. +/// +/// This macro generates code to extract each field of the struct from HTTP +/// request head, making it easy to create composite extractors that combine +/// multiple data sources from an incoming request. +/// +/// The macro works by calling [`FromRequestHead::from_request_head`] on each +/// field's type, allowing you to compose extractors seamlessly. All fields must +/// implement the [`FromRequestHead`] trait for the derivation to work. +/// +/// # Requirements +/// +/// - The target struct must have all fields implement [`FromRequestHead`] +/// - Works with named fields, unnamed fields (tuple structs), and unit structs +/// - The struct must be accessible where the macro is used +/// +/// # Examples +/// +/// ## Named Fields +/// +/// ```no_run +/// use cot::router::Urls; +/// use cot_core::request::extractors::{Path, StaticFiles, UrlQuery}; +/// use cot_macros::FromRequestHead; +/// use serde::Deserialize; +/// +/// #[derive(Debug, FromRequestHead)] +/// pub struct BaseContext { +/// urls: Urls, +/// static_files: StaticFiles, +/// } +/// ``` +pub use cot_macros::FromRequestHead; + +use crate::impl_into_cot_error; + +#[cfg(test)] +mod tests { + use cot::html::Html; + use cot::router::{Route, Router, Urls}; + use cot::test::TestRequestBuilder; + use serde::Deserialize; + + use super::*; + use crate::request::extractors::{FromRequest, Path, UrlQuery}; + + #[cot_macros::test] + async fn path_extraction() { + #[derive(Deserialize, Debug, PartialEq)] + struct TestParams { + id: i32, + name: String, + } + + let (mut head, _body) = Request::new(Body::empty()).into_parts(); + + let mut params = PathParams::new(); + params.insert("id".to_string(), "42".to_string()); + params.insert("name".to_string(), "test".to_string()); + head.extensions.insert(params); + + let Path(extracted): Path = Path::from_request_head(&head).await.unwrap(); + let expected = TestParams { + id: 42, + name: "test".to_string(), + }; + + assert_eq!(extracted, expected); + } + + #[cot_macros::test] + async fn url_query_extraction() { + #[derive(Deserialize, Debug, PartialEq)] + struct QueryParams { + page: i32, + filter: String, + } + + let (mut head, _body) = Request::new(Body::empty()).into_parts(); + head.uri = "https://example.com/?page=2&filter=active".parse().unwrap(); + + let UrlQuery(query): UrlQuery = + UrlQuery::from_request_head(&head).await.unwrap(); + + assert_eq!(query.page, 2); + assert_eq!(query.filter, "active"); + } + + #[cot_macros::test] + async fn url_query_empty() { + #[derive(Deserialize, Debug, PartialEq)] + struct EmptyParams {} + + let (mut head, _body) = Request::new(Body::empty()).into_parts(); + head.uri = "https://example.com/".parse().unwrap(); + + let result: UrlQuery = UrlQuery::from_request_head(&head).await.unwrap(); + assert!(matches!(result, UrlQuery(_))); + } + + #[cot_macros::test] + async fn request_form() { + #[derive(Debug, PartialEq, Eq, Form)] + struct MyForm { + hello: String, + foo: String, + } + + let request = TestRequestBuilder::post("/") + .form_data(&[("hello", "world"), ("foo", "bar")]) + .build(); + + let (head, body) = request.into_parts(); + let RequestForm(form_result): RequestForm = + RequestForm::from_request(&head, body).await.unwrap(); + + assert_eq!( + form_result.unwrap(), + MyForm { + hello: "world".to_string(), + foo: "bar".to_string(), + } + ); + } + + #[cot_macros::test] + async fn urls_extraction() { + async fn handler() -> Html { + Html::new("") + } + + let router = Router::with_urls([Route::with_handler_and_name( + "/test/", + handler, + "test_route", + )]); + + let mut request = TestRequestBuilder::get("/test/").router(router).build(); + + let urls: Urls = request.extract_from_head().await.unwrap(); + + assert!(reverse!(urls, "test_route").is_ok()); + } + + #[cot_macros::test] + async fn method_extraction() { + let mut request = TestRequestBuilder::get("/test/").build(); + + let method: Method = request.extract_from_head().await.unwrap(); + + assert_eq!(method, Method::GET); + } +} diff --git a/cot/src/request/path_params_deserializer.rs b/cot-core/src/request/path_params_deserializer.rs similarity index 99% rename from cot/src/request/path_params_deserializer.rs rename to cot-core/src/request/path_params_deserializer.rs index 73330e3c..17ba5b86 100644 --- a/cot/src/request/path_params_deserializer.rs +++ b/cot-core/src/request/path_params_deserializer.rs @@ -8,7 +8,7 @@ use crate::request::PathParams; /// An error that occurs when deserializing path parameters. #[derive(Debug, Clone, PartialEq, Eq, Hash, Error)] -pub(super) enum PathParamsDeserializerError { +pub enum PathParamsDeserializerError { /// Invalid number of path parameters #[error("invalid number of path parameters: expected {expected}, got {actual}")] InvalidParamNumber { @@ -57,13 +57,13 @@ impl serde::de::Error for PathParamsDeserializerError { } #[derive(Debug)] -pub(super) struct PathParamsDeserializer<'de> { +pub struct PathParamsDeserializer<'de> { path_params: &'de PathParams, } impl<'de> PathParamsDeserializer<'de> { #[must_use] - pub(super) fn new(path_params: &'de PathParams) -> Self { + pub fn new(path_params: &'de PathParams) -> Self { Self { path_params } } diff --git a/cot-core/src/response.rs b/cot-core/src/response.rs new file mode 100644 index 00000000..35cb1888 --- /dev/null +++ b/cot-core/src/response.rs @@ -0,0 +1,192 @@ +//! HTTP response type and helper methods. +//! +//! Cot uses the [`Response`](http::Response) type from the [`http`] crate +//! to represent outgoing HTTP responses. However, it also provides a +//! [`ResponseExt`] trait that contain various helper methods for working with +//! HTTP responses. These methods are used to create new responses with HTML +//! content types, redirects, and more. You probably want to have a `use` +//! statement for [`ResponseExt`] in your code most of the time to be able to +//! use these functions: +//! +//! ``` +//! use cot_core::response::ResponseExt; +//! ``` + +mod into_response; + +/// Derive macro for the [`IntoResponse`] trait. +/// +/// This macro can be applied to enums to automatically implement the +/// [`IntoResponse`] trait. The enum must consist of tuple variants with +/// exactly one field each, where each field type implements [`IntoResponse`]. +/// +/// # Requirements +/// +/// - **Only enums are supported**: This macro will produce a compile error if +/// applied to structs or unions. +/// - **Tuple variants with one field**: Each enum variant must be a tuple +/// variant with exactly one field (e.g., `Variant(Type)`). +/// - **Field types must implement `IntoResponse`**: Each field type must +/// implement the [`IntoResponse`] trait. +/// +/// # Generated Implementation +/// +/// The macro generates an implementation that matches on the enum variants and +/// calls `into_response()` on the inner value: +/// +/// ```compile_fail +/// impl IntoResponse for MyEnum { +/// fn into_response(self) -> cot::Result { +/// use cot::response::IntoResponse; +/// match self { +/// Self::Variant1(inner) => inner.into_response(), +/// Self::Variant2(inner) => inner.into_response(), +/// // ... for each variant +/// } +/// } +/// } +/// ``` +/// +/// # Examples +/// +/// ``` +/// use cot::html::Html; +/// use cot::json::Json; +/// use cot_core::response::IntoResponse; +/// +/// #[derive(IntoResponse)] +/// enum MyResponse { +/// Json(Json), +/// Html(Html), +/// } +/// ``` +/// +/// [`IntoResponse`]: cot_core::response::IntoResponse +pub use cot_macros::IntoResponse; +pub use into_response::{ + IntoResponse, WithBody, WithContentType, WithExtension, WithHeader, WithStatus, +}; + +use crate::{Body, StatusCode}; + +const RESPONSE_BUILD_FAILURE: &str = "Failed to build response"; + +/// HTTP response type. +pub type Response = http::Response; + +/// HTTP response head type. +pub type ResponseHead = http::response::Parts; + +mod private { + pub trait Sealed {} +} + +/// Extension trait for [`http::Response`] that provides helper methods for +/// working with HTTP response. +/// +/// # Sealed +/// +/// This trait is sealed since it doesn't make sense to be implemented for types +/// outside the context of Cot. +pub trait ResponseExt: Sized + private::Sealed { + /// Create a new response builder. + /// + /// # Examples + /// + /// ``` + /// use cot_core::response::{Response, ResponseExt}; + /// use cot_core::{Body, StatusCode}; + /// + /// let response = Response::builder() + /// .status(StatusCode::OK) + /// .body(Body::empty()) + /// .expect("Failed to build response"); + /// ``` + #[must_use] + fn builder() -> http::response::Builder; + + /// Create a new redirect response. + /// + /// This creates a new [`Response`] object with a status code of + /// [`StatusCode::SEE_OTHER`] and a location header set to the provided + /// location. + /// + /// # Examples + /// + /// ``` + /// use cot_core::StatusCode; + /// use cot_core::response::{Response, ResponseExt}; + /// + /// let response = Response::new_redirect("http://example.com"); + /// ``` + /// + /// # See also + /// + /// * [`cot::reverse_redirect!`] – a more ergonomic way to create redirects + /// to internal views + #[must_use] + fn new_redirect>(location: T) -> Self; +} + +impl private::Sealed for Response {} + +impl ResponseExt for Response { + fn builder() -> http::response::Builder { + http::Response::builder() + } + + fn new_redirect>(location: T) -> Self { + http::Response::builder() + .status(StatusCode::SEE_OTHER) + .header(http::header::LOCATION, location.into()) + .body(Body::empty()) + .expect(RESPONSE_BUILD_FAILURE) + } +} + +#[cfg(test)] +mod tests { + use cot::headers::JSON_CONTENT_TYPE; + + use super::*; + use crate::body::BodyInner; + use crate::response::{Response, ResponseExt}; + + #[test] + #[cfg(feature = "json")] + fn response_new_json() { + #[derive(serde::Serialize)] + struct MyData { + hello: String, + } + + let data = MyData { + hello: String::from("world"), + }; + let response = cot::json::Json(data).into_response().unwrap(); + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response.headers().get(http::header::CONTENT_TYPE).unwrap(), + JSON_CONTENT_TYPE + ); + match &response.body().inner { + BodyInner::Fixed(fixed) => { + assert_eq!(fixed, r#"{"hello":"world"}"#); + } + _ => { + panic!("Expected fixed body"); + } + } + } + + #[test] + fn response_new_redirect() { + let location = "http://example.com"; + let response = Response::new_redirect(location); + assert_eq!(response.status(), StatusCode::SEE_OTHER); + assert_eq!( + response.headers().get(http::header::LOCATION).unwrap(), + location + ); + } +} diff --git a/cot-core/src/response/into_response.rs b/cot-core/src/response/into_response.rs new file mode 100644 index 00000000..9382ddcc --- /dev/null +++ b/cot-core/src/response/into_response.rs @@ -0,0 +1,719 @@ +use bytes::{Bytes, BytesMut}; +use http; + +use crate::headers::{HTML_CONTENT_TYPE, OCTET_STREAM_CONTENT_TYPE, PLAIN_TEXT_CONTENT_TYPE}; +use crate::html::Html; +use crate::response::Response; +use crate::{Body, Error, StatusCode}; + +/// Trait for generating responses. +/// Types that implement `IntoResponse` can be returned from handlers. +/// +/// # Implementing `IntoResponse` +/// +/// You generally shouldn't have to implement `IntoResponse` manually, as cot +/// provides implementations for many common types. +/// +/// However, it might be necessary if you have a custom error type that you want +/// to return from handlers. +pub trait IntoResponse { + /// Converts the implementing type into a `cot::Result`. + /// + /// # Errors + /// Returns an error if the conversion fails. + fn into_response(self) -> crate::Result; + + /// Modifies the response by appending the specified header. + /// + /// # Errors + /// Returns an error if the header name or value is invalid. + fn with_header(self, key: K, value: V) -> WithHeader + where + K: TryInto, + V: TryInto, + Self: Sized, + { + let key = key.try_into().ok(); + let value = value.try_into().ok(); + + WithHeader { + inner: self, + header: key.zip(value), + } + } + + /// Modifies the response by setting the `Content-Type` header. + /// + /// # Errors + /// Returns an error if the content type value is invalid. + fn with_content_type(self, content_type: V) -> WithContentType + where + V: TryInto, + Self: Sized, + { + WithContentType { + inner: self, + content_type: content_type.try_into().ok(), + } + } + + /// Modifies the response by setting the status code. + /// + /// # Errors + /// Returns an error if the `IntoResponse` conversion fails. + fn with_status(self, status: StatusCode) -> WithStatus + where + Self: Sized, + { + WithStatus { + inner: self, + status, + } + } + + /// Modifies the response by setting the body. + /// + /// # Errors + /// Returns an error if the `IntoResponse` conversion fails. + fn with_body(self, body: impl Into) -> WithBody + where + Self: Sized, + { + WithBody { + inner: self, + body: body.into(), + } + } + + /// Modifies the response by inserting an extension. + /// + /// # Errors + /// Returns an error if the `IntoResponse` conversion fails. + fn with_extension(self, extension: T) -> WithExtension + where + T: Clone + Send + Sync + 'static, + Self: Sized, + { + WithExtension { + inner: self, + extension, + } + } +} + +/// Returned by [`with_header`](IntoResponse::with_header) method. +#[derive(Debug)] +pub struct WithHeader { + inner: T, + header: Option<(http::HeaderName, http::HeaderValue)>, +} + +impl IntoResponse for WithHeader { + fn into_response(self) -> crate::Result { + self.inner.into_response().map(|mut resp| { + if let Some((key, value)) = self.header { + resp.headers_mut().append(key, value); + } + resp + }) + } +} + +/// Returned by [`with_content_type`](IntoResponse::with_content_type) method. +#[derive(Debug)] +pub struct WithContentType { + inner: T, + content_type: Option, +} + +impl IntoResponse for WithContentType { + fn into_response(self) -> crate::Result { + self.inner.into_response().map(|mut resp| { + if let Some(content_type) = self.content_type { + resp.headers_mut() + .insert(http::header::CONTENT_TYPE, content_type); + } + resp + }) + } +} + +/// Returned by [`with_status`](IntoResponse::with_status) method. +#[derive(Debug)] +pub struct WithStatus { + inner: T, + status: StatusCode, +} + +impl IntoResponse for WithStatus { + fn into_response(self) -> crate::Result { + self.inner.into_response().map(|mut resp| { + *resp.status_mut() = self.status; + resp + }) + } +} + +/// Returned by [`with_body`](IntoResponse::with_body) method. +#[derive(Debug)] +pub struct WithBody { + inner: T, + body: Body, +} + +impl IntoResponse for WithBody { + fn into_response(self) -> crate::Result { + self.inner.into_response().map(|mut resp| { + *resp.body_mut() = self.body; + resp + }) + } +} + +/// Returned by [`with_extension`](IntoResponse::with_extension) method. +#[derive(Debug)] +pub struct WithExtension { + inner: T, + extension: D, +} + +impl IntoResponse for WithExtension +where + T: IntoResponse, + D: Clone + Send + Sync + 'static, +{ + fn into_response(self) -> crate::Result { + self.inner.into_response().map(|mut resp| { + resp.extensions_mut().insert(self.extension); + resp + }) + } +} + +macro_rules! impl_into_response_for_type_and_mime { + ($ty:ty, $mime:expr) => { + impl IntoResponse for $ty { + fn into_response(self) -> crate::Result { + Body::from(self) + .with_header(http::header::CONTENT_TYPE, $mime) + .into_response() + } + } + }; +} + +// General implementations + +impl IntoResponse for () { + fn into_response(self) -> crate::Result { + Body::empty().into_response() + } +} + +impl IntoResponse for Result +where + R: IntoResponse, + E: Into, +{ + fn into_response(self) -> crate::Result { + match self { + Ok(value) => value.into_response(), + Err(err) => Err(err.into()), + } + } +} + +impl IntoResponse for Error { + fn into_response(self) -> crate::Result { + Err(self) + } +} + +impl IntoResponse for Response { + fn into_response(self) -> crate::Result { + Ok(self) + } +} + +// Text implementations + +impl_into_response_for_type_and_mime!(&'static str, PLAIN_TEXT_CONTENT_TYPE); +impl_into_response_for_type_and_mime!(String, PLAIN_TEXT_CONTENT_TYPE); + +impl IntoResponse for Box { + fn into_response(self) -> crate::Result { + String::from(self).into_response() + } +} + +// Bytes implementations + +impl_into_response_for_type_and_mime!(&'static [u8], OCTET_STREAM_CONTENT_TYPE); +impl_into_response_for_type_and_mime!(Vec, OCTET_STREAM_CONTENT_TYPE); +impl_into_response_for_type_and_mime!(Bytes, OCTET_STREAM_CONTENT_TYPE); + +impl IntoResponse for &'static [u8; N] { + fn into_response(self) -> crate::Result { + self.as_slice().into_response() + } +} + +impl IntoResponse for [u8; N] { + fn into_response(self) -> crate::Result { + self.to_vec().into_response() + } +} + +impl IntoResponse for Box<[u8]> { + fn into_response(self) -> crate::Result { + Vec::from(self).into_response() + } +} + +impl IntoResponse for BytesMut { + fn into_response(self) -> crate::Result { + self.freeze().into_response() + } +} + +// HTTP structures for common uses + +impl IntoResponse for StatusCode { + fn into_response(self) -> crate::Result { + ().into_response().with_status(self).into_response() + } +} + +impl IntoResponse for http::HeaderMap { + fn into_response(self) -> crate::Result { + ().into_response().map(|mut resp| { + *resp.headers_mut() = self; + resp + }) + } +} + +impl IntoResponse for http::Extensions { + fn into_response(self) -> crate::Result { + ().into_response().map(|mut resp| { + *resp.extensions_mut() = self; + resp + }) + } +} + +impl IntoResponse for crate::response::ResponseHead { + fn into_response(self) -> crate::Result { + Ok(Response::from_parts(self, Body::empty())) + } +} + +impl IntoResponse for Body { + fn into_response(self) -> crate::Result { + Ok(Response::new(self)) + } +} + +impl IntoResponse for Html { + /// Create a new HTML response. + /// + /// This creates a new [`Response`] object with a content type of + /// `text/html; charset=utf-8` and given body. + /// + /// # Examples + /// + /// ``` + /// use cot_core::html::Html; + /// use cot_core::response::IntoResponse; + /// + /// let html = Html::new("
Hello
"); + /// + /// let response = html.into_response(); + /// ``` + fn into_response(self) -> crate::Result { + self.0 + .into_response() + .with_content_type(HTML_CONTENT_TYPE) + .into_response() + } +} + +#[cfg(test)] +mod tests { + use bytes::{Bytes, BytesMut}; + use http::{self, HeaderMap, HeaderValue}; + + use super::*; + use crate::error::NotFound; + use crate::response::Response; + use crate::{Body, StatusCode}; + + #[cot_macros::test] + async fn test_unit_into_response() { + let response = ().into_response().unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + assert!(response.headers().is_empty()); + assert_eq!(response.into_body().into_bytes().await.unwrap().len(), 0); + } + + #[cot_macros::test] + async fn test_result_ok_into_response() { + let res: Result<&'static str, Error> = Ok("hello"); + + let response = res.into_response().unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response.headers().get(http::header::CONTENT_TYPE).unwrap(), + "text/plain; charset=utf-8" + ); + assert_eq!(response.into_body().into_bytes().await.unwrap(), "hello"); + } + + #[cot_macros::test] + async fn test_result_err_into_response() { + let err = Error::from(NotFound::with_message("test")); + let res: Result<&'static str, Error> = Err(err); + + let error_result = res.into_response(); + + assert!(error_result.is_err()); + assert!(error_result.err().unwrap().to_string().contains("test")); + } + + #[cot_macros::test] + async fn test_response_into_response() { + let original_response = Response::new(Body::from("test")); + + let response = original_response.into_response().unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + assert_eq!(response.into_body().into_bytes().await.unwrap(), "test"); + } + + #[cot_macros::test] + async fn test_static_str_into_response() { + let response = "hello world".into_response().unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response.headers().get(http::header::CONTENT_TYPE).unwrap(), + "text/plain; charset=utf-8" + ); + assert_eq!( + response.into_body().into_bytes().await.unwrap(), + "hello world" + ); + } + + #[cot_macros::test] + async fn test_string_into_response() { + let s = String::from("hello string"); + + let response = s.into_response().unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response.headers().get(http::header::CONTENT_TYPE).unwrap(), + "text/plain; charset=utf-8" + ); + assert_eq!( + response.into_body().into_bytes().await.unwrap(), + "hello string" + ); + } + + #[cot_macros::test] + async fn test_box_str_into_response() { + let b: Box = "hello box".into(); + + let response = b.into_response().unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response.headers().get(http::header::CONTENT_TYPE).unwrap(), + "text/plain; charset=utf-8" + ); + assert_eq!( + response.into_body().into_bytes().await.unwrap(), + "hello box" + ); + } + + #[cot_macros::test] + async fn test_static_u8_slice_into_response() { + let data: &'static [u8] = b"hello bytes"; + + let response = data.into_response().unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response.headers().get(http::header::CONTENT_TYPE).unwrap(), + "application/octet-stream" + ); + assert_eq!( + response.into_body().into_bytes().await.unwrap(), + "hello bytes" + ); + } + + #[cot_macros::test] + async fn test_vec_u8_into_response() { + let data: Vec = vec![1, 2, 3]; + + let response = data.into_response().unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response.headers().get(http::header::CONTENT_TYPE).unwrap(), + "application/octet-stream" + ); + assert_eq!( + response.into_body().into_bytes().await.unwrap(), + Bytes::from(vec![1, 2, 3]) + ); + } + + #[cot_macros::test] + async fn test_bytes_into_response() { + let data = Bytes::from_static(b"hello bytes obj"); + + let response = data.into_response().unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response.headers().get(http::header::CONTENT_TYPE).unwrap(), + "application/octet-stream" + ); + assert_eq!( + response.into_body().into_bytes().await.unwrap(), + "hello bytes obj" + ); + } + + #[cot_macros::test] + async fn test_static_u8_array_into_response() { + let data: &'static [u8; 5] = b"array"; + + let response = data.into_response().unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response.headers().get(http::header::CONTENT_TYPE).unwrap(), + "application/octet-stream" + ); + assert_eq!(response.into_body().into_bytes().await.unwrap(), "array"); + } + + #[cot_macros::test] + async fn test_u8_array_into_response() { + let data: [u8; 3] = [4, 5, 6]; + + let response = data.into_response().unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response.headers().get(http::header::CONTENT_TYPE).unwrap(), + "application/octet-stream" + ); + assert_eq!( + response.into_body().into_bytes().await.unwrap(), + Bytes::from(vec![4, 5, 6]) + ); + } + + #[cot_macros::test] + async fn test_box_u8_slice_into_response() { + let data: Box<[u8]> = Box::new([7, 8, 9]); + + let response = data.into_response().unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response.headers().get(http::header::CONTENT_TYPE).unwrap(), + "application/octet-stream" + ); + assert_eq!( + response.into_body().into_bytes().await.unwrap(), + Bytes::from(vec![7, 8, 9]) + ); + } + + #[cot_macros::test] + async fn test_bytes_mut_into_response() { + let mut data = BytesMut::with_capacity(10); + data.extend_from_slice(b"mutable"); + + let response = data.into_response().unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response.headers().get(http::header::CONTENT_TYPE).unwrap(), + "application/octet-stream" + ); + assert_eq!(response.into_body().into_bytes().await.unwrap(), "mutable"); + } + + #[cot_macros::test] + async fn test_status_code_into_response() { + let response = StatusCode::NOT_FOUND.into_response().unwrap(); + + assert_eq!(response.status(), StatusCode::NOT_FOUND); + assert!(response.headers().is_empty()); + assert_eq!(response.into_body().into_bytes().await.unwrap().len(), 0); + } + + #[cot_macros::test] + async fn test_header_map_into_response() { + let mut headers = HeaderMap::new(); + headers.insert("X-Test", HeaderValue::from_static("value")); + + let response = headers.into_response().unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + assert_eq!(response.headers().get("X-Test").unwrap(), "value"); + assert_eq!(response.into_body().into_bytes().await.unwrap().len(), 0); + } + + #[cot_macros::test] + async fn test_extensions_into_response() { + let mut extensions = http::Extensions::new(); + extensions.insert("My Extension Data"); + + let response = extensions.into_response().unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + assert!(response.headers().is_empty()); + assert_eq!( + response.extensions().get::<&str>(), + Some(&"My Extension Data") + ); + assert_eq!(response.into_body().into_bytes().await.unwrap().len(), 0); + } + + #[cot_macros::test] + async fn test_parts_into_response() { + let mut response = Response::new(Body::empty()); + *response.status_mut() = StatusCode::ACCEPTED; + response + .headers_mut() + .insert("X-From-Parts", HeaderValue::from_static("yes")); + response.extensions_mut().insert(123usize); + let (head, _) = response.into_parts(); + + let new_response = head.into_response().unwrap(); + + assert_eq!(new_response.status(), StatusCode::ACCEPTED); + assert_eq!(new_response.headers().get("X-From-Parts").unwrap(), "yes"); + assert_eq!(new_response.extensions().get::(), Some(&123)); + assert_eq!( + new_response.into_body().into_bytes().await.unwrap().len(), + 0 + ); + } + + #[cot_macros::test] + async fn test_html_into_response() { + let html = Html::new("

Test

"); + + let response = html.into_response().unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response.headers().get(http::header::CONTENT_TYPE).unwrap(), + "text/html; charset=utf-8" + ); + assert_eq!( + response.into_body().into_bytes().await.unwrap(), + "

Test

" + ); + } + + #[cot_macros::test] + async fn test_body_into_response() { + let body = Body::from("body test"); + + let response = body.into_response().unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response.headers().get(http::header::CONTENT_TYPE), + None // Body itself doesn't set content-type + ); + assert_eq!( + response.into_body().into_bytes().await.unwrap(), + "body test" + ); + } + + #[cot_macros::test] + async fn test_with_header() { + let response = "test" + .with_header("X-Custom", "HeaderValue") + .into_response() + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + assert_eq!(response.headers().get("X-Custom").unwrap(), "HeaderValue"); + assert_eq!(response.into_body().into_bytes().await.unwrap(), "test"); + } + + #[cot_macros::test] + async fn test_with_content_type() { + let response = "test" + .with_content_type("application/json") + .into_response() + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response.headers().get(http::header::CONTENT_TYPE).unwrap(), + "application/json" + ); + assert_eq!(response.into_body().into_bytes().await.unwrap(), "test"); + } + + #[cot_macros::test] + async fn test_with_status() { + let response = "test" + .with_status(StatusCode::CREATED) + .into_response() + .unwrap(); + + assert_eq!(response.status(), StatusCode::CREATED); + assert_eq!( + response.headers().get(http::header::CONTENT_TYPE).unwrap(), + "text/plain; charset=utf-8" + ); + assert_eq!(response.into_body().into_bytes().await.unwrap(), "test"); + } + + #[cot_macros::test] + async fn test_with_body() { + let response = StatusCode::ACCEPTED + .with_body("new body") + .into_response() + .unwrap(); + + assert_eq!(response.status(), StatusCode::ACCEPTED); + assert_eq!(response.into_body().into_bytes().await.unwrap(), "new body"); + } + + #[cot_macros::test] + async fn test_with_extension() { + #[derive(Clone, Debug, PartialEq)] + struct MyExt(String); + + let response = "test" + .with_extension(MyExt("data".to_string())) + .into_response() + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response.extensions().get::(), + Some(&MyExt("data".to_string())) + ); + assert_eq!(response.into_body().into_bytes().await.unwrap(), "test"); + } +} diff --git a/cot-core/src/router.rs b/cot-core/src/router.rs new file mode 100644 index 00000000..170b4a58 --- /dev/null +++ b/cot-core/src/router.rs @@ -0,0 +1,1023 @@ +//! Router for passing requests to their respective views. +//! +//! # Examples +//! +//! ``` +//! use cot_core::request::Request; +//! use cot_core::response::Response; +//! use cot_core::router::{Route, Router}; +//! +//! async fn home(request: Request) -> cot::Result { +//! Ok(cot::reverse_redirect!(request, "get_page", page = 123)?) +//! } +//! +//! async fn get_page(request: Request) -> cot::Result { +//! unimplemented!() +//! } +//! +//! let router = Router::with_urls([Route::with_handler_and_name( +//! "/{page}", get_page, "get_page", +//! )]); +//! ``` + +use std::collections::HashMap; +use std::fmt::Formatter; +use std::future::Future; +use std::pin::Pin; +use std::sync::Arc; +use std::task::{Context, Poll}; + +use derive_more::with_trait::Debug; +use tracing::debug; + +use crate::error::NotFound; +use crate::handler::{BoxRequestHandler, RequestHandler, into_box_request_handler}; +use crate::request::{AppName, PathParams, Request, RequestHead, RouteName}; +use crate::response::Response; +use crate::router::path::{CaptureResult, PathMatcher, ReverseParamMap}; +use crate::{Error, Result, impl_into_cot_error}; + +pub mod method; +pub mod path; + +/// A router that can be used to route requests to their respective views. +/// +/// This struct is used to route requests to their respective views. It can be +/// created directly by calling the [`Router::with_urls`] method, and that's +/// what is typically done in [`project::App::router`] implementations. +/// +/// # Examples +/// +/// ``` +/// use cot_core::request::Request; +/// use cot_core::response::Response; +/// use cot_core::router::{Route, Router}; +/// +/// async fn home(request: Request) -> cot::Result { +/// unimplemented!() +/// } +/// +/// let router = Router::with_urls([Route::with_handler_and_name("/", home, "home")]); +/// ``` +#[derive(Clone, Debug)] +pub struct Router { + app_name: Option, + urls: Vec, + names: HashMap>, +} + +impl Router { + /// Create an empty router. + /// + /// This router will not route any requests. + /// + /// # Examples + /// + /// ``` + /// use cot_core::router::Router; + /// + /// let router = Router::empty(); + /// ``` + #[must_use] + pub fn empty() -> Self { + Self::with_urls(&[]) + } + + /// Create a router with the given routes. + /// + /// # Examples + /// + /// ``` + /// use cot_core::request::Request; + /// use cot_core::response::Response; + /// use cot_core::router::{Route, Router}; + /// + /// async fn home(request: Request) -> cot::Result { + /// unimplemented!() + /// } + /// + /// let router = Router::with_urls([Route::with_handler_and_name("/", home, "home")]); + /// ``` + #[must_use] + pub fn with_urls>>(urls: T) -> Self { + let urls = urls.into(); + let mut names = HashMap::new(); + + for url in &urls { + if let Some(name) = &url.name { + names.insert(name.clone(), url.url.clone()); + } + } + + Self { + app_name: None, + urls, + names, + } + } + + pub fn set_app_name(&mut self, app_name: AppName) { + self.app_name = Some(app_name); + } + + async fn route(&self, mut request: Request, request_path: &str) -> Result { + debug!("Routing request to {}", request_path); + + if let Some(result) = self.get_handler(request_path) { + let mut path_params = PathParams::new(); + for (key, value) in result.params.iter().rev() { + path_params.insert(key.clone(), value.clone()); + } + request.extensions_mut().insert(path_params); + if let Some(app_name) = result.app_name { + request.extensions_mut().insert(app_name); + } + if let Some(name) = result.name { + request.extensions_mut().insert(name); + } + result.handler.handle(request).await + } else { + debug!("Not found: {}", request_path); + Err(Error::from(NotFound::router())) + } + } + + fn get_handler(&self, request_path: &str) -> Option> { + for route in &self.urls { + if let Some(matches) = route.url.capture(request_path) { + let matches_fully = matches.matches_fully(); + + match &route.view { + RouteInner::Handler(handler) => { + if matches_fully { + return Some(HandlerFound { + handler: &**handler, + app_name: self.app_name.clone(), + name: route.name.clone(), + params: Self::matches_to_path_params(&matches, Vec::new()), + }); + } + } + RouteInner::Router(router) => { + if let Some(result) = router.get_handler(matches.remaining_path) { + return Some(HandlerFound { + handler: result.handler, + app_name: result.app_name.or_else(|| self.app_name.clone()), + name: result.name, + params: Self::matches_to_path_params(&matches, result.params), + }); + } + } + #[cfg(feature = "openapi")] + RouteInner::ApiHandler(handler) => { + if matches_fully { + return Some(HandlerFound { + // TODO: consider removing this when Rust trait_upcasting is + // stabilized and we bump the MSRV (lands in Rust 1.86) + handler: handler.as_box_request_handler(), + app_name: self.app_name.clone(), + name: route.name.clone(), + params: Self::matches_to_path_params(&matches, Vec::new()), + }); + } + } + } + } + } + + None + } + + fn matches_to_path_params( + matches: &CaptureResult<'_, '_>, + mut path_params: Vec<(String, String)>, + ) -> Vec<(String, String)> { + // Adding in reverse order, since we're doing this from the bottom up (we're + // going to reverse the order before running the handler) + for param in matches.params.iter().rev() { + path_params.push((param.name.to_owned(), param.value.clone())); + } + path_params + } + + /// Handle a request. + /// + /// This method is called by the [`CotApp`](cot::project::App) to handle + /// a request. + /// + /// # Errors + /// + /// This method re-throws any errors that occur in the request handler. + pub async fn handle(&self, request: Request) -> Result { + let path = request.uri().path().to_owned(); + self.route(request, &path).await + } + + /// Get a URL for a view by name. + /// + /// Instead of using this method directly, consider using the + /// [`reverse!`](crate::reverse) macro which provides much more ergonomic + /// way to call this. + /// + /// `app_name` is the name of the app that the view should be found in. If + /// `app_name` is `None`, the view will be searched for in any app. + /// + /// # Errors + /// + /// This method returns an error if the view name is not found. + /// + /// This method returns an error if the URL cannot be generated because of + /// missing parameters. + pub fn reverse( + &self, + app_name: Option<&str>, + name: &str, + params: &ReverseParamMap, + ) -> Result { + Ok(self + .reverse_option(app_name, name, params)? + .ok_or_else(|| NoViewToReverse { + app_name: app_name.map(ToOwned::to_owned), + view_name: name.to_owned(), + })?) + } + + /// Get a URL for a view by name. + /// + /// `app_name` is the name of the app that the view should be found in. If + /// `app_name` is `None`, the view will be searched for in any app. + /// + /// Returns `None` if the view name is not found. + /// + /// # Errors + /// + /// This method returns an error if the URL cannot be generated because of + /// missing parameters. + pub fn reverse_option( + &self, + app_name: Option<&str>, + name: &str, + params: &ReverseParamMap, + ) -> Result> { + if app_name.is_none() + || self.app_name.is_none() + || app_name == self.app_name.as_ref().map(|name| name.0.as_str()) + { + self.reverse_option_impl(app_name, name, params) + } else { + Ok(None) + } + } + + fn reverse_option_impl( + &self, + app_name: Option<&str>, + name: &str, + params: &ReverseParamMap, + ) -> Result> { + let url = self + .names + .get(&RouteName(String::from(name))) + .map(|matcher| matcher.reverse(params)); + if let Some(url) = url { + return Ok(Some(url?)); + } + + for route in &self.urls { + if let RouteInner::Router(router) = &route.view { + if let Some(url) = router.reverse_option(app_name, name, params)? { + return Ok(Some(route.url.reverse(params)? + &url)); + } + } + } + Ok(None) + } + + /// Get the routes in this router. + /// + /// # Examples + /// + /// ``` + /// use cot_core::request::Request; + /// use cot_core::response::Response; + /// use cot_core::router::{Route, Router}; + /// + /// async fn home(request: Request) -> cot::Result { + /// unimplemented!() + /// } + /// + /// let router = Router::with_urls([Route::with_handler_and_name("/", home, "home")]); + /// assert_eq!(router.routes().len(), 1); + /// ``` + #[must_use] + pub fn routes(&self) -> &[Route] { + &self.urls + } + + /// Check if this router is empty. + /// + /// # Examples + /// + /// ``` + /// use cot_core::request::Request; + /// use cot_core::response::Response; + /// use cot_core::router::{Route, Router}; + /// + /// async fn home(request: Request) -> cot::Result { + /// unimplemented!() + /// } + /// + /// let router = Router::empty(); + /// assert!(router.is_empty()); + /// + /// let router = Router::with_urls([Route::with_handler_and_name("/", home, "home")]); + /// assert!(!router.is_empty()); + /// ``` + #[must_use] + pub fn is_empty(&self) -> bool { + self.urls.is_empty() + } + + /// Returns the OpenAPI paths for the router. + /// + /// This might be useful if you want to manually serve the generated OpenAPI + /// specs. + /// + /// # Panics + /// + /// Panics if invalid schemas are generated. This should not happen in + /// normal operation, but if it does, it indicates a bug in the + /// [`schemars`](https://docs.rs/schemars/latest/schemars/) library + /// or in the way the OpenAPI specs are generated. + #[cfg(feature = "openapi")] + #[must_use] + pub fn as_api(&self) -> aide::openapi::OpenApi { + let mut paths = aide::openapi::Paths::default(); + let mut schema_generator = + schemars::SchemaGenerator::new(schemars::generate::SchemaSettings::openapi3()); + + self.as_openapi_impl("", &[], &mut paths, &mut schema_generator); + + let component_schemas = schema_generator + .take_definitions(true) + .into_iter() + .map(|(name, json_schema)| { + ( + name, + aide::openapi::SchemaObject { + json_schema: schemars::Schema::try_from(json_schema).expect( + "SchemaGenerator::take_definitions should return valid schemas", + ), + example: None, + external_docs: None, + }, + ) + }) + .collect(); + aide::openapi::OpenApi { + paths: Some(paths), + components: Some(aide::openapi::Components { + schemas: component_schemas, + ..Default::default() + }), + ..Default::default() + } + } + + #[cfg(feature = "openapi")] + fn as_openapi_impl( + &self, + url: &str, + param_names: &[&str], + paths: &mut aide::openapi::Paths, + schema_generator: &mut schemars::SchemaGenerator, + ) { + for route in &self.urls { + Self::route_as_openapi(route, param_names, paths, schema_generator, url); + } + } + + #[cfg(feature = "openapi")] + fn route_as_openapi( + route: &Route, + param_names: &[&str], + paths: &mut aide::openapi::Paths, + schema_generator: &mut schemars::SchemaGenerator, + url: &str, + ) { + match &route.view { + RouteInner::Router(router) => { + let mut params = Vec::from(param_names); + params.extend(route.url.param_names()); + + let url = format!("{url}{}", route.url); + + router.as_openapi_impl(&url, ¶ms, paths, schema_generator); + } + RouteInner::ApiHandler(handler) => { + let mut params = Vec::from(param_names); + params.extend(route.url.param_names()); + + let url = format!("{url}{}", route.url); + + let mut route_context = crate::openapi::RouteContext::new(); + route_context.param_names = ¶ms; + + paths.paths.insert( + url, + aide::openapi::ReferenceOr::Item( + handler.as_api_route(&route_context, schema_generator), + ), + ); + } + RouteInner::Handler(_) => {} + } + } +} + +impl Default for Router { + fn default() -> Self { + Self::empty() + } +} + +#[derive(Debug, thiserror::Error)] +#[error("failed to reverse route `{view_name}` due to view not existing")] +struct NoViewToReverse { + app_name: Option, + view_name: String, +} +impl_into_cot_error!(NoViewToReverse); + +#[derive(Debug)] +struct HandlerFound<'a> { + #[debug("handler(...)")] + handler: &'a (dyn BoxRequestHandler + Send + Sync), + app_name: Option, + name: Option, + params: Vec<(String, String)>, +} + +/// A service that routes requests to their respective views. +/// +/// This is mostly an internal service used by the [`CotApp`](cot::project::App) +/// to route requests to their respective views with an interface that is +/// compatible with the [`tower::Service`] trait. +#[derive(Debug, Clone)] +pub struct RouterService { + router: Arc, +} + +impl RouterService { + /// Create a new router service. + /// + /// # Examples + /// + /// ``` + /// use std::sync::Arc; + /// + /// use cot_core::request::Request; + /// use cot_core::response::Response; + /// use cot_core::router::{Route, Router, RouterService}; + /// + /// async fn home(request: Request) -> cot::Result { + /// unimplemented!() + /// } + /// + /// let router = Router::with_urls([Route::with_handler_and_name("/", home, "home")]); + /// let service = RouterService::new(Arc::new(router)); + /// ``` + #[must_use] + pub fn new(router: Arc) -> Self { + Self { router } + } +} + +impl tower::Service for RouterService { + type Error = Error; + type Future = Pin> + Send>>; + type Response = Response; + + fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, req: Request) -> Self::Future { + let router = self.router.clone(); + Box::pin(async move { router.handle(req).await }) + } +} + +/// A route that can be used to route requests to their respective views. +/// +/// # Examples +/// +/// ``` +/// use cot_core::request::Request; +/// use cot_core::response::Response; +/// use cot_core::router::{Route, Router}; +/// +/// async fn home(request: Request) -> cot::Result { +/// unimplemented!() +/// } +/// +/// let router = Router::with_urls([Route::with_handler_and_name("/", home, "home")]); +/// ``` +#[derive(Debug, Clone)] +pub struct Route { + url: Arc, + view: RouteInner, + name: Option, +} + +impl Route { + /// Create a new route with the given handler. + /// + /// # Examples + /// + /// ``` + /// use cot_core::request::Request; + /// use cot_core::response::Response; + /// use cot_core::router::{Route, Router}; + /// + /// async fn home(request: Request) -> cot::Result { + /// // ... + /// # unimplemented!() + /// } + /// + /// let route = Route::with_handler("/", home); + /// ``` + #[must_use] + pub fn with_handler(url: &str, handler: H) -> Self + where + HandlerParams: 'static, + H: RequestHandler + Send + Sync + 'static, + { + Self { + url: Arc::new(PathMatcher::new(url)), + view: RouteInner::Handler(Arc::new(into_box_request_handler(handler))), + name: None, + } + } + + /// Create a new route with the given handler for inclusion in the OpenAPI + /// specs. + /// + /// See [`cot::openapi`] module documentation for more details on how to + /// generate OpenAPI specs automatically. + /// + /// # Examples + /// + /// ``` + /// use cot_core::request::Request; + /// use cot_core::response::Response; + /// use cot_core::router::method::openapi::api_get; + /// use cot_core::router::{Route, Router}; + /// + /// async fn home(request: Request) -> cot::Result { + /// // ... + /// # unimplemented!() + /// } + /// + /// let route = Route::with_api_handler("/", api_get(home)); + /// ``` + #[must_use] + #[cfg(feature = "openapi")] + pub fn with_api_handler(url: &str, handler: H) -> Self + where + HandlerParams: 'static, + H: RequestHandler + crate::openapi::AsApiRoute + Send + Sync + 'static, + { + Self { + url: Arc::new(PathMatcher::new(url)), + view: RouteInner::ApiHandler(Arc::new( + crate::openapi::into_box_api_endpoint_request_handler(handler), + )), + name: None, + } + } + + /// Create a new route with the given handler and name. + /// + /// # Examples + /// + /// ``` + /// use cot_core::request::Request; + /// use cot_core::response::Response; + /// use cot_core::router::method::method::api_get; + /// use cot_core::router::{Route, Router}; + /// + /// async fn home(request: Request) -> cot::Result { + /// // ... + /// # unimplemented!() + /// } + /// + /// let route = Route::with_handler_and_name("/", api_get(home), "home"); + /// ``` + #[must_use] + pub fn with_handler_and_name(url: &str, handler: H, name: N) -> Self + where + N: Into, + HandlerParams: 'static, + H: RequestHandler + Send + Sync + 'static, + { + Self { + url: Arc::new(PathMatcher::new(url)), + view: RouteInner::Handler(Arc::new(into_box_request_handler(handler))), + name: Some(RouteName(name.into())), + } + } + + /// Create a new route with the given handler and name for inclusion in the + /// OpenAPI specs. + /// + /// See [`cot::openapi`] module documentation for more details on how to + /// generate OpenAPI specs automatically. + /// + /// # Examples + /// + /// ``` + /// use cot_core::request::Request; + /// use cot_core::response::Response; + /// use cot_core::router::method::openapi::api_post; + /// use cot_core::router::{Route, Router}; + /// + /// async fn home(request: Request) -> cot::Result { + /// // ... + /// # unimplemented!() + /// } + /// + /// let route = Route::with_api_handler_and_name("/", api_post(home), "home"); + /// ``` + #[must_use] + #[cfg(feature = "openapi")] + pub fn with_api_handler_and_name(url: &str, handler: H, name: N) -> Self + where + N: Into, + HandlerParams: 'static, + H: RequestHandler + crate::openapi::AsApiRoute + Send + Sync + 'static, + { + Self { + url: Arc::new(PathMatcher::new(url)), + view: RouteInner::ApiHandler(Arc::new( + crate::openapi::into_box_api_endpoint_request_handler(handler), + )), + name: Some(RouteName(name.into())), + } + } + + /// Create a new route with the given router. + /// + /// # Examples + /// + /// ``` + /// use cot_core::request::Request; + /// use cot_core::response::Response; + /// use cot_core::router::{Route, Router}; + /// + /// async fn home(request: Request) -> cot::Result { + /// unimplemented!() + /// } + /// + /// let router = Router::with_urls([Route::with_handler_and_name("/", home, "home")]); + /// let route = Route::with_router("/", router); + /// ``` + #[must_use] + pub fn with_router(url: &str, router: Router) -> Self { + Self { + url: Arc::new(PathMatcher::new(url)), + view: RouteInner::Router(router), + name: None, + } + } + + /// Get the URL for this route. + /// + /// # Examples + /// + /// ``` + /// use cot_core::request::Request; + /// use cot_core::response::Response; + /// use cot_core::router::{Route, Router}; + /// + /// async fn home(request: Request) -> cot::Result { + /// unimplemented!() + /// } + /// + /// let route = Route::with_handler("/test", home); + /// assert_eq!(route.url(), "/test"); + /// ``` + #[must_use] + pub fn url(&self) -> String { + self.url.to_string() + } + + /// Get the name of this route, if it was created with the + /// [`Self::with_handler_and_name`] function. + /// + /// # Examples + /// + /// ``` + /// use cot_core::request::Request; + /// use cot_core::response::Response; + /// use cot_core::router::{Route, Router}; + /// + /// async fn home(request: Request) -> cot::Result { + /// unimplemented!() + /// } + /// + /// let route = Route::with_handler_and_name("/", home, "home"); + /// assert_eq!(route.name(), Some("home")); + /// ``` + #[must_use] + pub fn name(&self) -> Option<&str> { + self.name.as_ref().map(|name| name.0.as_str()) + } + + #[must_use] + pub fn kind(&self) -> RouteKind { + match &self.view { + RouteInner::Handler(_) => RouteKind::Handler, + RouteInner::Router(_) => RouteKind::Router, + #[cfg(feature = "openapi")] + RouteInner::ApiHandler(_) => RouteKind::Handler, + } + } + + #[must_use] + pub fn router(&self) -> Option<&Router> { + match &self.view { + RouteInner::Router(router) => Some(router), + RouteInner::Handler(_) => None, + #[cfg(feature = "openapi")] + RouteInner::ApiHandler(_) => None, + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum RouteKind { + Handler, + Router, +} + +#[derive(Clone)] +enum RouteInner { + Handler(Arc), + Router(Router), + #[cfg(feature = "openapi")] + ApiHandler(Arc), +} + +impl Debug for RouteInner { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match &self { + RouteInner::Handler(_) => f.debug_tuple("Handler").field(&"handler(...)").finish(), + RouteInner::Router(router) => f.debug_tuple("Router").field(router).finish(), + #[cfg(feature = "openapi")] + RouteInner::ApiHandler(_) => { + f.debug_tuple("ApiHandler").field(&"handler(...)").finish() + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::StatusCode; + use crate::request::Request; + use crate::response::{IntoResponse, Response}; + + struct MockHandler; + + impl RequestHandler for MockHandler { + async fn handle(&self, _request: Request) -> Result { + Html::new("OK").into_response() + } + } + + #[cfg(feature = "openapi")] + impl cot::openapi::AsApiRoute for MockHandler { + fn as_api_route( + &self, + _route_context: &cot::openapi::RouteContext<'_>, + _schema_generator: &mut schemars::SchemaGenerator, + ) -> aide::openapi::PathItem { + aide::openapi::PathItem::default() + } + } + + #[test] + #[cfg(feature = "openapi")] + fn route_inner_debug() { + let route = Route::with_handler("/test", MockHandler); + assert!(format!("{route:?}").contains("Handler(\"handler(...)\")")); + + let route = Route::with_router("/test", Router::empty()); + assert!(format!("{route:?}").contains("Router(Router {")); + + let route = Route::with_api_handler("/test", MockHandler); + assert!(format!("{route:?}").contains("ApiHandler(\"handler(...)\")")); + } + + #[test] + #[cfg(feature = "openapi")] + fn route_kind() { + let handler_route = Route::with_handler("/test", MockHandler); + assert_eq!(handler_route.kind(), RouteKind::Handler); + + let router_route = Route::with_router("/test", Router::empty()); + assert_eq!(router_route.kind(), RouteKind::Router); + + let api_route = Route::with_api_handler("/test", MockHandler); + assert_eq!(api_route.kind(), RouteKind::Handler); + } + + #[test] + #[cfg(feature = "openapi")] + fn route_router() { + let router = Router::empty(); + let route = Route::with_router("/test", router.clone()); + assert!(route.router().is_some()); + + let route = Route::with_handler("/test", MockHandler); + assert!(route.router().is_none()); + + let route = Route::with_api_handler("/test", MockHandler); + assert!(route.router().is_none()); + } + + #[test] + fn router_with_urls() { + let route = Route::with_handler("/test", MockHandler); + let router = Router::with_urls(vec![route.clone()]); + assert_eq!(router.routes().len(), 1); + } + + #[cot_macros::test] + async fn router_route() { + let route = Route::with_handler("/test", MockHandler); + let router = Router::with_urls(vec![route.clone()]); + let response = router.route(test_request(), "/test").await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + } + + #[cot_macros::test] + async fn router_handle() { + let route = Route::with_handler("/test", MockHandler); + let router = Router::with_urls(vec![route.clone()]); + let response = router.handle(test_request()).await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + } + + #[cot_macros::test] + async fn sub_router_handle() { + let route_1 = Route::with_handler("/test", MockHandler); + let sub_router_1 = Router::with_urls(vec![route_1.clone()]); + let route_2 = Route::with_handler("/test", MockHandler); + let sub_router_2 = Router::with_urls(vec![route_2.clone()]); + + let router = Router::with_urls(vec![ + Route::with_router("/", sub_router_1), + Route::with_router("/sub", sub_router_2), + ]); + let response = router + .handle(TestRequestBuilder::get("/sub/test").build()) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + } + + #[test] + fn router_reverse() { + let route = Route::with_handler_and_name("/test", MockHandler, "test"); + let router = Router::with_urls(vec![route.clone()]); + let params = ReverseParamMap::new(); + let url = router.reverse(None, "test", ¶ms).unwrap(); + assert_eq!(url, "/test"); + } + + #[test] + fn router_reverse_with_param() { + let route = Route::with_handler_and_name("/test/{id}", MockHandler, "test"); + let router = Router::with_urls(vec![route.clone()]); + let mut params = ReverseParamMap::new(); + params.insert("id", "123"); + let url = router.reverse(None, "test", ¶ms).unwrap(); + assert_eq!(url, "/test/123"); + } + + #[test] + fn router_reverse_app_name() { + let route = Route::with_handler_and_name("/test", MockHandler, "test"); + let mut router_1 = Router::with_urls(vec![route.clone()]); + router_1.set_app_name(AppName("app_1".to_string())); + let mut router_2 = Router::with_urls(vec![route.clone()]); + router_2.set_app_name(AppName("app_2".to_string())); + let root_router = Router::with_urls(vec![ + Route::with_router("/", router_1), + Route::with_router("/sub", router_2), + ]); + + let params = ReverseParamMap::new(); + let url = root_router.reverse(Some("app_2"), "test", ¶ms).unwrap(); + + assert_eq!(url, "/sub/test"); + } + + #[test] + fn router_reverse_app_name_nested() { + let route = Route::with_handler_and_name("/test", MockHandler, "test"); + let router = Router::with_urls(vec![route.clone()]); + let sub_router = Router::with_urls(vec![Route::with_router("/sub", router)]); + let mut root_router = Router::with_urls(vec![Route::with_router("/subsub", sub_router)]); + root_router.set_app_name(AppName("app_root".to_string())); + + let params = ReverseParamMap::new(); + let url = root_router + .reverse(Some("app_root"), "test", ¶ms) + .unwrap(); + + assert_eq!(url, "/subsub/sub/test"); + } + + #[test] + fn router_reverse_option() { + let route = Route::with_handler_and_name("/test", MockHandler, "test"); + let router = Router::with_urls(vec![route.clone()]); + let params = ReverseParamMap::new(); + let url = router + .reverse_option(None, "test", ¶ms) + .unwrap() + .unwrap(); + assert_eq!(url, "/test"); + } + + #[test] + fn router_routes() { + let route = Route::with_handler("/test", MockHandler); + let router = Router::with_urls(vec![route.clone()]); + assert_eq!(router.routes().len(), 1); + } + + #[test] + fn router_is_empty() { + let router = Router::with_urls(vec![]); + assert!(router.is_empty()); + } + + #[test] + fn route_with_handler() { + let route = Route::with_handler("/test", MockHandler); + assert_eq!(route.url.to_string(), "/test"); + } + + #[test] + fn route_with_handler_and_params() { + let route = Route::with_handler("/test/{id}", MockHandler); + assert_eq!(route.url.to_string(), "/test/{id}"); + } + + #[test] + fn route_with_handler_and_name() { + let route = Route::with_handler_and_name("/test", MockHandler, "test"); + assert_eq!(route.url.to_string(), "/test"); + assert_eq!(route.name, Some(RouteName("test".to_string()))); + } + + #[test] + fn route_with_router() { + let sub_route = Route::with_handler("/sub", MockHandler); + let sub_router = Router::with_urls(vec![sub_route]); + let route = Route::with_router("/test", sub_router); + assert_eq!(route.url.to_string(), "/test"); + } + + #[test] + fn test_reverse_macro() { + let route = Route::with_handler_and_name("/test/{id}", MockHandler, "test"); + let router = Router::with_urls(vec![route]); + + let request = TestRequestBuilder::get("/").router(router).build(); + let url = reverse!(request, "test", id = 123).unwrap(); + + assert_eq!(url, "/test/123"); + } + + #[test] + fn test_reverse_redirect_macro() { + let route = Route::with_handler_and_name("/test/{id}", MockHandler, "test"); + let router = Router::with_urls(vec![route]); + + let request = TestRequestBuilder::get("/").router(router).build(); + let response = cot::reverse_redirect!(request, "test", id = 123).unwrap(); + + assert_eq!(response.status(), StatusCode::SEE_OTHER); + assert_eq!(response.headers().get("location").unwrap(), "/test/123"); + } + + fn test_request() -> Request { + TestRequestBuilder::get("/test").build() + } +} diff --git a/cot/src/router/method.rs b/cot-core/src/router/method.rs similarity index 91% rename from cot/src/router/method.rs rename to cot-core/src/router/method.rs index c2c55ac9..6c249226 100644 --- a/cot/src/router/method.rs +++ b/cot-core/src/router/method.rs @@ -1,15 +1,12 @@ //! Route to handlers based on HTTP methods. -#[cfg(feature = "openapi")] -pub mod openapi; - use std::fmt::{Debug, Formatter}; +use crate::Method; use crate::error::MethodNotAllowed; -use crate::handler::{BoxRequestHandler, into_box_request_handler}; +use crate::handler::{BoxRequestHandler, RequestHandler, into_box_request_handler}; use crate::request::Request; use crate::response::Response; -use crate::{Method, RequestHandler}; /// A router that routes requests based on the HTTP method. /// @@ -30,9 +27,9 @@ use crate::{Method, RequestHandler}; /// /// ``` /// use cot::html::Html; -/// use cot::router::method::{MethodRouter, get}; -/// use cot::router::{Route, Router}; /// use cot::test::TestRequestBuilder; +/// use cot_core::router::method::{MethodRouter, get}; +/// use cot_core::router::{Route, Router}; /// /// async fn get_handler() -> Html { /// Html::new("GET response") @@ -90,7 +87,7 @@ macro_rules! define_method { /// /// ``` /// use cot::html::Html; - /// use cot::router::method::MethodRouter; + /// use cot_core::router::method::MethodRouter; /// /// async fn test_handler() -> Html { /// Html::new("test") @@ -148,16 +145,17 @@ impl MethodRouter { /// Create a new [`MethodRouter`]. /// /// You might consider using [`get`], [`post`], or one of the other - /// functions defined in [`cot::router::method`] which serve as convenient - /// constructors for a [`MethodRouter`] with a specific handler. + /// functions defined in [`cot_core::router::method`] which serve as + /// convenient constructors for a [`MethodRouter`] with a specific + /// handler. /// /// # Examples /// /// ``` /// use cot::html::Html; - /// use cot::router::method::MethodRouter; - /// use cot::router::{Route, Router}; /// use cot::test::TestRequestBuilder; + /// use cot_core::router::method::MethodRouter; + /// use cot_core::router::{Route, Router}; /// /// async fn test_handler() -> Html { /// Html::new("GET response") @@ -205,10 +203,10 @@ impl MethodRouter { /// ``` /// use cot::StatusCode; /// use cot::html::Html; - /// use cot::response::IntoResponse; - /// use cot::router::method::MethodRouter; - /// use cot::router::{Route, Router}; /// use cot::test::TestRequestBuilder; + /// use cot_core::response::IntoResponse; + /// use cot_core::router::method::MethodRouter; + /// use cot_core::router::{Route, Router}; /// /// async fn fallback_handler() -> impl IntoResponse { /// Html::new("Method Not Allowed").with_status(StatusCode::METHOD_NOT_ALLOWED) @@ -244,29 +242,29 @@ impl MethodRouter { } impl RequestHandler for MethodRouter { - fn handle(&self, request: Request) -> impl Future> + Send { + fn handle(&self, request: Request) -> impl Future> + Send { self.inner.handle(request) } } #[derive(Debug)] #[must_use] -struct InnerMethodRouter { - pub(self) get: Option, - pub(self) head: Option, - pub(self) delete: Option, - pub(self) options: Option, - pub(self) patch: Option, - pub(self) post: Option, - pub(self) put: Option, - pub(self) trace: Option, +pub(crate) struct InnerMethodRouter { + pub get: Option, + pub head: Option, + pub delete: Option, + pub options: Option, + pub patch: Option, + pub post: Option, + pub put: Option, + pub trace: Option, // CONNECT can't be used in OpenAPI, so it's always a base handler - pub(self) connect: Option, - pub(self) fallback: InnerHandler, + pub connect: Option, + pub fallback: InnerHandler, } impl InnerMethodRouter { - pub(crate) fn new() -> Self { + pub fn new() -> Self { Self { get: None, head: None, @@ -283,7 +281,7 @@ impl InnerMethodRouter { } impl RequestHandler for InnerMethodRouter { - async fn handle(&self, request: Request) -> cot::Result { + async fn handle(&self, request: Request) -> crate::Result { macro_rules! handle_method { ($name:ident => $method:ident) => { if request.method() == Method::$method { @@ -317,10 +315,10 @@ impl RequestHandler for InnerMethodRouter { } } -struct InnerHandler(Box); +pub(crate) struct InnerHandler(Box); impl InnerHandler { - fn new(handler: H) -> Self + pub(crate) fn new(handler: H) -> Self where HandlerParams: 'static, H: RequestHandler + Send + Sync + 'static, @@ -336,7 +334,7 @@ impl Debug for InnerHandler { } impl RequestHandler for InnerHandler { - fn handle(&self, request: Request) -> impl Future> + Send { + fn handle(&self, request: Request) -> impl Future> + Send { self.0.handle(request) } } @@ -419,10 +417,11 @@ async fn default_fallback(method: Method) -> crate::Error { #[cfg(test)] mod tests { + use cot::test::TestRequestBuilder; + use super::*; use crate::StatusCode; use crate::html::Html; - use crate::test::TestRequestBuilder; async fn test_handler(method: Method) -> Html { Html::new(method.as_str()) @@ -437,7 +436,7 @@ mod tests { assert_eq!(debug_str, "InnerHandler(..)"); } - #[cot::test] + #[cot_macros::test] async fn method_router_fallback() { let router = MethodRouter::new(); @@ -449,7 +448,7 @@ mod tests { assert!(inner.is::()); } - #[cot::test] + #[cot_macros::test] async fn method_router_default_fallback() { let router = MethodRouter::default(); @@ -461,7 +460,7 @@ mod tests { assert!(inner.is::()); } - #[cot::test] + #[cot_macros::test] async fn method_router_custom_fallback() { let router = MethodRouter::new().fallback(test_handler); @@ -472,7 +471,7 @@ mod tests { assert_eq!(response.into_body().into_bytes().await.unwrap(), "GET"); } - #[cot::test] + #[cot_macros::test] async fn method_router_get() { let router = get(test_handler); @@ -524,7 +523,7 @@ mod tests { test_method_router!(method_router_trace, trace, TRACE); test_method_router!(method_router_connect, connect, CONNECT); - #[cot::test] + #[cot_macros::test] async fn method_router_default_head() { // verify that the default method router doesn't handle HEAD let router = MethodRouter::new(); @@ -545,7 +544,7 @@ mod tests { assert_eq!(response.status(), StatusCode::OK); } - #[cot::test] + #[cot_macros::test] async fn method_router_multiple() { let router = MethodRouter::new() .get(test_handler) diff --git a/cot/src/router/path.rs b/cot-core/src/router/path.rs similarity index 94% rename from cot/src/router/path.rs rename to cot-core/src/router/path.rs index 3b2e35d1..af027b66 100644 --- a/cot/src/router/path.rs +++ b/cot-core/src/router/path.rs @@ -7,19 +7,18 @@ use std::collections::HashMap; use std::fmt::Display; +use crate::impl_into_cot_error; use thiserror::Error; use tracing::debug; -use crate::error::error_impl::impl_into_cot_error; - #[derive(Debug, Clone)] -pub(super) struct PathMatcher { +pub struct PathMatcher { parts: Vec, } impl PathMatcher { #[must_use] - pub(crate) fn new>(path_pattern: T) -> Self { + pub fn new>(path_pattern: T) -> Self { #[derive(Debug, Copy, Clone)] enum State { Literal { start: usize }, @@ -116,7 +115,7 @@ impl PathMatcher { } #[must_use] - pub(crate) fn capture<'matcher, 'path>( + pub fn capture<'matcher, 'path>( &'matcher self, path: &'path str, ) -> Option> { @@ -151,7 +150,7 @@ impl PathMatcher { Some(CaptureResult::new(params, current_path)) } - pub(crate) fn reverse(&self, params: &ReverseParamMap) -> Result { + pub fn reverse(&self, params: &ReverseParamMap) -> Result { let mut result = String::new(); for part in &self.parts { @@ -174,7 +173,7 @@ impl PathMatcher { self.param_names().count() } - pub(super) fn param_names(&self) -> impl Iterator { + pub fn param_names(&self) -> impl Iterator { self.parts.iter().filter_map(|part| match part { PathPart::Literal(..) => None, PathPart::Param { name } => Some(name.as_str()), @@ -198,7 +197,7 @@ impl Display for PathMatcher { /// # Examples /// /// ``` -/// use cot::router::path::ReverseParamMap; +/// use cot_core::router::path::ReverseParamMap; /// /// let mut map = ReverseParamMap::new(); /// map.insert("id", "123"); @@ -221,7 +220,7 @@ impl ReverseParamMap { /// # Examples /// /// ``` - /// use cot::router::path::ReverseParamMap; + /// use cot_core::router::path::ReverseParamMap; /// /// let mut map = ReverseParamMap::new(); /// ``` @@ -238,7 +237,7 @@ impl ReverseParamMap { /// # Examples /// /// ``` - /// use cot::router::path::ReverseParamMap; + /// use cot_core::router::path::ReverseParamMap; /// /// let mut map = ReverseParamMap::new(); /// map.insert("id", "123"); @@ -281,9 +280,9 @@ pub enum ReverseError { impl_into_cot_error!(ReverseError); #[derive(Debug, PartialEq, Eq)] -pub(super) struct CaptureResult<'matcher, 'path> { - pub(super) params: Vec>, - pub(super) remaining_path: &'path str, +pub struct CaptureResult<'matcher, 'path> { + pub params: Vec>, + pub remaining_path: &'path str, } impl<'matcher, 'path> CaptureResult<'matcher, 'path> { @@ -296,7 +295,7 @@ impl<'matcher, 'path> CaptureResult<'matcher, 'path> { } #[must_use] - pub(crate) fn matches_fully(&self) -> bool { + pub fn matches_fully(&self) -> bool { self.remaining_path.is_empty() } } @@ -320,14 +319,14 @@ impl Display for PathPart { } #[derive(Debug, Clone, PartialEq, Eq)] -pub(super) struct PathParam<'a> { - pub(super) name: &'a str, - pub(super) value: String, +pub struct PathParam<'a> { + pub name: &'a str, + pub value: String, } impl<'a> PathParam<'a> { #[must_use] - pub(crate) fn new(name: &'a str, value: &str) -> Self { + pub fn new(name: &'a str, value: &str) -> Self { Self { name, value: value.to_string(), diff --git a/cot/Cargo.toml b/cot/Cargo.toml index 2fc057c6..5620bd73 100644 --- a/cot/Cargo.toml +++ b/cot/Cargo.toml @@ -25,6 +25,7 @@ bytes.workspace = true chrono = { workspace = true, features = ["alloc", "serde"] } chrono-tz.workspace = true clap.workspace = true +cot_core.workspace = true cot_macros.workspace = true deadpool-redis = { workspace = true, features = ["tokio-comp", "rt_tokio_1"], optional = true } derive_builder.workspace = true @@ -105,13 +106,13 @@ ignored = [ default = ["sqlite", "postgres", "mysql", "json"] full = ["default", "fake", "live-reload", "test", "cache", "redis"] fake = ["dep:fake"] -db = ["dep:sea-query", "dep:sea-query-binder", "dep:sqlx"] +db = ["dep:sea-query", "dep:sea-query-binder", "dep:sqlx", "cot_core/db"] sqlite = ["db", "sea-query/backend-sqlite", "sea-query-binder/sqlx-sqlite", "sqlx/sqlite"] postgres = ["db", "sea-query/backend-postgres", "sea-query-binder/sqlx-postgres", "sqlx/postgres"] mysql = ["db", "sea-query/backend-mysql", "sea-query-binder/sqlx-mysql", "sqlx/mysql"] redis = ["cache", "dep:deadpool-redis", "dep:redis", "json"] -json = ["dep:serde_json"] -openapi = ["json", "dep:aide", "dep:schemars"] +json = ["dep:serde_json", "cot_core/json"] +openapi = ["json", "dep:aide", "dep:schemars", "cot_core/openapi"] swagger-ui = ["openapi", "dep:swagger-ui-redist"] live-reload = ["dep:tower-livereload"] cache = [] diff --git a/cot/benches/bench_utils.rs b/cot/benches/bench_utils.rs index 54ab3d02..274f9c63 100644 --- a/cot/benches/bench_utils.rs +++ b/cot/benches/bench_utils.rs @@ -9,8 +9,8 @@ use cot::cli::CliMetadata; use cot::config::ProjectConfig; use cot::project::RegisterAppsContext; -use cot::router::Router; use cot::{App, AppBuilder, Project}; +use cot_core::router::Router; use criterion::{Criterion, Throughput}; use futures_util::future::join_all; use reqwest::{Client, Request}; diff --git a/cot/benches/router.rs b/cot/benches/router.rs index 6d645852..cd88db36 100644 --- a/cot/benches/router.rs +++ b/cot/benches/router.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; mod bench_utils; use bench_utils::bench; use cot::json::Json; -use cot::router::{Route, Router}; +use cot_core::router::{Route, Router}; async fn hello_world() -> &'static str { "Hello, World!" diff --git a/cot/src/admin.rs b/cot/src/admin.rs index 42f193a2..056e3976 100644 --- a/cot/src/admin.rs +++ b/cot/src/admin.rs @@ -6,6 +6,13 @@ use std::any::Any; use std::marker::PhantomData; +use crate::error::NotFound; +use crate::html::Html; +use crate::request::extractors::{FromRequestHead, Path, StaticFiles, UrlQuery}; +use crate::request::{Request, RequestExt, RequestHead}; +use crate::response::{IntoResponse, Response}; +use crate::reverse_redirect; +use crate::router::{Router, Urls}; use askama::Template; use async_trait::async_trait; use bytes::Bytes; @@ -22,17 +29,11 @@ use serde::Deserialize; use crate::auth::Auth; use crate::common_types::Password; -use crate::error::NotFound; use crate::form::{ Form, FormContext, FormErrorTarget, FormField, FormFieldValidationError, FormResult, }; -use crate::html::Html; -use crate::request::extractors::{FromRequestHead, Path, StaticFiles, UrlQuery}; -use crate::request::{Request, RequestExt, RequestHead}; -use crate::response::{IntoResponse, Response}; -use crate::router::{Router, Urls}; use crate::static_files::StaticFile; -use crate::{App, Error, Method, RequestHandler, reverse_redirect}; +use crate::{App, Error, Method, RequestHandler}; struct AdminAuthenticated(H, PhantomData T>); @@ -682,28 +683,28 @@ impl App for AdminApp { fn router(&self) -> Router { Router::with_urls([ - crate::router::Route::with_handler_and_name( + cot_core::router::Route::with_handler_and_name( "/", AdminAuthenticated::new(index), "index", ), - crate::router::Route::with_handler_and_name("/login/", login, "login"), - crate::router::Route::with_handler_and_name( + cot_core::router::Route::with_handler_and_name("/login/", login, "login"), + cot_core::router::Route::with_handler_and_name( "/{model_name}/", AdminAuthenticated::new(view_model), "view_model", ), - crate::router::Route::with_handler_and_name( + cot_core::router::Route::with_handler_and_name( "/{model_name}/create/", AdminAuthenticated::new(create_model_instance), "create_model_instance", ), - crate::router::Route::with_handler_and_name( + cot_core::router::Route::with_handler_and_name( "/{model_name}/{pk}/edit/", AdminAuthenticated::new(edit_model_instance), "edit_model_instance", ), - crate::router::Route::with_handler_and_name( + cot_core::router::Route::with_handler_and_name( "/{model_name}/{pk}/remove/", AdminAuthenticated::new(remove_model_instance), "remove_model_instance", diff --git a/cot/src/auth.rs b/cot/src/auth.rs index 8dd86daf..f3082597 100644 --- a/cot/src/auth.rs +++ b/cot/src/auth.rs @@ -13,6 +13,8 @@ use std::any::Any; use std::borrow::Cow; use std::sync::{Arc, Mutex, MutexGuard}; +use crate::error::error_impl::impl_into_cot_error; +use crate::request::{Request, RequestExt}; /// backwards compatible shim for form Password type. use async_trait::async_trait; use chrono::{DateTime, FixedOffset}; @@ -27,8 +29,6 @@ use thiserror::Error; use crate::config::SecretKey; #[cfg(feature = "db")] use crate::db::{ColumnType, DatabaseField, DbValue, FromDbValue, SqlxValueRef, ToDbValue}; -use crate::error::error_impl::impl_into_cot_error; -use crate::request::{Request, RequestExt}; use crate::session::Session; const ERROR_PREFIX: &str = "failed to authenticate user:"; @@ -982,7 +982,7 @@ pub trait AuthBackend: Send + Sync { /// /// ``` /// use cot::auth::UserId; - /// use cot::request::{Request, RequestExt}; + /// use cot_core::request::{Request, RequestExt}; /// /// async fn view_user_profile(request: &Request) { /// let user = request diff --git a/cot/src/auth/db.rs b/cot/src/auth/db.rs index d7a7310d..ed5690a8 100644 --- a/cot/src/auth/db.rs +++ b/cot/src/auth/db.rs @@ -78,8 +78,8 @@ impl DatabaseUser { /// ``` /// use cot::auth::db::DatabaseUser; /// use cot::common_types::Password; - /// use cot::html::Html; /// use cot::request::{Request, RequestExt}; + /// use cot_core::html::Html; /// /// async fn view(request: &Request) -> cot::Result { /// let user = DatabaseUser::create_user( @@ -140,8 +140,8 @@ impl DatabaseUser { /// use cot::auth::UserId; /// use cot::auth::db::DatabaseUser; /// use cot::common_types::Password; - /// use cot::html::Html; /// use cot::request::{Request, RequestExt}; + /// use cot_core::html::Html; /// /// async fn view(request: &Request) -> cot::Result { /// let user = DatabaseUser::create_user( @@ -192,8 +192,8 @@ impl DatabaseUser { /// use cot::auth::UserId; /// use cot::auth::db::DatabaseUser; /// use cot::common_types::Password; - /// use cot::html::Html; /// use cot::request::extractors::RequestDb; + /// use cot_core::html::Html; /// /// async fn view(RequestDb(db): RequestDb) -> cot::Result { /// let user = @@ -284,8 +284,8 @@ impl DatabaseUser { /// use cot::auth::UserId; /// use cot::auth::db::DatabaseUser; /// use cot::common_types::Password; - /// use cot::html::Html; /// use cot::request::extractors::RequestDb; + /// use cot_core::html::Html; /// /// async fn view(RequestDb(db): RequestDb) -> cot::Result { /// let user = @@ -327,8 +327,8 @@ impl DatabaseUser { /// use cot::auth::UserId; /// use cot::auth::db::DatabaseUser; /// use cot::common_types::Password; - /// use cot::html::Html; /// use cot::request::extractors::RequestDb; + /// use cot_core::html::Html; /// /// async fn view(RequestDb(db): RequestDb) -> cot::Result { /// let user = diff --git a/cot/src/config.rs b/cot/src/config.rs index a1f9c190..6f8747f9 100644 --- a/cot/src/config.rs +++ b/cot/src/config.rs @@ -19,13 +19,13 @@ use std::path::PathBuf; use std::time::Duration; use chrono::{DateTime, FixedOffset}; +use cot_core::error::error_impl::impl_into_cot_error; use derive_builder::Builder; use derive_more::with_trait::{Debug, From}; use serde::{Deserialize, Serialize}; use subtle::ConstantTimeEq; use thiserror::Error; -use crate::error::error_impl::impl_into_cot_error; use crate::utils::chrono::DateTimeWithOffsetAdapter; /// The configuration for a project. diff --git a/cot/src/db.rs b/cot/src/db.rs index 8222442e..9512b9d6 100644 --- a/cot/src/db.rs +++ b/cot/src/db.rs @@ -20,6 +20,7 @@ use std::hash::Hash; use std::str::FromStr; use async_trait::async_trait; +use cot_core::error::error_impl::impl_into_cot_error; pub use cot_macros::{model, query}; use derive_more::{Debug, Deref, Display}; #[cfg(test)] @@ -41,7 +42,6 @@ use crate::db::impl_postgres::{DatabasePostgres, PostgresRow, PostgresValueRef}; #[cfg(feature = "sqlite")] use crate::db::impl_sqlite::{DatabaseSqlite, SqliteRow, SqliteValueRef}; use crate::db::migrations::ColumnTypeMapper; -use crate::error::error_impl::impl_into_cot_error; const ERROR_PREFIX: &str = "database error:"; /// An error that can occur when interacting with the database. diff --git a/cot/src/error_page.rs b/cot/src/error_page.rs index 71629d2e..b0ed1cb6 100644 --- a/cot/src/error_page.rs +++ b/cot/src/error_page.rs @@ -3,13 +3,13 @@ use std::panic::PanicHookInfo; use std::sync::Arc; use askama::Template; +use cot_core::error::NotFound; +use cot_core::error::backtrace::{__cot_create_backtrace, Backtrace}; use tracing::{Level, error, warn}; use crate::config::ProjectConfig; -use crate::error::NotFound; -use crate::error::backtrace::{__cot_create_backtrace, Backtrace}; -use crate::router::Router; use crate::{Error, Result, StatusCode}; +use cot_core::router::Router; #[derive(Debug)] pub(super) struct Diagnostics { @@ -71,16 +71,17 @@ impl ErrorPageTemplateBuilder { let mut error_message = None; if let Some(not_found) = error.inner().downcast_ref::() { - use crate::error::NotFoundKind as Kind; + use cot_core::error::NotFoundKind as Kind; match ¬_found.kind { - Kind::FromRouter => {} - Kind::Custom => { + Kind::FromRouter { .. } => {} + Kind::Custom { .. } => { Self::build_error_data(&mut error_data, error); } - Kind::WithMessage(message) => { + Kind::WithMessage { 0: message, .. } => { Self::build_error_data(&mut error_data, error); error_message = Some(message.clone()); } + _ => {} } } @@ -142,13 +143,13 @@ impl ErrorPageTemplateBuilder { index: format!("{index_prefix}{index}"), path: format!("{url_prefix}{}", route.url()), kind: match route.kind() { - crate::router::RouteKind::Router => if route_data.is_empty() { + cot_core::router::RouteKind::Router => if route_data.is_empty() { "Root Router" } else { "Router" } .to_owned(), - crate::router::RouteKind::Handler => "View".to_owned(), + cot_core::router::RouteKind::Handler => "View".to_owned(), }, name: route.name().unwrap_or_default().to_owned(), }); @@ -327,7 +328,7 @@ fn build_response( .status(status_code) .header( http::header::CONTENT_TYPE, - crate::headers::HTML_CONTENT_TYPE, + cot_core::headers::HTML_CONTENT_TYPE, ) .body(axum::body::Body::new(error_str)) .unwrap_or_else(|_| build_cot_failure_page()), @@ -456,8 +457,8 @@ mod tests { use tracing_test::traced_test; use super::*; - use crate::router::{Route, Router}; use crate::test::TestRequestBuilder; + use cot_core::router::{Route, Router}; fn create_test_request_data() -> RequestData { RequestData { diff --git a/cot/src/form.rs b/cot/src/form.rs index 67d790f9..52d26599 100644 --- a/cot/src/form.rs +++ b/cot/src/form.rs @@ -27,6 +27,9 @@ pub mod fields; use std::borrow::Cow; use std::fmt::Display; +use crate::error::error_impl::impl_into_cot_error; +use crate::headers::{MULTIPART_FORM_CONTENT_TYPE, URLENCODED_FORM_CONTENT_TYPE}; +use crate::request::{Request, RequestExt}; use async_trait::async_trait; use bytes::Bytes; use chrono::NaiveDateTime; @@ -61,10 +64,6 @@ pub use field_value::{FormFieldValue, FormFieldValueError}; use http_body_util::BodyExt; use thiserror::Error; -use crate::error::error_impl::impl_into_cot_error; -use crate::headers::{MULTIPART_FORM_CONTENT_TYPE, URLENCODED_FORM_CONTENT_TYPE}; -use crate::request::{Request, RequestExt}; - const ERROR_PREFIX: &str = "failed to process a form:"; /// Error occurred while processing a form. #[derive(Debug, Error)] @@ -657,10 +656,10 @@ pub trait AsFormField { #[cfg(test)] mod tests { use bytes::Bytes; + use cot_core::headers::{MULTIPART_FORM_CONTENT_TYPE, URLENCODED_FORM_CONTENT_TYPE}; use super::*; use crate::Body; - use crate::headers::{MULTIPART_FORM_CONTENT_TYPE, URLENCODED_FORM_CONTENT_TYPE}; #[cot::test] async fn urlencoded_form_data_extract_get_empty() { diff --git a/cot/src/form/field_value.rs b/cot/src/form/field_value.rs index 04c31c1b..ea28c9cb 100644 --- a/cot/src/form/field_value.rs +++ b/cot/src/form/field_value.rs @@ -2,10 +2,9 @@ use std::error::Error as StdError; use std::fmt::Display; use bytes::Bytes; +use cot_core::error::error_impl::impl_into_cot_error; use thiserror::Error; -use crate::error::error_impl::impl_into_cot_error; - /// A value from a form field. /// /// This type represents a value from a form field, which can be either a text diff --git a/cot/src/form/fields.rs b/cot/src/form/fields.rs index 920e19b1..0d9b7169 100644 --- a/cot/src/form/fields.rs +++ b/cot/src/form/fields.rs @@ -15,6 +15,7 @@ pub use chrono::{ DateField, DateFieldOptions, DateTimeField, DateTimeFieldOptions, DateTimeWithTimezoneField, DateTimeWithTimezoneFieldOptions, TimeField, TimeFieldOptions, }; +use cot_core::html::HtmlTag; pub use files::{FileField, FileFieldOptions, InMemoryUploadedFile}; pub(crate) use select::check_required_multiple; pub use select::{ @@ -26,7 +27,6 @@ use crate::common_types::{Email, Password, Url}; #[cfg(feature = "db")] use crate::db::{Auto, ForeignKey, LimitedString, Model}; use crate::form::{AsFormField, FormField, FormFieldOptions, FormFieldValidationError}; -use crate::html::HtmlTag; macro_rules! impl_form_field { ($field_type_name:ident, $field_options_type_name:ident, $purpose:literal $(, $generic_param:ident $(: $generic_param_bound:ident $(+ $generic_param_bound_more:ident)*)?)?) => { diff --git a/cot/src/form/fields/chrono.rs b/cot/src/form/fields/chrono.rs index af02c144..0a3ff3ca 100644 --- a/cot/src/form/fields/chrono.rs +++ b/cot/src/form/fields/chrono.rs @@ -8,7 +8,7 @@ use chrono::{ use chrono_tz::Tz; use cot::form::FormField; use cot::form::fields::impl_form_field; -use cot::html::HtmlTag; +use cot_core::html::HtmlTag; use crate::form::fields::{ SelectChoice, SelectField, SelectMultipleField, Step, check_required, check_required_multiple, diff --git a/cot/src/form/fields/files.rs b/cot/src/form/fields/files.rs index fb107280..a3447118 100644 --- a/cot/src/form/fields/files.rs +++ b/cot/src/form/fields/files.rs @@ -3,7 +3,7 @@ use std::fmt::{Display, Formatter}; use askama::filters::HtmlSafe; use bytes::Bytes; use cot::form::{AsFormField, FormFieldValidationError}; -use cot::html::HtmlTag; +use cot_core::html::HtmlTag; use crate::form::{FormField, FormFieldOptions, FormFieldValue, FormFieldValueError}; diff --git a/cot/src/form/fields/select.rs b/cot/src/form/fields/select.rs index 25b5d693..a9b6995b 100644 --- a/cot/src/form/fields/select.rs +++ b/cot/src/form/fields/select.rs @@ -1,6 +1,7 @@ use std::fmt::{Debug, Display, Formatter}; use askama::filters::HtmlSafe; +use cot_core::html::HtmlTag; /// Derive the [`SelectChoice`] trait for an enum. /// /// This macro automatically implements the [`SelectChoice`] trait for enums, @@ -114,7 +115,6 @@ use crate::form::fields::impl_form_field; use crate::form::{ FormField, FormFieldOptions, FormFieldValidationError, FormFieldValue, FormFieldValueError, }; -use crate::html::HtmlTag; impl_form_field!(SelectField, SelectFieldOptions, "a dropdown list", T: SelectChoice + Send); diff --git a/cot/src/headers.rs b/cot/src/headers.rs deleted file mode 100644 index 35a1a847..00000000 --- a/cot/src/headers.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub(crate) const HTML_CONTENT_TYPE: &str = "text/html; charset=utf-8"; -pub(crate) const MULTIPART_FORM_CONTENT_TYPE: &str = "multipart/form-data"; -pub(crate) const URLENCODED_FORM_CONTENT_TYPE: &str = "application/x-www-form-urlencoded"; -#[cfg(feature = "json")] -pub(crate) const JSON_CONTENT_TYPE: &str = "application/json"; -pub(crate) const PLAIN_TEXT_CONTENT_TYPE: &str = "text/plain; charset=utf-8"; -pub(crate) const OCTET_STREAM_CONTENT_TYPE: &str = "application/octet-stream"; diff --git a/cot/src/lib.rs b/cot/src/lib.rs index 8e14b530..b4de9155 100644 --- a/cot/src/lib.rs +++ b/cot/src/lib.rs @@ -55,28 +55,17 @@ extern crate self as cot; #[cfg(feature = "db")] pub mod db; -/// Error handling types and utilities for Cot applications. -/// -/// This module provides error types, error handlers, and utilities for -/// handling various types of errors that can occur in Cot applications, -/// including 404 Not Found errors, uncaught panics, and custom error pages. -pub mod error; pub mod form; -mod headers; // Not public API. Referenced by macro-generated code. #[doc(hidden)] #[path = "private.rs"] pub mod __private; pub mod admin; pub mod auth; -mod body; pub mod cli; pub mod common_types; pub mod config; mod error_page; -#[macro_use] -pub(crate) mod handler; -pub mod html; #[cfg(feature = "json")] pub mod json; pub mod middleware; @@ -95,7 +84,14 @@ pub(crate) mod utils; #[cfg(feature = "openapi")] pub use aide; -pub use body::Body; +/// A type alias for a result that can return a [`cot::Error`]. +pub use cot_core::Result; +/// A type alias for an HTTP status code. +pub use cot_core::StatusCode; +pub use cot_core::error::error_impl::Error; +pub use cot_core::handler::{BoxedHandler, RequestHandler}; +#[doc(inline)] +pub use cot_core::{Body, Method, body, error, handler, headers, html}; /// An attribute macro that defines an end-to-end test function for a /// Cot-powered app. /// @@ -159,21 +155,10 @@ pub use cot_macros::e2e_test; /// ``` pub use cot_macros::main; pub use cot_macros::test; -pub use error::error_impl::Error; #[cfg(feature = "openapi")] pub use schemars; pub use {bytes, http}; -pub use crate::handler::{BoxedHandler, RequestHandler}; pub use crate::project::{ App, AppBuilder, Bootstrapper, Project, ProjectContext, run, run_at, run_cli, }; - -/// A type alias for a result that can return a [`cot::Error`]. -pub type Result = std::result::Result; - -/// A type alias for an HTTP status code. -pub type StatusCode = http::StatusCode; - -/// A type alias for an HTTP method. -pub type Method = http::Method; diff --git a/cot/src/middleware.rs b/cot/src/middleware.rs index 5ce0427f..c3435a68 100644 --- a/cot/src/middleware.rs +++ b/cot/src/middleware.rs @@ -9,11 +9,15 @@ use std::fmt::Debug; use std::sync::Arc; use std::task::{Context, Poll}; -use bytes::Bytes; +#[doc(inline)] +pub use cot_core::middleware::{ + IntoCotError, IntoCotErrorLayer, IntoCotResponse, IntoCotResponseLayer, +}; +use cot_core::request::Request; +use cot_core::response::Response; use futures_core::future::BoxFuture; use futures_util::TryFutureExt; use http_body_util::BodyExt; -use http_body_util::combinators::BoxBody; use tower::Service; use tower_sessions::service::PlaintextCookie; use tower_sessions::{SessionManagerLayer, SessionStore}; @@ -22,8 +26,6 @@ use tower_sessions::{SessionManagerLayer, SessionStore}; use crate::config::CacheType; use crate::config::{Expiry, SameSite, SessionStoreTypeConfig}; use crate::project::MiddlewareContext; -use crate::request::Request; -use crate::response::Response; use crate::session::store::SessionStoreWrapper; #[cfg(all(feature = "db", feature = "json"))] use crate::session::store::db::DbStore; @@ -40,232 +42,6 @@ mod live_reload; #[cfg(feature = "live-reload")] pub use live_reload::LiveReloadMiddleware; -/// Middleware that converts a any [`http::Response`] generic type to a -/// [`cot::response::Response`]. -/// -/// This is useful for converting a response from a middleware that is -/// compatible with the `tower` crate to a response that is compatible with -/// Cot. It's applied automatically by -/// [`RootHandlerBuilder::middleware()`](cot::project::RootHandlerBuilder::middleware()) -/// and is not needed to be added manually. -/// -/// # Examples -/// -/// ``` -/// use cot::Project; -/// use cot::middleware::LiveReloadMiddleware; -/// use cot::project::{MiddlewareContext, RootHandler, RootHandlerBuilder}; -/// -/// struct MyProject; -/// impl Project for MyProject { -/// fn middlewares( -/// &self, -/// handler: RootHandlerBuilder, -/// context: &MiddlewareContext, -/// ) -> RootHandler { -/// handler -/// // IntoCotResponseLayer used internally in middleware() -/// .middleware(LiveReloadMiddleware::from_context(context)) -/// .build() -/// } -/// } -/// ``` -#[derive(Debug, Copy, Clone)] -pub struct IntoCotResponseLayer; - -impl IntoCotResponseLayer { - /// Create a new [`IntoCotResponseLayer`]. - /// - /// # Examples - /// - /// ``` - /// use cot::middleware::IntoCotResponseLayer; - /// - /// let middleware = IntoCotResponseLayer::new(); - /// ``` - #[must_use] - pub fn new() -> Self { - Self {} - } -} - -impl Default for IntoCotResponseLayer { - fn default() -> Self { - Self::new() - } -} - -impl tower::Layer for IntoCotResponseLayer { - type Service = IntoCotResponse; - - fn layer(&self, inner: S) -> Self::Service { - IntoCotResponse { inner } - } -} - -/// Service struct that converts any [`http::Response`] generic type to -/// [`cot::response::Response`]. -/// -/// Used by [`IntoCotResponseLayer`]. -/// -/// # Examples -/// -/// ``` -/// use std::any::TypeId; -/// -/// use cot::middleware::{IntoCotResponse, IntoCotResponseLayer}; -/// -/// assert_eq!( -/// TypeId::of::<>::Service>(), -/// TypeId::of::>() -/// ); -/// ``` -#[derive(Debug, Clone)] -pub struct IntoCotResponse { - inner: S, -} - -impl Service for IntoCotResponse -where - S: Service>, - ResBody: http_body::Body + Send + Sync + 'static, - E: std::error::Error + Send + Sync + 'static, -{ - type Response = Response; - type Error = S::Error; - type Future = futures_util::future::MapOk) -> Response>; - - #[inline] - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - self.inner.poll_ready(cx) - } - - #[inline] - fn call(&mut self, request: Request) -> Self::Future { - self.inner.call(request).map_ok(map_response) - } -} - -fn map_response(response: http::response::Response) -> Response -where - ResBody: http_body::Body + Send + Sync + 'static, - E: std::error::Error + Send + Sync + 'static, -{ - response.map(|body| Body::wrapper(BoxBody::new(body.map_err(map_err)))) -} - -/// Middleware that converts any error type to [`cot::Error`]. -/// -/// This is useful for converting a response from a middleware that is -/// compatible with the `tower` crate to a response that is compatible with -/// Cot. It's applied automatically by -/// [`RootHandlerBuilder::middleware()`](cot::project::RootHandlerBuilder::middleware()) -/// and is not needed to be added manually. -/// -/// # Examples -/// -/// ``` -/// use cot::Project; -/// use cot::middleware::LiveReloadMiddleware; -/// use cot::project::{MiddlewareContext, RootHandler, RootHandlerBuilder}; -/// -/// struct MyProject; -/// impl Project for MyProject { -/// fn middlewares( -/// &self, -/// handler: RootHandlerBuilder, -/// context: &MiddlewareContext, -/// ) -> RootHandler { -/// handler -/// // IntoCotErrorLayer used internally in middleware() -/// .middleware(LiveReloadMiddleware::from_context(context)) -/// .build() -/// } -/// } -/// ``` -#[derive(Debug, Copy, Clone)] -pub struct IntoCotErrorLayer; - -impl IntoCotErrorLayer { - /// Create a new [`IntoCotErrorLayer`]. - /// - /// # Examples - /// - /// ``` - /// use cot::middleware::IntoCotErrorLayer; - /// - /// let middleware = IntoCotErrorLayer::new(); - /// ``` - #[must_use] - pub fn new() -> Self { - Self {} - } -} - -impl Default for IntoCotErrorLayer { - fn default() -> Self { - Self::new() - } -} - -impl tower::Layer for IntoCotErrorLayer { - type Service = IntoCotError; - - fn layer(&self, inner: S) -> Self::Service { - IntoCotError { inner } - } -} - -/// Service struct that converts a any error type to a [`cot::Error`]. -/// -/// Used by [`IntoCotErrorLayer`]. -/// -/// # Examples -/// -/// ``` -/// use std::any::TypeId; -/// -/// use cot::middleware::{IntoCotError, IntoCotErrorLayer}; -/// -/// assert_eq!( -/// TypeId::of::<>::Service>(), -/// TypeId::of::>() -/// ); -/// ``` -#[derive(Debug, Clone)] -pub struct IntoCotError { - inner: S, -} - -impl Service for IntoCotError -where - S: Service, - >::Error: std::error::Error + Send + Sync + 'static, -{ - type Response = S::Response; - type Error = Error; - type Future = futures_util::future::MapErr Error>; - - #[inline] - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - self.inner.poll_ready(cx).map_err(map_err) - } - - #[inline] - fn call(&mut self, request: Request) -> Self::Future { - self.inner.call(request).map_err(map_err) - } -} - -fn map_err(error: E) -> Error -where - E: std::error::Error + Send + Sync + 'static, -{ - #[expect(trivial_casts)] - let boxed = Box::new(error) as Box; - boxed.downcast::().map_or_else(Error::wrap, |e| *e) -} - type DynamicSessionStore = SessionManagerLayer; /// A middleware that provides session management. @@ -722,6 +498,7 @@ mod tests { use std::path::PathBuf; use std::sync::Arc; + use cot_core::response::Response; use http::Request; use tower::{Layer, Service, ServiceExt}; @@ -733,7 +510,6 @@ mod tests { }; use crate::middleware::SessionMiddleware; use crate::project::{RegisterAppsContext, WithDatabase}; - use crate::response::Response; use crate::session::Session; use crate::test::TestRequestBuilder; use crate::{AppBuilder, Body, Bootstrapper, Error, Project, ProjectContext}; diff --git a/cot/src/middleware/live_reload.rs b/cot/src/middleware/live_reload.rs index df87580f..5711897b 100644 --- a/cot/src/middleware/live_reload.rs +++ b/cot/src/middleware/live_reload.rs @@ -1,5 +1,5 @@ -use cot::middleware::{IntoCotErrorLayer, IntoCotResponseLayer}; use cot::project::MiddlewareContext; +use cot_core::middleware::{IntoCotErrorLayer, IntoCotResponseLayer}; #[cfg(feature = "live-reload")] type LiveReloadLayerType = tower::util::Either< diff --git a/cot/src/openapi.rs b/cot/src/openapi.rs index f6cde8c0..a1dc6100 100644 --- a/cot/src/openapi.rs +++ b/cot/src/openapi.rs @@ -9,16 +9,16 @@ //! //! 1. Add [`#[derive(schemars::JsonSchema)]`](schemars::JsonSchema) to the //! types used in the extractors and response types. -//! 2. Use [`ApiMethodRouter`](crate::router::method::openapi::ApiMethodRouter) +//! 2. Use [`ApiMethodRouter`](cot_core::router::method::method::ApiMethodRouter) //! to set up your API routes and register them with a router (possibly using //! convenience functions, such as -//! [`api_get`](crate::router::method::openapi::api_get) or -//! [`api_post`](crate::router::method::openapi::api_post)). +//! [`api_get`](cot_core::router::method::method::api_get) or +//! [`api_post`](cot_core::router::method::method::api_post)). //! 3. Register your -//! [`ApiMethodRouter`](crate::router::method::openapi::ApiMethodRouter)s -//! with a [`Router`](crate::router::Router) using -//! [`Route::with_api_handler`](crate::router::Route::with_api_handler) or -//! [`Route::with_api_handler_and_name`](crate::router::Route::with_api_handler_and_name). +//! [`ApiMethodRouter`](cot_core::router::method::method::ApiMethodRouter)s +//! with a [`Router`](cot_core::router::Router) using +//! [`Route::with_api_handler`](cot_core::router::Route::with_api_handler) or +//! [`Route::with_api_handler_and_name`](cot_core::router::Route::with_api_handler_and_name). //! 4. Register the [`SwaggerUi`](crate::openapi::swagger_ui::SwaggerUi) app //! inside [`Project::register_apps`](crate::project::Project::register_apps) //! using [`AppBuilder::register_with_views`](crate::project::AppBuilder::register_with_views). @@ -34,10 +34,10 @@ //! use cot::openapi::swagger_ui::SwaggerUi; //! use cot::project::{MiddlewareContext, RegisterAppsContext, RootHandler, RootHandlerBuilder}; //! use cot::response::{Response, ResponseExt}; -//! use cot::router::method::openapi::api_post; -//! use cot::router::{Route, Router}; //! use cot::static_files::StaticFilesMiddleware; //! use cot::{App, AppBuilder, Project, StatusCode}; +//! use cot_core::router::method::method::api_post; +//! use cot_core::router::{Route, Router}; //! use serde::{Deserialize, Serialize}; //! //! #[derive(Deserialize, schemars::JsonSchema)] @@ -106,13 +106,22 @@ #[cfg(feature = "swagger-ui")] pub mod swagger_ui; -use std::marker::PhantomData; -use std::pin::Pin; - use aide::openapi::{ - MediaType, Operation, Parameter, ParameterData, ParameterSchemaOrContent, PathItem, PathStyle, + MediaType, Operation, Parameter, ParameterData, ParameterSchemaOrContent, PathStyle, QueryStyle, ReferenceOr, RequestBody, StatusCode, }; +use cot::router::Urls; +use cot_core::handle_all_parameters; +use cot_core::handler::BoxRequestHandler; +use cot_core::impl_as_openapi_operation; +use cot_core::openapi::add_query_param; +#[doc(inline)] +pub use cot_core::openapi::{ + ApiOperationPart, ApiOperationResponse, AsApiOperation, AsApiRoute, + BoxApiEndpointRequestHandler, RouteContext, into_box_api_endpoint_request_handler, method, +}; +use cot_core::request::extractors::{Path, UrlQuery}; +use cot_core::response::{Response, WithExtension}; /// Derive macro for the [`ApiOperationResponse`] trait. /// /// This macro can be applied to enums to automatically implement the @@ -207,297 +216,12 @@ use serde_json::Value; use crate::auth::Auth; use crate::form::Form; -use crate::handler::BoxRequestHandler; use crate::json::Json; -use crate::request::extractors::{FromRequest, FromRequestHead, Path, RequestForm, UrlQuery}; +use crate::request::extractors::{FromRequest, FromRequestHead, RequestForm}; use crate::request::{Request, RequestHead}; -use crate::response::{Response, WithExtension}; -use crate::router::Urls; use crate::session::Session; use crate::{Body, Method, RequestHandler}; -/// Context for API route generation. -/// -/// `RouteContext` is used to generate OpenAPI paths from routes. It provides -/// information about the route, such as the HTTP method and route parameter -/// names. -#[non_exhaustive] -#[derive(Debug, Clone)] -pub struct RouteContext<'a> { - /// The HTTP method of the route. - pub method: Option, - /// The names of the route parameters. - pub param_names: &'a [&'a str], -} - -impl RouteContext<'_> { - /// Creates a new `RouteContext` with no information about the route. - /// - /// # Examples - /// - /// ``` - /// use cot::openapi::RouteContext; - /// - /// let context = RouteContext::new(); - /// ``` - #[must_use] - pub fn new() -> Self { - Self { - method: None, - param_names: &[], - } - } -} - -impl Default for RouteContext<'_> { - fn default() -> Self { - Self::new() - } -} - -/// Returns the OpenAPI path item for the route - a collection of different -/// HTTP operations (GET, POST, etc.) at a given URL. -/// -/// You usually shouldn't need to implement this directly. Instead, it's easiest -/// to use [`ApiMethodRouter`](crate::router::method::openapi::ApiMethodRouter). -/// You might want to implement this if you want to create a wrapper that -/// modifies the OpenAPI spec or want to create it manually. -/// -/// An object implementing [`AsApiRoute`] can be used passed to -/// [`Route::with_api_handler`](crate::router::Route::with_api_handler) to -/// generate the OpenAPI specs. -/// -/// # Examples -/// -/// ``` -/// use aide::openapi::PathItem; -/// use cot::aide::openapi::Operation; -/// use cot::openapi::{AsApiOperation, AsApiRoute, RouteContext}; -/// use schemars::SchemaGenerator; -/// -/// struct RouteWrapper(T); -/// -/// impl AsApiRoute for RouteWrapper { -/// fn as_api_route( -/// &self, -/// route_context: &RouteContext<'_>, -/// schema_generator: &mut SchemaGenerator, -/// ) -> PathItem { -/// let mut spec = self.0.as_api_route(route_context, schema_generator); -/// spec.summary = Some("This route was wrapped with RouteWrapper".to_owned()); -/// spec -/// } -/// } -/// -/// # assert_eq!( -/// # RouteWrapper(cot::router::method::openapi::ApiMethodRouter::new()) -/// # .as_api_route(&RouteContext::new(), &mut SchemaGenerator::default()) -/// # .summary, -/// # Some("This route was wrapped with RouteWrapper".to_owned()) -/// # ); -/// ``` -pub trait AsApiRoute { - /// Returns the OpenAPI path item for the route. - /// - /// # Examples - /// - /// ``` - /// use aide::openapi::PathItem; - /// use cot::aide::openapi::Operation; - /// use cot::openapi::{AsApiOperation, AsApiRoute, RouteContext}; - /// use schemars::SchemaGenerator; - /// - /// struct RouteWrapper(T); - /// - /// impl AsApiRoute for RouteWrapper { - /// fn as_api_route( - /// &self, - /// route_context: &RouteContext<'_>, - /// schema_generator: &mut SchemaGenerator, - /// ) -> PathItem { - /// let mut spec = self.0.as_api_route(route_context, schema_generator); - /// spec.summary = Some("This route was wrapped with RouteWrapper".to_owned()); - /// spec - /// } - /// } - /// - /// # assert_eq!( - /// # RouteWrapper(cot::router::method::openapi::ApiMethodRouter::new()) - /// # .as_api_route(&RouteContext::new(), &mut SchemaGenerator::default()) - /// # .summary, - /// # Some("This route was wrapped with RouteWrapper".to_owned()) - /// # ); - /// ``` - fn as_api_route( - &self, - route_context: &RouteContext<'_>, - schema_generator: &mut SchemaGenerator, - ) -> PathItem; -} - -/// Returns the OpenAPI operation for the route - a specific HTTP operation -/// (GET, POST, etc.) at a given URL. -/// -/// You shouldn't typically need to implement this trait yourself. It is -/// implemented automatically for all functions that can be used as request -/// handlers, as long as all the parameters and the return type implement the -/// [`ApiOperationPart`] trait. You might need to implement it yourself if you -/// are creating a wrapper over a [`RequestHandler`] that adds some extra -/// functionality, or you want to modify the OpenAPI specs or create them -/// manually. -/// -/// # Examples -/// -/// ``` -/// use cot::aide::openapi::Operation; -/// use cot::openapi::{AsApiOperation, RouteContext}; -/// use schemars::SchemaGenerator; -/// -/// struct HandlerWrapper(T); -/// -/// impl AsApiOperation for HandlerWrapper { -/// fn as_api_operation( -/// &self, -/// route_context: &RouteContext<'_>, -/// schema_generator: &mut SchemaGenerator, -/// ) -> Option { -/// // a wrapper that hides the operation from OpenAPI spec -/// None -/// } -/// } -/// -/// # assert!(HandlerWrapper::<()>(()).as_api_operation(&RouteContext::new(), &mut SchemaGenerator::default()).is_none()); -/// ``` -pub trait AsApiOperation { - /// Returns the OpenAPI operation for the route. - /// - /// # Examples - /// - /// ``` - /// use cot::aide::openapi::Operation; - /// use cot::openapi::{AsApiOperation, RouteContext}; - /// use schemars::SchemaGenerator; - /// - /// struct HandlerWrapper(T); - /// - /// impl AsApiOperation for HandlerWrapper { - /// fn as_api_operation( - /// &self, - /// route_context: &RouteContext<'_>, - /// schema_generator: &mut SchemaGenerator, - /// ) -> Option { - /// // a wrapper that hides the operation from OpenAPI spec - /// None - /// } - /// } - /// - /// # assert!(HandlerWrapper::<()>(()).as_api_operation(&RouteContext::new(), &mut SchemaGenerator::default()).is_none()); - /// ``` - fn as_api_operation( - &self, - route_context: &RouteContext<'_>, - schema_generator: &mut SchemaGenerator, - ) -> Option; -} - -pub(crate) trait BoxApiRequestHandler: BoxRequestHandler + AsApiOperation {} - -pub(crate) fn into_box_api_request_handler( - handler: H, -) -> impl BoxApiRequestHandler -where - H: RequestHandler + AsApiOperation + Send + Sync, -{ - struct Inner( - H, - PhantomData HandlerParams>, - PhantomData ApiParams>, - ); - - impl BoxRequestHandler for Inner - where - H: RequestHandler + AsApiOperation + Send + Sync, - { - fn handle( - &self, - request: Request, - ) -> Pin> + Send + '_>> { - Box::pin(self.0.handle(request)) - } - } - - impl AsApiOperation for Inner - where - H: RequestHandler + AsApiOperation + Send + Sync, - { - fn as_api_operation( - &self, - route_context: &RouteContext<'_>, - schema_generator: &mut SchemaGenerator, - ) -> Option { - self.0.as_api_operation(route_context, schema_generator) - } - } - - impl BoxApiRequestHandler for Inner where - H: RequestHandler + AsApiOperation + Send + Sync - { - } - - Inner(handler, PhantomData, PhantomData) -} - -pub(crate) trait BoxApiEndpointRequestHandler: BoxRequestHandler + AsApiRoute { - // TODO: consider removing this when Rust trait_upcasting is stabilized and we - // bump the MSRV (lands in Rust 1.86) - fn as_box_request_handler(&self) -> &(dyn BoxRequestHandler + Send + Sync); -} - -pub(crate) fn into_box_api_endpoint_request_handler( - handler: H, -) -> impl BoxApiEndpointRequestHandler -where - H: RequestHandler + AsApiRoute + Send + Sync, -{ - struct Inner(H, PhantomData HandlerParams>); - - impl BoxRequestHandler for Inner - where - H: RequestHandler + AsApiRoute + Send + Sync, - { - fn handle( - &self, - request: Request, - ) -> Pin> + Send + '_>> { - Box::pin(self.0.handle(request)) - } - } - - impl AsApiRoute for Inner - where - H: RequestHandler + AsApiRoute + Send + Sync, - { - fn as_api_route( - &self, - route_context: &RouteContext<'_>, - schema_generator: &mut SchemaGenerator, - ) -> PathItem { - self.0.as_api_route(route_context, schema_generator) - } - } - - impl BoxApiEndpointRequestHandler for Inner - where - H: RequestHandler + AsApiRoute + Send + Sync, - { - fn as_box_request_handler(&self) -> &(dyn BoxRequestHandler + Send + Sync) { - self - } - } - - Inner(handler, PhantomData) -} - /// A wrapper type that allows using non-OpenAPI handlers and request parameters /// in OpenAPI routes. /// @@ -512,8 +236,8 @@ where /// use cot::request::RequestHead; /// use cot::request::extractors::FromRequestHead; /// use cot::response::Response; -/// use cot::router::Route; -/// use cot::router::method::openapi::api_get; +/// use cot_core::router::Route; +/// use cot_core::router::method::method::api_get; /// /// struct MyExtractor; /// impl FromRequestHead for MyExtractor { @@ -529,15 +253,17 @@ where /// # unimplemented!() /// } /// -/// let router = -/// cot::router::Router::with_urls([Route::with_api_handler("/with_api", api_get(handler))]); +/// let router = cot_core::router::Router::with_urls([Route::with_api_handler( +/// "/with_api", +/// api_get(handler), +/// )]); /// ``` /// /// ``` /// use cot::openapi::NoApi; /// use cot::response::Response; -/// use cot::router::Route; -/// use cot::router::method::openapi::api_get; +/// use cot_core::router::Route; +/// use cot_core::router::method::method::api_get; /// /// async fn handler_with_openapi() -> cot::Result { /// // ... @@ -548,7 +274,7 @@ where /// # unimplemented!() /// } /// -/// let router = cot::router::Router::with_urls([Route::with_api_handler( +/// let router = cot_core::router::Router::with_urls([Route::with_api_handler( /// "/with_api", /// // POST will be ignored in OpenAPI spec /// api_get(handler_with_openapi).post(NoApi(handler_without_openapi)), @@ -590,258 +316,7 @@ impl AsApiOperation for NoApi { } } -macro_rules! impl_as_openapi_operation { - ($($ty:ident),*) => { - impl AsApiOperation<($($ty,)*)> for T - where - T: Fn($($ty,)*) -> R + Clone + Send + Sync + 'static, - $($ty: ApiOperationPart,)* - R: for<'a> Future + Send, - Response: ApiOperationResponse, - { - #[allow( - clippy::allow_attributes, - non_snake_case, - reason = "for the case where there are no FromRequestHead params" - )] - fn as_api_operation( - &self, - route_context: &RouteContext<'_>, - schema_generator: &mut SchemaGenerator, - ) -> Option { - let mut operation = Operation::default(); - - $( - $ty::modify_api_operation( - &mut operation, - &route_context, - schema_generator - ); - )* - let responses = Response::api_operation_responses( - &mut operation, - &route_context, - schema_generator - ); - let operation_responses = operation.responses.get_or_insert_default(); - for (response_code, response) in responses { - if let Some(response_code) = response_code { - operation_responses.responses.insert( - response_code, - ReferenceOr::Item(response), - ); - } else { - operation_responses.default = Some(ReferenceOr::Item(response)); - } - } - - Some(operation) - } - } - }; -} - -handle_all_parameters!(impl_as_openapi_operation); - -/// A trait that can be implemented for types that should be taken into -/// account when generating OpenAPI paths. -/// -/// When implementing this trait for a type, you can modify the `Operation` -/// object to add information about the type to the OpenAPI spec. The -/// default implementation of [`ApiOperationPart::modify_api_operation`] -/// does nothing to indicate that the type has no effect on the OpenAPI spec. -/// -/// # Example -/// -/// ``` -/// use cot::aide::openapi::{Operation, MediaType, ReferenceOr, RequestBody}; -/// use cot::openapi::{ApiOperationPart, RouteContext}; -/// use cot::request::Request; -/// use cot::request::extractors::FromRequest; -/// use indexmap::IndexMap; -/// use cot::schemars::SchemaGenerator; -/// use serde::de::DeserializeOwned; -/// -/// pub struct Json(pub D); -/// -/// impl FromRequest for Json { -/// async fn from_request(head: &cot::request::RequestHead, body: cot::Body) -> cot::Result { -/// // parse the request body as JSON -/// # unimplemented!() -/// } -/// } -/// -/// impl ApiOperationPart for Json { -/// fn modify_api_operation( -/// operation: &mut Operation, -/// _route_context: &RouteContext<'_>, -/// schema_generator: &mut SchemaGenerator, -/// ) { -/// operation.request_body = Some(ReferenceOr::Item(RequestBody { -/// content: IndexMap::from([( -/// "application/json".to_owned(), -/// MediaType { -/// schema: Some(aide::openapi::SchemaObject { -/// json_schema: D::json_schema(schema_generator), -/// external_docs: None, -/// example: None, -/// }), -/// ..Default::default() -/// }, -/// )]), -/// ..Default::default() -/// })); -/// } -/// } -/// -/// # let mut operation = Operation::default(); -/// # let route_context = RouteContext::new(); -/// # let mut schema_generator = SchemaGenerator::default(); -/// # Json::::modify_api_operation(&mut operation, &route_context, &mut schema_generator); -/// # assert!(operation.request_body.is_some()); -/// ``` -pub trait ApiOperationPart { - /// Modify the OpenAPI operation object. - /// - /// This function is called by the framework when generating the OpenAPI - /// spec for a route. You can use this function to add custom information - /// to the operation object. - /// - /// The default implementation does nothing. - /// - /// # Examples - /// - /// ``` - /// use aide::openapi::Operation; - /// use cot::openapi::{ApiOperationPart, RouteContext}; - /// use schemars::SchemaGenerator; - /// - /// struct MyExtractor(T); - /// - /// impl ApiOperationPart for MyExtractor { - /// fn modify_api_operation( - /// operation: &mut Operation, - /// _route_context: &RouteContext<'_>, - /// _schema_generator: &mut SchemaGenerator, - /// ) { - /// // Add custom OpenAPI information to the operation - /// } - /// } - /// ``` - #[expect(unused)] - fn modify_api_operation( - operation: &mut Operation, - route_context: &RouteContext<'_>, - schema_generator: &mut SchemaGenerator, - ) { - } -} - -/// A trait that generates OpenAPI response objects for handler return types. -/// -/// This trait is implemented for types that can be returned from request -/// handlers and need to be documented in the OpenAPI specification. It allows -/// you to specify how a type should be represented in the OpenAPI -/// documentation. -/// -/// # Examples -/// -/// ``` -/// use cot::aide::openapi::{MediaType, Operation, Response, StatusCode}; -/// use cot::openapi::{ApiOperationResponse, RouteContext}; -/// use indexmap::IndexMap; -/// use schemars::SchemaGenerator; -/// -/// // A custom response type -/// struct MyResponse(T); -/// -/// impl ApiOperationResponse for MyResponse { -/// fn api_operation_responses( -/// _operation: &mut Operation, -/// _route_context: &RouteContext<'_>, -/// schema_generator: &mut SchemaGenerator, -/// ) -> Vec<(Option, Response)> { -/// vec![( -/// Some(StatusCode::Code(201)), -/// Response { -/// description: "Created".to_string(), -/// content: IndexMap::from([( -/// "application/json".to_string(), -/// MediaType { -/// schema: Some(aide::openapi::SchemaObject { -/// json_schema: T::json_schema(schema_generator), -/// external_docs: None, -/// example: None, -/// }), -/// ..Default::default() -/// }, -/// )]), -/// ..Default::default() -/// }, -/// )] -/// } -/// } -/// ``` -pub trait ApiOperationResponse { - /// Returns a list of OpenAPI response objects for this type. - /// - /// This method is called by the framework when generating the OpenAPI - /// specification for a route. It should return a list of responses - /// that this type can produce, along with their status codes. - /// - /// The status code can be `None` to indicate a default response. - /// - /// # Examples - /// - /// ``` - /// use cot::aide::openapi::{MediaType, Operation, Response, StatusCode}; - /// use cot::openapi::{ApiOperationResponse, RouteContext}; - /// use indexmap::IndexMap; - /// use schemars::SchemaGenerator; - /// - /// // A custom response type that always returns 201 Created - /// struct CreatedResponse(T); - /// - /// impl ApiOperationResponse for CreatedResponse { - /// fn api_operation_responses( - /// _operation: &mut Operation, - /// _route_context: &RouteContext<'_>, - /// schema_generator: &mut SchemaGenerator, - /// ) -> Vec<(Option, Response)> { - /// vec![( - /// Some(StatusCode::Code(201)), - /// Response { - /// description: "Created".to_string(), - /// content: IndexMap::from([( - /// "application/json".to_string(), - /// MediaType { - /// schema: Some(aide::openapi::SchemaObject { - /// json_schema: T::json_schema(schema_generator), - /// external_docs: None, - /// example: None, - /// }), - /// ..Default::default() - /// }, - /// )]), - /// ..Default::default() - /// }, - /// )] - /// } - /// } - /// ``` - #[expect(unused)] - fn api_operation_responses( - operation: &mut Operation, - route_context: &RouteContext<'_>, - schema_generator: &mut SchemaGenerator, - ) -> Vec<(Option, aide::openapi::Response)> { - Vec::new() - } -} - -impl ApiOperationPart for Request {} impl ApiOperationPart for Urls {} -impl ApiOperationPart for Method {} impl ApiOperationPart for Session {} impl ApiOperationPart for Auth {} #[cfg(feature = "db")] @@ -871,93 +346,6 @@ impl ApiOperationPart for Json { } } -impl ApiOperationPart for Path { - #[track_caller] - fn modify_api_operation( - operation: &mut Operation, - route_context: &RouteContext<'_>, - schema_generator: &mut SchemaGenerator, - ) { - let mut schema = D::json_schema(schema_generator); - let schema_obj = schema.ensure_object(); - - if let Some(items) = schema_obj.get("prefixItems") { - // a tuple of path params, e.g. Path<(i32, String)> - - if let Value::Array(item_list) = items { - assert_eq!( - route_context.param_names.len(), - item_list.len(), - "the number of path parameters in the route URL must match \ - the number of params in the Path type (found path params: {:?})", - route_context.param_names, - ); - - for (¶m_name, item) in route_context.param_names.iter().zip(item_list.iter()) { - let array_item = Schema::try_from(item.clone()) - .expect("schema.items must contain valid schemas"); - - add_path_param(operation, array_item, param_name.to_owned()); - } - } - } else if let Some(properties) = schema_obj.get("properties") { - // a struct of path params, e.g. Path - - if let Value::Object(properties) = properties { - let mut route_context_sorted = route_context.param_names.to_vec(); - route_context_sorted.sort_unstable(); - let mut object_props_sorted = properties.keys().collect::>(); - object_props_sorted.sort(); - - assert_eq!( - route_context_sorted, object_props_sorted, - "Path parameters in the route info must exactly match parameters \ - in the Path type. Make sure that the type you pass to Path contains \ - all the parameters for the route, and that the names match exactly." - ); - - for (key, item) in properties { - let object_item = Schema::try_from(item.clone()) - .expect("schema.properties must contain valid schemas"); - - add_path_param(operation, object_item, key.clone()); - } - } - } else if schema_obj.contains_key("type") { - // single path param, e.g. Path - - assert_eq!( - route_context.param_names.len(), - 1, - "the number of path parameters in the route URL must equal \ - to 1 if a single parameter was passed to the Path type (found path params: {:?})", - route_context.param_names, - ); - - add_path_param(operation, schema, route_context.param_names[0].to_owned()); - } - } -} - -impl ApiOperationPart for UrlQuery { - fn modify_api_operation( - operation: &mut Operation, - _route_context: &RouteContext<'_>, - schema_generator: &mut SchemaGenerator, - ) { - let schema = D::json_schema(schema_generator); - - if let Some(Value::Object(properties)) = schema.get("properties") { - for (key, item) in properties { - let object_item = Schema::try_from(item.clone()) - .expect("schema.properties must contain valid schemas"); - - add_query_param(operation, object_item, key.clone()); - } - } - } -} - impl ApiOperationPart for RequestForm { fn modify_api_operation( operation: &mut Operation, @@ -995,70 +383,6 @@ impl ApiOperationPart for RequestForm { } } -fn add_path_param(operation: &mut Operation, mut schema: Schema, param_name: String) { - let required = extract_is_required(&mut schema); - - operation - .parameters - .push(ReferenceOr::Item(Parameter::Path { - parameter_data: param_with_name(param_name, schema, required), - style: PathStyle::default(), - })); -} - -fn add_query_param(operation: &mut Operation, mut schema: Schema, param_name: String) { - let required = extract_is_required(&mut schema); - - operation - .parameters - .push(ReferenceOr::Item(Parameter::Query { - parameter_data: param_with_name(param_name, schema, required), - allow_reserved: false, - style: QueryStyle::default(), - allow_empty_value: None, - })); -} - -fn extract_is_required(object_item: &mut Schema) -> bool { - let object = object_item.ensure_object(); - let obj_type = object.get_mut("type"); - let null_value = Value::String("null".to_string()); - - if let Some(Value::Array(types)) = obj_type { - if types.contains(&null_value) { - // If the type is nullable, we need to remove "null" from the types - // and return false, indicating that the parameter is not required. - types.retain(|t| t != &null_value); - false - } else { - // If "null" is not in the types, we assume it's a required parameter - true - } - } else { - // If the type is a single string (or some other unknown value), we assume it's - // a required parameter - true - } -} - -fn param_with_name(param_name: String, schema: Schema, required: bool) -> ParameterData { - ParameterData { - name: param_name, - description: None, - required, - deprecated: None, - format: ParameterSchemaOrContent::Schema(aide::openapi::SchemaObject { - json_schema: schema, - external_docs: None, - example: None, - }), - example: None, - examples: IndexMap::default(), - explode: None, - extensions: IndexMap::default(), - } -} - impl ApiOperationResponse for Json { fn api_operation_responses( _operation: &mut Operation, @@ -1086,68 +410,16 @@ impl ApiOperationResponse for Json { } } -impl ApiOperationResponse for WithExtension { - fn api_operation_responses( - operation: &mut Operation, - route_context: &RouteContext<'_>, - schema_generator: &mut SchemaGenerator, - ) -> Vec<(Option, aide::openapi::Response)> { - T::api_operation_responses(operation, route_context, schema_generator) - } -} - -impl ApiOperationResponse for crate::Result { - fn api_operation_responses( - _operation: &mut Operation, - _route_context: &RouteContext<'_>, - _schema_generator: &mut SchemaGenerator, - ) -> Vec<(Option, aide::openapi::Response)> { - vec![( - None, - aide::openapi::Response { - description: "*<unspecified>*".to_string(), - ..Default::default() - }, - )] - } -} - -// we don't require `E: ApiOperationResponse` here because a global error -// handler will typically take care of generating OpenAPI responses for errors -// -// we might want to add a version for `E: ApiOperationResponse` when (if ever) -// specialization lands in Rust: https://github.com/rust-lang/rust/issues/31844 -impl ApiOperationResponse for Result -where - T: ApiOperationResponse, -{ - fn api_operation_responses( - operation: &mut Operation, - route_context: &RouteContext<'_>, - schema_generator: &mut SchemaGenerator, - ) -> Vec<(Option, aide::openapi::Response)> { - let mut responses = Vec::new(); - - let ok_response = T::api_operation_responses(operation, route_context, schema_generator); - for (status_code, response) in ok_response { - responses.push((status_code, response)); - } - - responses - } -} - #[cfg(test)] mod tests { use aide::openapi::{Operation, Parameter}; + use cot_core::html::Html; use schemars::SchemaGenerator; use serde::{Deserialize, Serialize}; use super::*; - use crate::html::Html; use crate::json::Json; use crate::openapi::AsApiOperation; - use crate::request::extractors::{Path, UrlQuery}; #[derive(Deserialize, Serialize, schemars::JsonSchema)] struct TestRequest { diff --git a/cot/src/openapi/swagger_ui.rs b/cot/src/openapi/swagger_ui.rs index 02bd1784..7caa0eb1 100644 --- a/cot/src/openapi/swagger_ui.rs +++ b/cot/src/openapi/swagger_ui.rs @@ -6,14 +6,14 @@ use std::borrow::Cow; use std::sync::{Arc, OnceLock}; use bytes::Bytes; +use cot_core::html::Html; +use cot_core::router::{Route, Router}; use swagger_ui_redist::SwaggerUiStaticFile; use crate::App; -use crate::html::Html; use crate::json::Json; use crate::request::extractors::StaticFiles; use crate::request::{Request, RequestExt}; -use crate::router::{Route, Router}; use crate::static_files::StaticFile; /// A wrapper around the Swagger UI functionality. diff --git a/cot/src/project.rs b/cot/src/project.rs index 1ecaf462..5e84206b 100644 --- a/cot/src/project.rs +++ b/cot/src/project.rs @@ -25,6 +25,15 @@ use std::panic::AssertUnwindSafe; use std::path::PathBuf; use std::sync::Arc; +use crate::error::UncaughtPanic; +use crate::error::error_impl::impl_into_cot_error; +use crate::error::handler::{DynErrorPageHandler, RequestOuterError}; +use crate::handler::BoxedHandler; +use crate::html::Html; +use crate::middleware::{IntoCotError, IntoCotErrorLayer, IntoCotResponse, IntoCotResponseLayer}; +use crate::request::{AppName, Request, RequestExt, RequestHead}; +use crate::response::{IntoResponse, Response}; +use crate::router::{Route, Router, RouterService}; use askama::Template; use async_trait::async_trait; use axum::handler::HandlerWithoutStateExt; @@ -47,16 +56,7 @@ use crate::config::{AuthBackendConfig, ProjectConfig}; use crate::db::Database; #[cfg(feature = "db")] use crate::db::migrations::{MigrationEngine, SyncDynMigration}; -use crate::error::UncaughtPanic; -use crate::error::error_impl::impl_into_cot_error; -use crate::error::handler::{DynErrorPageHandler, RequestOuterError}; use crate::error_page::Diagnostics; -use crate::handler::BoxedHandler; -use crate::html::Html; -use crate::middleware::{IntoCotError, IntoCotErrorLayer, IntoCotResponse, IntoCotResponseLayer}; -use crate::request::{AppName, Request, RequestExt, RequestHead}; -use crate::response::{IntoResponse, Response}; -use crate::router::{Route, Router, RouterService}; use crate::static_files::StaticFile; use crate::utils::accept_header_parser::AcceptHeaderParser; use crate::{Body, Error, cli, error_page}; @@ -115,8 +115,8 @@ pub trait App: Send + Sync { /// /// ``` /// use cot::App; - /// use cot::html::Html; - /// use cot::router::{Route, Router}; + /// use cot_core::html::Html; + /// use cot_core::router::{Route, Router}; /// /// async fn index() -> Html { /// Html::new("Hello world!") @@ -407,9 +407,9 @@ pub trait Project { /// /// ``` /// use cot::Project; - /// use cot::error::handler::{DynErrorPageHandler, RequestError}; - /// use cot::html::Html; - /// use cot::response::IntoResponse; + /// use cot_core::error::handler::{DynErrorPageHandler, RequestError}; + /// use cot_core::html::Html; + /// use cot_core::response::IntoResponse; /// /// struct MyProject; /// impl Project for MyProject { @@ -1580,8 +1580,8 @@ impl>> ProjectContext { /// # Examples /// /// ``` - /// use cot::request::{Request, RequestExt}; - /// use cot::response::Response; + /// use cot_core::request::{Request, RequestExt}; + /// use cot_core::response::Response; /// /// async fn index(request: Request) -> cot::Result { /// let config = request.context().config(); @@ -1620,8 +1620,8 @@ impl>>> ProjectContext { /// # Examples /// /// ``` - /// use cot::request::{Request, RequestExt}; - /// use cot::response::Response; + /// use cot_core::request::{Request, RequestExt}; + /// use cot_core::response::Response; /// /// async fn index(request: Request) -> cot::Result { /// let apps = request.context().apps(); @@ -1693,8 +1693,8 @@ impl>> ProjectContext { /// # Examples /// /// ``` - /// use cot::request::{Request, RequestExt}; - /// use cot::response::Response; + /// use cot_core::request::{Request, RequestExt}; + /// use cot_core::response::Response; /// /// async fn index(request: Request) -> cot::Result { /// let router = request.context().config(); @@ -1718,8 +1718,8 @@ impl>> ProjectContext { /// # Examples /// /// ``` - /// use cot::request::{Request, RequestExt}; - /// use cot::response::Response; + /// use cot_core::request::{Request, RequestExt}; + /// use cot_core::response::Response; /// /// async fn index(request: Request) -> cot::Result { /// let auth_backend = request.context().auth_backend(); @@ -1740,8 +1740,8 @@ impl>>> ProjectContext { /// # Examples /// /// ``` - /// use cot::request::{Request, RequestExt}; - /// use cot::response::Response; + /// use cot_core::request::{Request, RequestExt}; + /// use cot_core::response::Response; /// /// async fn index(request: Request) -> cot::Result { /// let database = request.context().try_database(); @@ -1768,8 +1768,8 @@ impl>>> ProjectContext { /// # Examples /// /// ``` - /// use cot::request::{Request, RequestExt}; - /// use cot::response::Response; + /// use cot_core::request::{Request, RequestExt}; + /// use cot_core::response::Response; /// /// async fn index(request: Request) -> cot::Result { /// let database = request.context().database(); @@ -2137,6 +2137,9 @@ async fn shutdown_signal() { mod tests { use std::task::{Context, Poll}; + use cot_core::error::handler::{RequestError, RequestOuterError}; + use cot_core::html::Html; + use cot_core::request::extractors::FromRequestHead; use tower::util::MapResultLayer; use tower::{ServiceExt, service_fn}; @@ -2144,9 +2147,6 @@ mod tests { use crate::StatusCode; use crate::auth::UserId; use crate::config::SecretKey; - use crate::error::handler::{RequestError, RequestOuterError}; - use crate::html::Html; - use crate::request::extractors::FromRequestHead; use crate::test::serial_guard; struct TestApp; diff --git a/cot/src/request.rs b/cot/src/request.rs index 092e0d57..107cfba1 100644 --- a/cot/src/request.rs +++ b/cot/src/request.rs @@ -1,38 +1,17 @@ -//! HTTP request type and helper methods. -//! -//! Cot uses the [`Request`](http::Request) type from the [`http`] crate -//! to represent incoming HTTP requests. However, it also provides a -//! [`RequestExt`] trait that contain various helper methods for working with -//! HTTP requests. These methods are used to access the application context, -//! project configuration, path parameters, and more. You probably want to have -//! a `use` statement for [`RequestExt`] in your code most of the time to be -//! able to use these functions: -//! -//! ``` -//! use cot::request::RequestExt; -//! ``` - -use std::future::Future; use std::sync::Arc; +use cot::db::Database; +use cot::request::extractors::InvalidContentType; +use cot_core::router::Router; use http::Extensions; -use indexmap::IndexMap; - -#[cfg(feature = "db")] -use crate::db::Database; -use crate::error::error_impl::impl_into_cot_error; -use crate::request::extractors::FromRequestHead; -use crate::router::Router; -use crate::{Body, Result}; - pub mod extractors; -mod path_params_deserializer; -/// HTTP request type. -pub type Request = http::Request; +#[doc(inline)] +pub use cot_core::request::{ + AppName, PathParams, PathParamsDeserializerError, Request, RequestHead, RouteName, +}; -/// HTTP request head type. -pub type RequestHead = http::request::Parts; +use crate::request::extractors::FromRequestHead; mod private { pub trait Sealed {} @@ -51,17 +30,17 @@ pub trait RequestExt: private::Sealed { /// # Examples /// /// ``` - /// use cot::request::extractors::Path; - /// use cot::request::{Request, RequestExt}; /// use cot::response::Response; + /// use cot_core::request::extractors::Path; + /// use cot_core::request::{Request, RequestExt}; /// - /// async fn my_handler(mut request: Request) -> cot::Result { + /// async fn my_handler(mut request: Request) -> cot_core::Result { /// let path_params = request.extract_from_head::>().await?; /// // ... /// # unimplemented!() /// } /// ``` - fn extract_from_head(&mut self) -> impl Future> + Send + fn extract_from_head(&mut self) -> impl Future> + Send where E: FromRequestHead + 'static; @@ -70,44 +49,44 @@ pub trait RequestExt: private::Sealed { /// # Examples /// /// ``` - /// use cot::request::{Request, RequestExt}; /// use cot::response::Response; + /// use cot_core::request::{Request, RequestExt}; /// - /// async fn my_handler(mut request: Request) -> cot::Result { + /// async fn my_handler(mut request: Request) -> cot_core::Result { /// let context = request.context(); /// // ... do something with the context /// # unimplemented!() /// } /// ``` #[must_use] - fn context(&self) -> &crate::ProjectContext; + fn context(&self) -> &cot::project::ProjectContext; /// Get the project configuration. /// /// # Examples /// /// ``` - /// use cot::request::{Request, RequestExt}; /// use cot::response::Response; + /// use cot_core::request::{Request, RequestExt}; /// - /// async fn my_handler(mut request: Request) -> cot::Result { + /// async fn my_handler(mut request: Request) -> cot_core::Result { /// let config = request.project_config(); /// // ... do something with the config /// # unimplemented!() /// } /// ``` #[must_use] - fn project_config(&self) -> &crate::config::ProjectConfig; + fn project_config(&self) -> &cot::config::ProjectConfig; /// Get the router. /// /// # Examples /// /// ``` - /// use cot::request::{Request, RequestExt}; /// use cot::response::Response; + /// use cot_core::request::{Request, RequestExt}; /// - /// async fn my_handler(mut request: Request) -> cot::Result { + /// async fn my_handler(mut request: Request) -> cot_core::Result { /// let router = request.router(); /// // ... do something with the router /// # unimplemented!() @@ -125,10 +104,10 @@ pub trait RequestExt: private::Sealed { /// # Examples /// /// ``` - /// use cot::request::{Request, RequestExt}; /// use cot::response::Response; + /// use cot_core::request::{Request, RequestExt}; /// - /// async fn my_handler(mut request: Request) -> cot::Result { + /// async fn my_handler(mut request: Request) -> cot_core::Result { /// let app_name = request.app_name(); /// // ... do something with the app name /// # unimplemented!() @@ -145,10 +124,10 @@ pub trait RequestExt: private::Sealed { /// # Examples /// /// ``` - /// use cot::request::{Request, RequestExt}; /// use cot::response::Response; + /// use cot_core::request::{Request, RequestExt}; /// - /// async fn my_handler(mut request: Request) -> cot::Result { + /// async fn my_handler(mut request: Request) -> cot_core::Result { /// let route_name = request.route_name(); /// // ... do something with the route name /// # unimplemented!() @@ -162,10 +141,10 @@ pub trait RequestExt: private::Sealed { /// # Examples /// /// ``` - /// use cot::request::{Request, RequestExt}; /// use cot::response::Response; + /// use cot_core::request::{Request, RequestExt}; /// - /// async fn my_handler(mut request: Request) -> cot::Result { + /// async fn my_handler(mut request: Request) -> cot_core::Result { /// let path_params = request.path_params(); /// // ... do something with the path params /// # unimplemented!() @@ -179,10 +158,10 @@ pub trait RequestExt: private::Sealed { /// # Examples /// /// ``` - /// use cot::request::{Request, RequestExt}; /// use cot::response::Response; + /// use cot_core::request::{Request, RequestExt}; /// - /// async fn my_handler(mut request: Request) -> cot::Result { + /// async fn my_handler(mut request: Request) -> cot_core::Result { /// let path_params = request.path_params_mut(); /// // ... do something with the path params /// # unimplemented!() @@ -196,10 +175,10 @@ pub trait RequestExt: private::Sealed { /// # Examples /// /// ``` - /// use cot::request::{Request, RequestExt}; /// use cot::response::Response; + /// use cot_core::request::{Request, RequestExt}; /// - /// async fn my_handler(mut request: Request) -> cot::Result { + /// async fn my_handler(mut request: Request) -> cot_core::Result { /// let db = request.db(); /// // ... do something with the database /// # unimplemented!() @@ -214,10 +193,10 @@ pub trait RequestExt: private::Sealed { /// # Examples /// /// ``` - /// use cot::request::{Request, RequestExt}; /// use cot::response::Response; + /// use cot_core::request::{Request, RequestExt}; /// - /// async fn my_handler(mut request: Request) -> cot::Result { + /// async fn my_handler(mut request: Request) -> cot_core::Result { /// let content_type = request.content_type(); /// // ... do something with the content type /// # unimplemented!() @@ -235,16 +214,16 @@ pub trait RequestExt: private::Sealed { /// # Examples /// /// ``` - /// use cot::request::{Request, RequestExt}; /// use cot::response::Response; + /// use cot_core::request::{Request, RequestExt}; /// - /// async fn my_handler(mut request: Request) -> cot::Result { + /// async fn my_handler(mut request: Request) -> cot_core::Result { /// request.expect_content_type("application/json")?; /// // ... /// # unimplemented!() /// } /// ``` - fn expect_content_type(&mut self, expected: &'static str) -> Result<()> { + fn expect_content_type(&mut self, expected: &'static str) -> cot_core::Result<()> { let content_type = self .content_type() .map_or("".into(), |value| String::from_utf8_lossy(value.as_bytes())); @@ -266,7 +245,7 @@ pub trait RequestExt: private::Sealed { impl private::Sealed for Request {} impl RequestExt for Request { - async fn extract_from_head(&mut self) -> Result + async fn extract_from_head(&mut self) -> cot_core::Result where E: FromRequestHead + 'static, { @@ -280,13 +259,13 @@ impl RequestExt for Request { } #[track_caller] - fn context(&self) -> &crate::ProjectContext { + fn context(&self) -> &cot::project::ProjectContext { self.extensions() - .get::>() + .get::>() .expect("AppContext extension missing") } - fn project_config(&self) -> &crate::config::ProjectConfig { + fn project_config(&self) -> &cot::config::ProjectConfig { self.context().config() } @@ -334,20 +313,20 @@ impl RequestExt for Request { impl private::Sealed for RequestHead {} impl RequestExt for RequestHead { - async fn extract_from_head(&mut self) -> Result + async fn extract_from_head(&mut self) -> cot_core::Result where E: FromRequestHead + 'static, { E::from_request_head(self).await } - fn context(&self) -> &crate::ProjectContext { + fn context(&self) -> &cot::project::ProjectContext { self.extensions - .get::>() + .get::>() .expect("AppContext extension missing") } - fn project_config(&self) -> &crate::config::ProjectConfig { + fn project_config(&self) -> &cot::config::ProjectConfig { self.context().config() } @@ -391,307 +370,15 @@ impl RequestExt for RequestHead { } } -#[derive(Debug, thiserror::Error)] -#[error("invalid content type; expected `{expected}`, found `{actual}`")] -pub(crate) struct InvalidContentType { - expected: &'static str, - actual: String, -} -impl_into_cot_error!(InvalidContentType, BAD_REQUEST); - -#[repr(transparent)] -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub(crate) struct AppName(pub(crate) String); - -#[repr(transparent)] -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub(crate) struct RouteName(pub(crate) String); - -/// Path parameters extracted from the request URL, and available as a map of -/// strings. -/// -/// This struct is meant to be mainly used using the [`PathParams::parse`] -/// method, which will deserialize the path parameters into a type `T` -/// implementing `serde::DeserializeOwned`. If needed, you can also access the -/// path parameters directly using the [`PathParams::get`] method. -/// -/// # Examples -/// -/// ``` -/// use cot::request::{PathParams, Request, RequestExt}; -/// use cot::response::Response; -/// use cot::test::TestRequestBuilder; -/// -/// async fn my_handler(mut request: Request) -> cot::Result { -/// let path_params = request.path_params(); -/// let name = path_params.get("name").unwrap(); -/// -/// // using more ergonomic syntax: -/// let name: String = request.path_params().parse()?; -/// -/// let name = println!("Hello, {}!", name); -/// // ... -/// # unimplemented!() -/// } -/// ``` -#[derive(Debug, Clone)] -pub struct PathParams { - params: IndexMap, -} - -impl Default for PathParams { - fn default() -> Self { - Self::new() - } -} - -impl PathParams { - /// Creates a new [`PathParams`] instance. - /// - /// # Examples - /// - /// ``` - /// use cot::request::PathParams; - /// - /// let mut path_params = PathParams::new(); - /// path_params.insert("name".into(), "world".into()); - /// assert_eq!(path_params.get("name"), Some("world")); - /// ``` - #[must_use] - pub fn new() -> Self { - Self { - params: IndexMap::new(), - } - } - - /// Inserts a new path parameter. - /// - /// # Examples - /// - /// ``` - /// use cot::request::PathParams; - /// - /// let mut path_params = PathParams::new(); - /// path_params.insert("name".into(), "world".into()); - /// assert_eq!(path_params.get("name"), Some("world")); - /// ``` - pub fn insert(&mut self, name: String, value: String) { - self.params.insert(name, value); - } - - /// Iterates over the path parameters. - /// - /// # Examples - /// - /// ``` - /// use cot::request::PathParams; - /// - /// let mut path_params = PathParams::new(); - /// path_params.insert("name".into(), "world".into()); - /// for (name, value) in path_params.iter() { - /// println!("{}: {}", name, value); - /// } - /// ``` - pub fn iter(&self) -> impl Iterator { - self.params - .iter() - .map(|(name, value)| (name.as_str(), value.as_str())) - } - - /// Returns the number of path parameters. - /// - /// # Examples - /// - /// ``` - /// use cot::request::PathParams; - /// - /// let path_params = PathParams::new(); - /// assert_eq!(path_params.len(), 0); - /// ``` - #[must_use] - pub fn len(&self) -> usize { - self.params.len() - } - - /// Returns `true` if the path parameters are empty. - /// - /// # Examples - /// - /// ``` - /// use cot::request::PathParams; - /// - /// let path_params = PathParams::new(); - /// assert!(path_params.is_empty()); - /// ``` - #[must_use] - pub fn is_empty(&self) -> bool { - self.params.is_empty() - } - - /// Returns the value of a path parameter. - /// - /// # Examples - /// - /// ``` - /// use cot::request::PathParams; - /// - /// let mut path_params = PathParams::new(); - /// path_params.insert("name".into(), "world".into()); - /// assert_eq!(path_params.get("name"), Some("world")); - /// ``` - #[must_use] - pub fn get(&self, name: &str) -> Option<&str> { - self.params.get(name).map(String::as_str) - } - - /// Returns the value of a path parameter at the given index. - /// - /// # Examples - /// - /// ``` - /// use cot::request::PathParams; - /// - /// let mut path_params = PathParams::new(); - /// path_params.insert("name".into(), "world".into()); - /// assert_eq!(path_params.get_index(0), Some("world")); - /// ``` - #[must_use] - pub fn get_index(&self, index: usize) -> Option<&str> { - self.params - .get_index(index) - .map(|(_, value)| value.as_str()) - } - - /// Returns the key of a path parameter at the given index. - /// - /// # Examples - /// - /// ``` - /// use cot::request::PathParams; - /// - /// let mut path_params = PathParams::new(); - /// path_params.insert("name".into(), "world".into()); - /// assert_eq!(path_params.key_at_index(0), Some("name")); - /// ``` - #[must_use] - pub fn key_at_index(&self, index: usize) -> Option<&str> { - self.params.get_index(index).map(|(key, _)| key.as_str()) - } - - /// Deserializes the path parameters into a type `T` implementing - /// `serde::DeserializeOwned`. - /// - /// # Errors - /// - /// Throws an error if the path parameters could not be deserialized. - /// - /// # Examples - /// - /// ``` - /// use cot::request::PathParams; - /// - /// # fn main() -> Result<(), cot::Error> { - /// let mut path_params = PathParams::new(); - /// path_params.insert("hello".into(), "world".into()); - /// - /// let hello: String = path_params.parse()?; - /// assert_eq!(hello, "world"); - /// # Ok(()) - /// # } - /// ``` - /// - /// ``` - /// use cot::request::PathParams; - /// - /// # fn main() -> Result<(), cot::Error> { - /// let mut path_params = PathParams::new(); - /// path_params.insert("hello".into(), "world".into()); - /// path_params.insert("name".into(), "john".into()); - /// - /// let (hello, name): (String, String) = path_params.parse()?; - /// assert_eq!(hello, "world"); - /// assert_eq!(name, "john"); - /// # Ok(()) - /// # } - /// ``` - /// - /// ``` - /// use cot::request::PathParams; - /// use serde::Deserialize; - /// - /// # fn main() -> Result<(), cot::Error> { - /// let mut path_params = PathParams::new(); - /// path_params.insert("hello".into(), "world".into()); - /// path_params.insert("name".into(), "john".into()); - /// - /// #[derive(Deserialize)] - /// struct Params { - /// hello: String, - /// name: String, - /// } - /// - /// let params: Params = path_params.parse()?; - /// assert_eq!(params.hello, "world"); - /// assert_eq!(params.name, "john"); - /// # Ok(()) - /// # } - /// ``` - pub fn parse<'de, T: serde::Deserialize<'de>>( - &'de self, - ) -> std::result::Result { - let deserializer = path_params_deserializer::PathParamsDeserializer::new(self); - serde_path_to_error::deserialize(deserializer).map_err(PathParamsDeserializerError) - } -} - -/// An error that occurs when deserializing path parameters. -#[derive(Debug, Clone, thiserror::Error)] -#[error("could not parse path parameters: {0}")] -pub struct PathParamsDeserializerError( - // A wrapper over the original deserializer error. The exact error reason - // shouldn't be useful to the user, hence we're not exposing it. - #[source] serde_path_to_error::Error, -); -impl_into_cot_error!(PathParamsDeserializerError, BAD_REQUEST); - #[cfg(test)] mod tests { - use super::*; - use crate::request::extractors::Path; - use crate::response::Response; - use crate::router::{Route, Router}; - use crate::test::TestRequestBuilder; - - #[test] - fn path_params() { - let mut path_params = PathParams::new(); - path_params.insert("name".into(), "world".into()); - - assert_eq!(path_params.get("name"), Some("world")); - assert_eq!(path_params.get("missing"), None); - } + use cot::test::TestRequestBuilder; + use cot_core::Body; + use cot_core::request::extractors::Path; + use cot_core::response::Response; + use cot_core::router::Route; - #[test] - fn path_params_parse() { - #[derive(Debug, PartialEq, Eq, serde::Deserialize)] - struct Params { - hello: String, - foo: String, - } - - let mut path_params = PathParams::new(); - path_params.insert("hello".into(), "world".into()); - path_params.insert("foo".into(), "bar".into()); - - let params: Params = path_params.parse().unwrap(); - assert_eq!( - params, - Params { - hello: "world".to_string(), - foo: "bar".to_string(), - } - ); - } + use super::*; #[test] fn request_ext_app_name() { @@ -782,9 +469,9 @@ mod tests { assert!(request.expect_content_type("application/json").is_err()); } - #[cot::test] + #[cot_macros::test] async fn request_ext_extract_from_head() { - async fn handler(mut request: Request) -> Result { + async fn handler(mut request: Request) -> cot_core::Result { let Path(id): Path = request.extract_from_head().await?; assert_eq!(id, "42"); @@ -802,7 +489,7 @@ mod tests { #[test] fn parts_ext_path_params() { - let (mut head, _) = Request::new(Body::empty()).into_parts(); + let (mut head, _) = Request::new(crate::Body::empty()).into_parts(); let mut params = PathParams::new(); params.insert("id".to_string(), "42".to_string()); head.extensions.insert(params); @@ -812,7 +499,7 @@ mod tests { #[test] fn parts_ext_mutating_path_params() { - let (mut head, _) = Request::new(Body::empty()).into_parts(); + let (mut head, _) = Request::new(crate::Body::empty()).into_parts(); head.path_params_mut() .insert("page".to_string(), "1".to_string()); @@ -821,7 +508,7 @@ mod tests { #[test] fn parts_ext_app_name() { - let (mut head, _) = Request::new(Body::empty()).into_parts(); + let (mut head, _) = Request::new(crate::Body::empty()).into_parts(); head.extensions.insert(AppName("test_app".to_string())); assert_eq!(head.app_name(), Some("test_app")); @@ -829,7 +516,7 @@ mod tests { #[test] fn parts_ext_route_name() { - let (mut head, _) = Request::new(Body::empty()).into_parts(); + let (mut head, _) = Request::new(crate::Body::empty()).into_parts(); head.extensions.insert(RouteName("test_route".to_string())); assert_eq!(head.route_name(), Some("test_route")); @@ -837,7 +524,7 @@ mod tests { #[test] fn parts_ext_content_type() { - let (mut head, _) = Request::new(Body::empty()).into_parts(); + let (mut head, _) = Request::new(crate::Body::empty()).into_parts(); head.headers.insert( http::header::CONTENT_TYPE, http::HeaderValue::from_static("text/plain"), @@ -848,16 +535,4 @@ mod tests { Some(&http::HeaderValue::from_static("text/plain")) ); } - - #[cot::test] - async fn path_extract_from_head() { - let (mut head, _) = Request::new(Body::empty()).into_parts(); - - let mut params = PathParams::new(); - params.insert("id".to_string(), "42".to_string()); - head.extensions.insert(params); - - let Path(id): Path = head.extract_from_head().await.unwrap(); - assert_eq!(id, "42"); - } } diff --git a/cot/src/request/extractors.rs b/cot/src/request/extractors.rs index d4c0fc75..5449a85f 100644 --- a/cot/src/request/extractors.rs +++ b/cot/src/request/extractors.rs @@ -1,320 +1,51 @@ -//! Extractors for request data. -//! -//! An extractor is a function that extracts data from a request. The main -//! benefit of using an extractor is that it can be used directly as a parameter -//! in a route handler. -//! -//! An extractor implements either [`FromRequest`] or [`FromRequestHead`]. -//! There are two variants because the request body can only be read once, so it -//! needs to be read in the [`FromRequest`] implementation. Therefore, there can -//! only be one extractor that implements [`FromRequest`] per route handler. -//! -//! # Examples -//! -//! For example, the [`Path`] extractor is used to extract path parameters: -//! -//! ``` -//! use cot::html::Html; -//! use cot::request::extractors::{FromRequest, Path}; -//! use cot::request::{Request, RequestExt}; -//! use cot::router::{Route, Router}; -//! use cot::test::TestRequestBuilder; -//! -//! async fn my_handler(Path(my_param): Path) -> Html { -//! Html::new(format!("Hello {my_param}!")) -//! } -//! -//! # #[tokio::main] -//! # async fn main() -> cot::Result<()> { -//! let router = Router::with_urls([Route::with_handler_and_name( -//! "/{my_param}/", -//! my_handler, -//! "home", -//! )]); -//! let request = TestRequestBuilder::get("/world/") -//! .router(router.clone()) -//! .build(); -//! -//! assert_eq!( -//! router -//! .handle(request) -//! .await? -//! .into_body() -//! .into_bytes() -//! .await?, -//! "Hello world!" -//! ); -//! # Ok(()) -//! # } -//! ``` - -use std::future::Future; use std::sync::Arc; +use cot::auth::Auth; +use cot::form::{Form, FormResult}; +#[cfg(feature = "json")] +use cot::json::Json; +use cot::router::Urls; +use cot::session::Session; +#[doc(inline)] +pub use cot_core::request::extractors::{FromRequest, FromRequestHead, Path, UrlQuery}; +use cot_core::request::{Request, RequestHead}; +use cot_core::{Body, impl_into_cot_error}; use serde::de::DeserializeOwned; -use crate::auth::Auth; -use crate::form::{Form, FormResult}; -#[cfg(feature = "json")] -use crate::json::Json; -use crate::request::{InvalidContentType, PathParams, Request, RequestExt, RequestHead}; -use crate::router::Urls; -use crate::session::Session; -use crate::{Body, Method}; +use crate::request::RequestExt; -/// Trait for extractors that consume the request body. -/// -/// Extractors implementing this trait are used in route handlers that consume -/// the request body and therefore can only be used once per request. -/// -/// See [`crate::request::extractors`] documentation for more information about -/// extractors. -pub trait FromRequest: Sized { - /// Extracts data from the request. - /// - /// # Errors - /// - /// Throws an error if the extractor fails to extract the data from the - /// request. - fn from_request( - head: &RequestHead, - body: Body, - ) -> impl Future> + Send; +#[derive(Debug, thiserror::Error)] +#[error("invalid content type; expected `{expected}`, found `{actual}`")] +pub struct InvalidContentType { + pub(crate) expected: &'static str, + pub(crate) actual: String, } +impl_into_cot_error!(InvalidContentType, BAD_REQUEST); -impl FromRequest for Request { - async fn from_request(head: &RequestHead, body: Body) -> cot::Result { - Ok(Request::from_parts(head.clone(), body)) +impl FromRequestHead for Session { + async fn from_request_head(head: &RequestHead) -> crate::Result { + Ok(Session::from_extensions(&head.extensions).clone()) } } -/// Trait for extractors that don't consume the request body. -/// -/// Extractors implementing this trait are used in route handlers that don't -/// consume the request and therefore can be used multiple times per request. -/// -/// If you need to consume the body of the request, use [`FromRequest`] instead. -/// -/// See [`crate::request::extractors`] documentation for more information about -/// extractors. -pub trait FromRequestHead: Sized { - /// Extracts data from the request head. - /// - /// # Errors - /// - /// Throws an error if the extractor fails to extract the data from the - /// request head. - fn from_request_head(head: &RequestHead) -> impl Future> + Send; -} - impl FromRequestHead for Urls { - async fn from_request_head(head: &RequestHead) -> cot::Result { + async fn from_request_head(head: &RequestHead) -> crate::Result { Ok(Self::from_parts(head)) } } -/// An extractor that extracts data from the URL params. -/// -/// The extractor is generic over a type that implements -/// `serde::de::DeserializeOwned`. -/// -/// # Examples -/// -/// ``` -/// use cot::html::Html; -/// use cot::request::extractors::{FromRequest, Path}; -/// use cot::request::{Request, RequestExt}; -/// use cot::router::{Route, Router}; -/// use cot::test::TestRequestBuilder; -/// -/// async fn my_handler(Path(my_param): Path) -> Html { -/// Html::new(format!("Hello {my_param}!")) -/// } -/// -/// # #[tokio::main] -/// # async fn main() -> cot::Result<()> { -/// let router = Router::with_urls([Route::with_handler_and_name( -/// "/{my_param}/", -/// my_handler, -/// "home", -/// )]); -/// let request = TestRequestBuilder::get("/world/") -/// .router(router.clone()) -/// .build(); -/// -/// assert_eq!( -/// router -/// .handle(request) -/// .await? -/// .into_body() -/// .into_bytes() -/// .await?, -/// "Hello world!" -/// ); -/// # Ok(()) -/// # } -/// ``` -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct Path(pub D); - -impl FromRequestHead for Path { - async fn from_request_head(head: &RequestHead) -> cot::Result { - let params = head +impl FromRequestHead for Auth { + async fn from_request_head(head: &RequestHead) -> crate::Result { + let auth = head .extensions - .get::() - .expect("PathParams extension missing") - .parse()?; - Ok(Self(params)) - } -} - -/// An extractor that extracts data from the URL query parameters. -/// -/// The extractor is generic over a type that implements -/// `serde::de::DeserializeOwned`. -/// -/// # Example -/// -/// ``` -/// use cot::RequestHandler; -/// use cot::html::Html; -/// use cot::request::extractors::{FromRequest, UrlQuery}; -/// use cot::router::{Route, Router}; -/// use cot::test::TestRequestBuilder; -/// use serde::Deserialize; -/// -/// #[derive(Deserialize)] -/// struct MyQuery { -/// hello: String, -/// } -/// -/// async fn my_handler(UrlQuery(query): UrlQuery) -> Html { -/// Html::new(format!("Hello {}!", query.hello)) -/// } -/// -/// # #[tokio::main] -/// # async fn main() -> cot::Result<()> { -/// let request = TestRequestBuilder::get("/?hello=world").build(); -/// -/// assert_eq!( -/// my_handler -/// .handle(request) -/// .await? -/// .into_body() -/// .into_bytes() -/// .await?, -/// "Hello world!" -/// ); -/// # Ok(()) -/// # } -/// ``` -#[derive(Debug, Clone, Copy, Default)] -pub struct UrlQuery(pub T); - -impl FromRequestHead for UrlQuery -where - D: DeserializeOwned, -{ - async fn from_request_head(head: &RequestHead) -> cot::Result { - let query = head.uri.query().unwrap_or_default(); - - let deserializer = - serde_html_form::Deserializer::new(form_urlencoded::parse(query.as_bytes())); - - let value = - serde_path_to_error::deserialize(deserializer).map_err(QueryParametersParseError)?; - - Ok(UrlQuery(value)) - } -} - -#[derive(Debug, thiserror::Error)] -#[error("could not parse query parameters: {0}")] -struct QueryParametersParseError(serde_path_to_error::Error); -impl_into_cot_error!(QueryParametersParseError, BAD_REQUEST); - -/// Extractor that gets the request body as JSON and deserializes it into a type -/// `T` implementing `serde::de::DeserializeOwned`. -/// -/// The content type of the request must be `application/json`. -/// -/// # Errors -/// -/// Throws an error if the content type is not `application/json`. -/// Throws an error if the request body could not be read. -/// Throws an error if the request body could not be deserialized - either -/// because the JSON is invalid or because the deserialization to the target -/// structure failed. -/// -/// # Example -/// -/// ``` -/// use cot::RequestHandler; -/// use cot::json::Json; -/// use cot::test::TestRequestBuilder; -/// use serde::{Deserialize, Serialize}; -/// -/// #[derive(Serialize, Deserialize)] -/// struct MyData { -/// hello: String, -/// } -/// -/// async fn my_handler(Json(data): Json) -> Json { -/// Json(data) -/// } -/// -/// # #[tokio::main] -/// # async fn main() -> cot::Result<()> { -/// let request = TestRequestBuilder::get("/") -/// .json(&MyData { -/// hello: "world".to_string(), -/// }) -/// .build(); -/// -/// assert_eq!( -/// my_handler -/// .handle(request) -/// .await? -/// .into_body() -/// .into_bytes() -/// .await?, -/// "{\"hello\":\"world\"}" -/// ); -/// # Ok(()) -/// # } -/// ``` -#[cfg(feature = "json")] -impl FromRequest for Json { - async fn from_request(head: &RequestHead, body: Body) -> cot::Result { - let content_type = head - .headers - .get(http::header::CONTENT_TYPE) - .map_or("".into(), |value| String::from_utf8_lossy(value.as_bytes())); - if content_type != cot::headers::JSON_CONTENT_TYPE { - return Err(InvalidContentType { - expected: cot::headers::JSON_CONTENT_TYPE, - actual: content_type.into_owned(), - } - .into()); - } - - let bytes = body.into_bytes().await?; - - let deserializer = &mut serde_json::Deserializer::from_slice(&bytes); - let result = - serde_path_to_error::deserialize(deserializer).map_err(JsonDeserializeError)?; + .get::() + .expect("AuthMiddleware not enabled for the route/project") + .clone(); - Ok(Self(result)) + Ok(auth) } } -#[cfg(feature = "json")] -#[derive(Debug, thiserror::Error)] -#[error("JSON deserialization error: {0}")] -struct JsonDeserializeError(serde_path_to_error::Error); -#[cfg(feature = "json")] -impl_into_cot_error!(JsonDeserializeError, BAD_REQUEST); - /// An extractor that gets the request body as form data and deserializes it /// into a type `F` implementing `cot::form::Form`. /// @@ -332,9 +63,9 @@ impl_into_cot_error!(JsonDeserializeError, BAD_REQUEST); /// /// ``` /// use cot::form::{Form, FormResult}; -/// use cot::html::Html; -/// use cot::request::extractors::RequestForm; /// use cot::test::TestRequestBuilder; +/// use cot_core::html::Html; +/// use cot_core::request::extractors::RequestForm; /// /// #[derive(Form)] /// struct MyForm { @@ -364,19 +95,51 @@ impl_into_cot_error!(JsonDeserializeError, BAD_REQUEST); pub struct RequestForm(pub FormResult); impl FromRequest for RequestForm { - async fn from_request(head: &RequestHead, body: Body) -> cot::Result { + async fn from_request(head: &RequestHead, body: Body) -> crate::Result { let mut request = Request::from_parts(head.clone(), body); Ok(Self(F::from_request(&mut request).await?)) } } +#[cfg(feature = "json")] +impl FromRequest for Json { + async fn from_request(head: &RequestHead, body: Body) -> crate::Result { + let content_type = head + .headers + .get(http::header::CONTENT_TYPE) + .map_or("".into(), |value| String::from_utf8_lossy(value.as_bytes())); + if content_type != cot_core::headers::JSON_CONTENT_TYPE { + return Err(InvalidContentType { + expected: cot_core::headers::JSON_CONTENT_TYPE, + actual: content_type.into_owned(), + } + .into()); + } + + let bytes = body.into_bytes().await?; + + let deserializer = &mut serde_json::Deserializer::from_slice(&bytes); + let result = + serde_path_to_error::deserialize(deserializer).map_err(JsonDeserializeError)?; + + Ok(Self(result)) + } +} + +#[cfg(feature = "json")] +#[derive(Debug, thiserror::Error)] +#[error("JSON deserialization error: {0}")] +struct JsonDeserializeError(serde_path_to_error::Error); +#[cfg(feature = "json")] +impl_into_cot_error!(JsonDeserializeError, BAD_REQUEST); + /// An extractor that gets the database from the request extensions. /// /// # Example /// /// ``` -/// use cot::html::Html; /// use cot::request::extractors::RequestDb; +/// use cot_core::html::Html; /// /// async fn my_handler(RequestDb(db): RequestDb) -> Html { /// // ... do something with the database @@ -394,13 +157,14 @@ impl FromRequest for RequestForm { /// # Ok(()) /// # } /// ``` + #[cfg(feature = "db")] #[derive(Debug)] -pub struct RequestDb(pub Arc); +pub struct RequestDb(pub Arc); #[cfg(feature = "db")] impl FromRequestHead for RequestDb { - async fn from_request_head(head: &RequestHead) -> cot::Result { + async fn from_request_head(head: &RequestHead) -> crate::Result { Ok(Self(head.db().clone())) } } @@ -411,10 +175,10 @@ impl FromRequestHead for RequestDb { /// # Examples /// /// ``` -/// use cot::html::Html; -/// use cot::request::Request; /// use cot::request::extractors::StaticFiles; /// use cot::test::TestRequestBuilder; +/// use cot_core::html::Html; +/// use cot_core::request::Request; /// /// async fn my_handler(static_files: StaticFiles) -> cot::Result { /// let url = static_files.url_for("css/main.css")?; @@ -436,7 +200,7 @@ impl FromRequestHead for RequestDb { /// ``` #[derive(Debug, Clone, PartialEq, Eq)] pub struct StaticFiles { - inner: Arc, + inner: Arc, } impl StaticFiles { @@ -454,9 +218,9 @@ impl StaticFiles { /// # Examples /// /// ``` - /// use cot::html::Html; /// use cot::request::extractors::StaticFiles; /// use cot::test::TestRequestBuilder; + /// use cot_core::html::Html; /// /// async fn my_handler(static_files: StaticFiles) -> cot::Result { /// let url = static_files.url_for("css/main.css")?; @@ -504,102 +268,35 @@ pub enum StaticFilesGetError { impl_into_cot_error!(StaticFilesGetError); impl FromRequestHead for StaticFiles { - async fn from_request_head(head: &RequestHead) -> cot::Result { + async fn from_request_head(head: &RequestHead) -> crate::Result { Ok(StaticFiles { inner: head .extensions - .get::>() + .get::>() .cloned() .expect("StaticFilesMiddleware not enabled for the route/project"), }) } } -// extractor impls for existing types -impl FromRequestHead for RequestHead { - async fn from_request_head(head: &RequestHead) -> cot::Result { - Ok(head.clone()) - } -} - -impl FromRequestHead for Method { - async fn from_request_head(head: &RequestHead) -> cot::Result { - Ok(head.method.clone()) - } -} - -impl FromRequestHead for Session { - async fn from_request_head(head: &RequestHead) -> cot::Result { - Ok(Session::from_extensions(&head.extensions).clone()) - } -} - -impl FromRequestHead for Auth { - async fn from_request_head(head: &RequestHead) -> cot::Result { - let auth = head - .extensions - .get::() - .expect("AuthMiddleware not enabled for the route/project") - .clone(); - - Ok(auth) - } -} - -/// A derive macro that automatically implements the [`FromRequestHead`] trait -/// for structs. -/// -/// This macro generates code to extract each field of the struct from HTTP -/// request head, making it easy to create composite extractors that combine -/// multiple data sources from an incoming request. -/// -/// The macro works by calling [`FromRequestHead::from_request_head`] on each -/// field's type, allowing you to compose extractors seamlessly. All fields must -/// implement the [`FromRequestHead`] trait for the derivation to work. -/// -/// # Requirements -/// -/// - The target struct must have all fields implement [`FromRequestHead`] -/// - Works with named fields, unnamed fields (tuple structs), and unit structs -/// - The struct must be accessible where the macro is used -/// -/// # Examples -/// -/// ## Named Fields -/// -/// ```no_run -/// use cot::request::extractors::{Path, StaticFiles, UrlQuery}; -/// use cot::router::Urls; -/// use cot_macros::FromRequestHead; -/// use serde::Deserialize; -/// -/// #[derive(Debug, FromRequestHead)] -/// pub struct BaseContext { -/// urls: Urls, -/// static_files: StaticFiles, -/// } -/// ``` -pub use cot_macros::FromRequestHead; - -use crate::error::error_impl::impl_into_cot_error; - #[cfg(test)] mod tests { + use cot::request::extractors::Json; + use cot::test::TestRequestBuilder; + use cot_core::request::extractors::FromRequest; use serde::Deserialize; use super::*; - use crate::html::Html; - use crate::request::extractors::{FromRequest, Json, Path, UrlQuery}; - use crate::router::{Route, Router, Urls}; - use crate::test::TestRequestBuilder; - use crate::{Body, reverse}; #[cfg(feature = "json")] - #[cot::test] + #[cot_macros::test] async fn json() { let request = http::Request::builder() .method(http::Method::POST) - .header(http::header::CONTENT_TYPE, cot::headers::JSON_CONTENT_TYPE) + .header( + http::header::CONTENT_TYPE, + cot_core::headers::JSON_CONTENT_TYPE, + ) .body(Body::fixed(r#"{"hello":"world"}"#)) .unwrap(); @@ -609,14 +306,17 @@ mod tests { } #[cfg(feature = "json")] - #[cot::test] + #[cot_macros::test] async fn json_empty() { #[derive(Debug, Deserialize, PartialEq, Eq)] struct TestData {} let request = http::Request::builder() .method(http::Method::POST) - .header(http::header::CONTENT_TYPE, cot::headers::JSON_CONTENT_TYPE) + .header( + http::header::CONTENT_TYPE, + cot_core::headers::JSON_CONTENT_TYPE, + ) .body(Body::fixed("{}")) .unwrap(); @@ -626,7 +326,7 @@ mod tests { } #[cfg(feature = "json")] - #[cot::test] + #[cot_macros::test] async fn json_struct() { #[derive(Debug, Deserialize, PartialEq, Eq)] struct TestDataInner { @@ -640,7 +340,10 @@ mod tests { let request = http::Request::builder() .method(http::Method::POST) - .header(http::header::CONTENT_TYPE, cot::headers::JSON_CONTENT_TYPE) + .header( + http::header::CONTENT_TYPE, + cot_core::headers::JSON_CONTENT_TYPE, + ) .body(Body::fixed(r#"{"inner":{"hello":"world"}}"#)) .unwrap(); @@ -656,62 +359,8 @@ mod tests { ); } - #[cot::test] - async fn path_extraction() { - #[derive(Deserialize, Debug, PartialEq)] - struct TestParams { - id: i32, - name: String, - } - - let (mut head, _body) = Request::new(Body::empty()).into_parts(); - - let mut params = PathParams::new(); - params.insert("id".to_string(), "42".to_string()); - params.insert("name".to_string(), "test".to_string()); - head.extensions.insert(params); - - let Path(extracted): Path = Path::from_request_head(&head).await.unwrap(); - let expected = TestParams { - id: 42, - name: "test".to_string(), - }; - - assert_eq!(extracted, expected); - } - - #[cot::test] - async fn url_query_extraction() { - #[derive(Deserialize, Debug, PartialEq)] - struct QueryParams { - page: i32, - filter: String, - } - - let (mut head, _body) = Request::new(Body::empty()).into_parts(); - head.uri = "https://example.com/?page=2&filter=active".parse().unwrap(); - - let UrlQuery(query): UrlQuery = - UrlQuery::from_request_head(&head).await.unwrap(); - - assert_eq!(query.page, 2); - assert_eq!(query.filter, "active"); - } - - #[cot::test] - async fn url_query_empty() { - #[derive(Deserialize, Debug, PartialEq)] - struct EmptyParams {} - - let (mut head, _body) = Request::new(Body::empty()).into_parts(); - head.uri = "https://example.com/".parse().unwrap(); - - let result: UrlQuery = UrlQuery::from_request_head(&head).await.unwrap(); - assert!(matches!(result, UrlQuery(_))); - } - #[cfg(feature = "json")] - #[cot::test] + #[cot_macros::test] async fn json_invalid_content_type() { let request = http::Request::builder() .method(http::Method::POST) @@ -724,67 +373,14 @@ mod tests { assert!(result.is_err()); } - #[cot::test] - async fn request_form() { - #[derive(Debug, PartialEq, Eq, Form)] - struct MyForm { - hello: String, - foo: String, - } - - let request = TestRequestBuilder::post("/") - .form_data(&[("hello", "world"), ("foo", "bar")]) - .build(); - - let (head, body) = request.into_parts(); - let RequestForm(form_result): RequestForm = - RequestForm::from_request(&head, body).await.unwrap(); - - assert_eq!( - form_result.unwrap(), - MyForm { - hello: "world".to_string(), - foo: "bar".to_string(), - } - ); - } - - #[cot::test] - async fn urls_extraction() { - async fn handler() -> Html { - Html::new("") - } - - let router = Router::with_urls([Route::with_handler_and_name( - "/test/", - handler, - "test_route", - )]); - - let mut request = TestRequestBuilder::get("/test/").router(router).build(); - - let urls: Urls = request.extract_from_head().await.unwrap(); - - assert!(reverse!(urls, "test_route").is_ok()); - } - - #[cot::test] - async fn method_extraction() { - let mut request = TestRequestBuilder::get("/test/").build(); - - let method: Method = request.extract_from_head().await.unwrap(); - - assert_eq!(method, Method::GET); - } - #[cfg(feature = "db")] - #[cot::test] + #[cot_macros::test] #[cfg_attr( miri, ignore = "unsupported operation: can't call foreign function `sqlite3_open_v2` on OS `linux`" )] async fn request_db() { - let db = crate::test::TestDatabase::new_sqlite().await.unwrap(); + let db = cot::test::TestDatabase::new_sqlite().await.unwrap(); let mut test_request = TestRequestBuilder::get("/").database(db.database()).build(); let RequestDb(extracted_db) = test_request.extract_from_head().await.unwrap(); diff --git a/cot/src/response.rs b/cot/src/response.rs index e6233a73..8bbbbff0 100644 --- a/cot/src/response.rs +++ b/cot/src/response.rs @@ -1,191 +1,7 @@ -//! HTTP response type and helper methods. -//! -//! Cot uses the [`Response`](http::Response) type from the [`http`] crate -//! to represent outgoing HTTP responses. However, it also provides a -//! [`ResponseExt`] trait that contain various helper methods for working with -//! HTTP responses. These methods are used to create new responses with HTML -//! content types, redirects, and more. You probably want to have a `use` -//! statement for [`ResponseExt`] in your code most of the time to be able to -//! use these functions: -//! -//! ``` -//! use cot::response::ResponseExt; -//! ``` - -use crate::{Body, StatusCode}; - mod into_response; -/// Derive macro for the [`IntoResponse`] trait. -/// -/// This macro can be applied to enums to automatically implement the -/// [`IntoResponse`] trait. The enum must consist of tuple variants with -/// exactly one field each, where each field type implements [`IntoResponse`]. -/// -/// # Requirements -/// -/// - **Only enums are supported**: This macro will produce a compile error if -/// applied to structs or unions. -/// - **Tuple variants with one field**: Each enum variant must be a tuple -/// variant with exactly one field (e.g., `Variant(Type)`). -/// - **Field types must implement `IntoResponse`**: Each field type must -/// implement the [`IntoResponse`] trait. -/// -/// # Generated Implementation -/// -/// The macro generates an implementation that matches on the enum variants and -/// calls `into_response()` on the inner value: -/// -/// ```compile_fail -/// impl IntoResponse for MyEnum { -/// fn into_response(self) -> cot::Result { -/// use cot::response::IntoResponse; -/// match self { -/// Self::Variant1(inner) => inner.into_response(), -/// Self::Variant2(inner) => inner.into_response(), -/// // ... for each variant -/// } -/// } -/// } -/// ``` -/// -/// # Examples -/// -/// ``` -/// use cot::html::Html; -/// use cot::json::Json; -/// use cot::response::IntoResponse; -/// -/// #[derive(IntoResponse)] -/// enum MyResponse { -/// Json(Json), -/// Html(Html), -/// } -/// ``` -/// -/// [`IntoResponse`]: crate::response::IntoResponse -pub use cot_macros::IntoResponse; -pub use into_response::{ - IntoResponse, WithBody, WithContentType, WithExtension, WithHeader, WithStatus, +#[doc(inline)] +pub use cot_core::response::{ + IntoResponse, Response, ResponseExt, WithBody, WithContentType, WithExtension, WithHeader, + WithStatus, }; - -const RESPONSE_BUILD_FAILURE: &str = "Failed to build response"; - -/// HTTP response type. -pub type Response = http::Response; - -/// HTTP response head type. -pub type ResponseHead = http::response::Parts; - -mod private { - pub trait Sealed {} -} - -/// Extension trait for [`http::Response`] that provides helper methods for -/// working with HTTP response. -/// -/// # Sealed -/// -/// This trait is sealed since it doesn't make sense to be implemented for types -/// outside the context of Cot. -pub trait ResponseExt: Sized + private::Sealed { - /// Create a new response builder. - /// - /// # Examples - /// - /// ``` - /// use cot::StatusCode; - /// use cot::response::{Response, ResponseExt}; - /// - /// let response = Response::builder() - /// .status(StatusCode::OK) - /// .body(cot::Body::empty()) - /// .expect("Failed to build response"); - /// ``` - #[must_use] - fn builder() -> http::response::Builder; - - /// Create a new redirect response. - /// - /// This creates a new [`Response`] object with a status code of - /// [`StatusCode::SEE_OTHER`] and a location header set to the provided - /// location. - /// - /// # Examples - /// - /// ``` - /// use cot::StatusCode; - /// use cot::response::{Response, ResponseExt}; - /// - /// let response = Response::new_redirect("http://example.com"); - /// ``` - /// - /// # See also - /// - /// * [`crate::reverse_redirect!`] – a more ergonomic way to create - /// redirects to internal views - #[must_use] - fn new_redirect>(location: T) -> Self; -} - -impl private::Sealed for Response {} - -impl ResponseExt for Response { - fn builder() -> http::response::Builder { - http::Response::builder() - } - - fn new_redirect>(location: T) -> Self { - http::Response::builder() - .status(StatusCode::SEE_OTHER) - .header(http::header::LOCATION, location.into()) - .body(Body::empty()) - .expect(RESPONSE_BUILD_FAILURE) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::body::BodyInner; - use crate::headers::JSON_CONTENT_TYPE; - use crate::response::{Response, ResponseExt}; - - #[test] - #[cfg(feature = "json")] - fn response_new_json() { - #[derive(serde::Serialize)] - struct MyData { - hello: String, - } - - let data = MyData { - hello: String::from("world"), - }; - let response = crate::json::Json(data).into_response().unwrap(); - assert_eq!(response.status(), StatusCode::OK); - assert_eq!( - response.headers().get(http::header::CONTENT_TYPE).unwrap(), - JSON_CONTENT_TYPE - ); - match &response.body().inner { - BodyInner::Fixed(fixed) => { - assert_eq!(fixed, r#"{"hello":"world"}"#); - } - _ => { - panic!("Expected fixed body"); - } - } - } - - #[test] - fn response_new_redirect() { - let location = "http://example.com"; - let response = Response::new_redirect(location); - assert_eq!(response.status(), StatusCode::SEE_OTHER); - assert_eq!( - response.headers().get(http::header::LOCATION).unwrap(), - location - ); - } -} diff --git a/cot/src/response/into_response.rs b/cot/src/response/into_response.rs index 1f1d1f90..37a499ed 100644 --- a/cot/src/response/into_response.rs +++ b/cot/src/response/into_response.rs @@ -1,341 +1,6 @@ -use bytes::{Bytes, BytesMut}; -use cot::error::error_impl::impl_into_cot_error; -use cot::headers::{HTML_CONTENT_TYPE, OCTET_STREAM_CONTENT_TYPE, PLAIN_TEXT_CONTENT_TYPE}; -use cot::response::Response; -use cot::{Body, Error, StatusCode}; -use http; - -#[cfg(feature = "json")] use crate::headers::JSON_CONTENT_TYPE; -use crate::html::Html; - -/// Trait for generating responses. -/// Types that implement `IntoResponse` can be returned from handlers. -/// -/// # Implementing `IntoResponse` -/// -/// You generally shouldn't have to implement `IntoResponse` manually, as cot -/// provides implementations for many common types. -/// -/// However, it might be necessary if you have a custom error type that you want -/// to return from handlers. -pub trait IntoResponse { - /// Converts the implementing type into a `cot::Result`. - /// - /// # Errors - /// Returns an error if the conversion fails. - fn into_response(self) -> cot::Result; - - /// Modifies the response by appending the specified header. - /// - /// # Errors - /// Returns an error if the header name or value is invalid. - fn with_header(self, key: K, value: V) -> WithHeader - where - K: TryInto, - V: TryInto, - Self: Sized, - { - let key = key.try_into().ok(); - let value = value.try_into().ok(); - - WithHeader { - inner: self, - header: key.zip(value), - } - } - - /// Modifies the response by setting the `Content-Type` header. - /// - /// # Errors - /// Returns an error if the content type value is invalid. - fn with_content_type(self, content_type: V) -> WithContentType - where - V: TryInto, - Self: Sized, - { - WithContentType { - inner: self, - content_type: content_type.try_into().ok(), - } - } - - /// Modifies the response by setting the status code. - /// - /// # Errors - /// Returns an error if the `IntoResponse` conversion fails. - fn with_status(self, status: StatusCode) -> WithStatus - where - Self: Sized, - { - WithStatus { - inner: self, - status, - } - } - - /// Modifies the response by setting the body. - /// - /// # Errors - /// Returns an error if the `IntoResponse` conversion fails. - fn with_body(self, body: impl Into) -> WithBody - where - Self: Sized, - { - WithBody { - inner: self, - body: body.into(), - } - } - - /// Modifies the response by inserting an extension. - /// - /// # Errors - /// Returns an error if the `IntoResponse` conversion fails. - fn with_extension(self, extension: T) -> WithExtension - where - T: Clone + Send + Sync + 'static, - Self: Sized, - { - WithExtension { - inner: self, - extension, - } - } -} - -/// Returned by [`with_header`](IntoResponse::with_header) method. -#[derive(Debug)] -pub struct WithHeader { - inner: T, - header: Option<(http::HeaderName, http::HeaderValue)>, -} - -impl IntoResponse for WithHeader { - fn into_response(self) -> cot::Result { - self.inner.into_response().map(|mut resp| { - if let Some((key, value)) = self.header { - resp.headers_mut().append(key, value); - } - resp - }) - } -} - -/// Returned by [`with_content_type`](IntoResponse::with_content_type) method. -#[derive(Debug)] -pub struct WithContentType { - inner: T, - content_type: Option, -} - -impl IntoResponse for WithContentType { - fn into_response(self) -> cot::Result { - self.inner.into_response().map(|mut resp| { - if let Some(content_type) = self.content_type { - resp.headers_mut() - .insert(http::header::CONTENT_TYPE, content_type); - } - resp - }) - } -} - -/// Returned by [`with_status`](IntoResponse::with_status) method. -#[derive(Debug)] -pub struct WithStatus { - inner: T, - status: StatusCode, -} - -impl IntoResponse for WithStatus { - fn into_response(self) -> cot::Result { - self.inner.into_response().map(|mut resp| { - *resp.status_mut() = self.status; - resp - }) - } -} - -/// Returned by [`with_body`](IntoResponse::with_body) method. -#[derive(Debug)] -pub struct WithBody { - inner: T, - body: Body, -} - -impl IntoResponse for WithBody { - fn into_response(self) -> cot::Result { - self.inner.into_response().map(|mut resp| { - *resp.body_mut() = self.body; - resp - }) - } -} - -/// Returned by [`with_extension`](IntoResponse::with_extension) method. -#[derive(Debug)] -pub struct WithExtension { - inner: T, - extension: D, -} - -impl IntoResponse for WithExtension -where - T: IntoResponse, - D: Clone + Send + Sync + 'static, -{ - fn into_response(self) -> cot::Result { - self.inner.into_response().map(|mut resp| { - resp.extensions_mut().insert(self.extension); - resp - }) - } -} - -macro_rules! impl_into_response_for_type_and_mime { - ($ty:ty, $mime:expr) => { - impl IntoResponse for $ty { - fn into_response(self) -> cot::Result { - Body::from(self) - .with_header(http::header::CONTENT_TYPE, $mime) - .into_response() - } - } - }; -} - -// General implementations - -impl IntoResponse for () { - fn into_response(self) -> cot::Result { - Body::empty().into_response() - } -} - -impl IntoResponse for Result -where - R: IntoResponse, - E: Into, -{ - fn into_response(self) -> cot::Result { - match self { - Ok(value) => value.into_response(), - Err(err) => Err(err.into()), - } - } -} - -impl IntoResponse for Error { - fn into_response(self) -> cot::Result { - Err(self) - } -} - -impl IntoResponse for Response { - fn into_response(self) -> cot::Result { - Ok(self) - } -} - -// Text implementations - -impl_into_response_for_type_and_mime!(&'static str, PLAIN_TEXT_CONTENT_TYPE); -impl_into_response_for_type_and_mime!(String, PLAIN_TEXT_CONTENT_TYPE); - -impl IntoResponse for Box { - fn into_response(self) -> cot::Result { - String::from(self).into_response() - } -} - -// Bytes implementations - -impl_into_response_for_type_and_mime!(&'static [u8], OCTET_STREAM_CONTENT_TYPE); -impl_into_response_for_type_and_mime!(Vec, OCTET_STREAM_CONTENT_TYPE); -impl_into_response_for_type_and_mime!(Bytes, OCTET_STREAM_CONTENT_TYPE); - -impl IntoResponse for &'static [u8; N] { - fn into_response(self) -> cot::Result { - self.as_slice().into_response() - } -} - -impl IntoResponse for [u8; N] { - fn into_response(self) -> cot::Result { - self.to_vec().into_response() - } -} - -impl IntoResponse for Box<[u8]> { - fn into_response(self) -> cot::Result { - Vec::from(self).into_response() - } -} - -impl IntoResponse for BytesMut { - fn into_response(self) -> cot::Result { - self.freeze().into_response() - } -} - -// HTTP structures for common uses - -impl IntoResponse for StatusCode { - fn into_response(self) -> cot::Result { - ().into_response().with_status(self).into_response() - } -} - -impl IntoResponse for http::HeaderMap { - fn into_response(self) -> cot::Result { - ().into_response().map(|mut resp| { - *resp.headers_mut() = self; - resp - }) - } -} - -impl IntoResponse for http::Extensions { - fn into_response(self) -> cot::Result { - ().into_response().map(|mut resp| { - *resp.extensions_mut() = self; - resp - }) - } -} - -impl IntoResponse for crate::response::ResponseHead { - fn into_response(self) -> cot::Result { - Ok(Response::from_parts(self, Body::empty())) - } -} - -// Data type structures implementations - -impl IntoResponse for Html { - /// Create a new HTML response. - /// - /// This creates a new [`Response`] object with a content type of - /// `text/html; charset=utf-8` and given body. - /// - /// # Examples - /// - /// ``` - /// use cot::html::Html; - /// use cot::response::IntoResponse; - /// - /// let html = Html::new("
Hello
"); - /// - /// let response = html.into_response(); - /// ``` - fn into_response(self) -> cot::Result { - self.0 - .into_response() - .with_content_type(HTML_CONTENT_TYPE) - .into_response() - } -} +use crate::response::{IntoResponse, Response}; +use cot_core::impl_into_cot_error; #[cfg(feature = "json")] impl IntoResponse for cot::json::Json { @@ -350,14 +15,14 @@ impl IntoResponse for cot::json::Json { /// use std::collections::HashMap; /// /// use cot::json::Json; - /// use cot::response::IntoResponse; + /// use cot_core::response::IntoResponse; /// /// let data = HashMap::from([("hello", "world")]); /// let json = Json(data); /// /// let response = json.into_response(); /// ``` - fn into_response(self) -> cot::Result { + fn into_response(self) -> crate::Result { // a "reasonable default" for a JSON response size const DEFAULT_JSON_SIZE: usize = 128; @@ -377,396 +42,14 @@ struct JsonSerializeError(serde_path_to_error::Error); #[cfg(feature = "json")] impl_into_cot_error!(JsonSerializeError, INTERNAL_SERVER_ERROR); -// Shortcuts for common uses - -impl IntoResponse for Body { - fn into_response(self) -> cot::Result { - Ok(Response::new(self)) - } -} - #[cfg(test)] mod tests { - use bytes::{Bytes, BytesMut}; - use cot::response::Response; - use cot::{Body, StatusCode}; - use http::{self, HeaderMap, HeaderValue}; + use cot_core::StatusCode; use super::*; - use crate::error::NotFound; - use crate::html::Html; - - #[cot::test] - async fn test_unit_into_response() { - let response = ().into_response().unwrap(); - - assert_eq!(response.status(), StatusCode::OK); - assert!(response.headers().is_empty()); - assert_eq!(response.into_body().into_bytes().await.unwrap().len(), 0); - } - - #[cot::test] - async fn test_result_ok_into_response() { - let res: Result<&'static str, Error> = Ok("hello"); - - let response = res.into_response().unwrap(); - - assert_eq!(response.status(), StatusCode::OK); - assert_eq!( - response.headers().get(http::header::CONTENT_TYPE).unwrap(), - "text/plain; charset=utf-8" - ); - assert_eq!(response.into_body().into_bytes().await.unwrap(), "hello"); - } - - #[cot::test] - async fn test_result_err_into_response() { - let err = Error::from(NotFound::with_message("test")); - let res: Result<&'static str, Error> = Err(err); - - let error_result = res.into_response(); - - assert!(error_result.is_err()); - assert!(error_result.err().unwrap().to_string().contains("test")); - } - - #[cot::test] - async fn test_response_into_response() { - let original_response = Response::new(Body::from("test")); - - let response = original_response.into_response().unwrap(); - - assert_eq!(response.status(), StatusCode::OK); - assert_eq!(response.into_body().into_bytes().await.unwrap(), "test"); - } - - #[cot::test] - async fn test_static_str_into_response() { - let response = "hello world".into_response().unwrap(); - - assert_eq!(response.status(), StatusCode::OK); - assert_eq!( - response.headers().get(http::header::CONTENT_TYPE).unwrap(), - "text/plain; charset=utf-8" - ); - assert_eq!( - response.into_body().into_bytes().await.unwrap(), - "hello world" - ); - } - - #[cot::test] - async fn test_string_into_response() { - let s = String::from("hello string"); - - let response = s.into_response().unwrap(); - - assert_eq!(response.status(), StatusCode::OK); - assert_eq!( - response.headers().get(http::header::CONTENT_TYPE).unwrap(), - "text/plain; charset=utf-8" - ); - assert_eq!( - response.into_body().into_bytes().await.unwrap(), - "hello string" - ); - } - - #[cot::test] - async fn test_box_str_into_response() { - let b: Box = "hello box".into(); - - let response = b.into_response().unwrap(); - - assert_eq!(response.status(), StatusCode::OK); - assert_eq!( - response.headers().get(http::header::CONTENT_TYPE).unwrap(), - "text/plain; charset=utf-8" - ); - assert_eq!( - response.into_body().into_bytes().await.unwrap(), - "hello box" - ); - } - - #[cot::test] - async fn test_static_u8_slice_into_response() { - let data: &'static [u8] = b"hello bytes"; - - let response = data.into_response().unwrap(); - - assert_eq!(response.status(), StatusCode::OK); - assert_eq!( - response.headers().get(http::header::CONTENT_TYPE).unwrap(), - "application/octet-stream" - ); - assert_eq!( - response.into_body().into_bytes().await.unwrap(), - "hello bytes" - ); - } - - #[cot::test] - async fn test_vec_u8_into_response() { - let data: Vec = vec![1, 2, 3]; - - let response = data.into_response().unwrap(); - - assert_eq!(response.status(), StatusCode::OK); - assert_eq!( - response.headers().get(http::header::CONTENT_TYPE).unwrap(), - "application/octet-stream" - ); - assert_eq!( - response.into_body().into_bytes().await.unwrap(), - Bytes::from(vec![1, 2, 3]) - ); - } - - #[cot::test] - async fn test_bytes_into_response() { - let data = Bytes::from_static(b"hello bytes obj"); - - let response = data.into_response().unwrap(); - - assert_eq!(response.status(), StatusCode::OK); - assert_eq!( - response.headers().get(http::header::CONTENT_TYPE).unwrap(), - "application/octet-stream" - ); - assert_eq!( - response.into_body().into_bytes().await.unwrap(), - "hello bytes obj" - ); - } - - #[cot::test] - async fn test_static_u8_array_into_response() { - let data: &'static [u8; 5] = b"array"; - - let response = data.into_response().unwrap(); - - assert_eq!(response.status(), StatusCode::OK); - assert_eq!( - response.headers().get(http::header::CONTENT_TYPE).unwrap(), - "application/octet-stream" - ); - assert_eq!(response.into_body().into_bytes().await.unwrap(), "array"); - } - - #[cot::test] - async fn test_u8_array_into_response() { - let data: [u8; 3] = [4, 5, 6]; - - let response = data.into_response().unwrap(); - - assert_eq!(response.status(), StatusCode::OK); - assert_eq!( - response.headers().get(http::header::CONTENT_TYPE).unwrap(), - "application/octet-stream" - ); - assert_eq!( - response.into_body().into_bytes().await.unwrap(), - Bytes::from(vec![4, 5, 6]) - ); - } - - #[cot::test] - async fn test_box_u8_slice_into_response() { - let data: Box<[u8]> = Box::new([7, 8, 9]); - - let response = data.into_response().unwrap(); - - assert_eq!(response.status(), StatusCode::OK); - assert_eq!( - response.headers().get(http::header::CONTENT_TYPE).unwrap(), - "application/octet-stream" - ); - assert_eq!( - response.into_body().into_bytes().await.unwrap(), - Bytes::from(vec![7, 8, 9]) - ); - } - - #[cot::test] - async fn test_bytes_mut_into_response() { - let mut data = BytesMut::with_capacity(10); - data.extend_from_slice(b"mutable"); - - let response = data.into_response().unwrap(); - - assert_eq!(response.status(), StatusCode::OK); - assert_eq!( - response.headers().get(http::header::CONTENT_TYPE).unwrap(), - "application/octet-stream" - ); - assert_eq!(response.into_body().into_bytes().await.unwrap(), "mutable"); - } - - #[cot::test] - async fn test_status_code_into_response() { - let response = StatusCode::NOT_FOUND.into_response().unwrap(); - - assert_eq!(response.status(), StatusCode::NOT_FOUND); - assert!(response.headers().is_empty()); - assert_eq!(response.into_body().into_bytes().await.unwrap().len(), 0); - } - - #[cot::test] - async fn test_header_map_into_response() { - let mut headers = HeaderMap::new(); - headers.insert("X-Test", HeaderValue::from_static("value")); - - let response = headers.into_response().unwrap(); - - assert_eq!(response.status(), StatusCode::OK); - assert_eq!(response.headers().get("X-Test").unwrap(), "value"); - assert_eq!(response.into_body().into_bytes().await.unwrap().len(), 0); - } - - #[cot::test] - async fn test_extensions_into_response() { - let mut extensions = http::Extensions::new(); - extensions.insert("My Extension Data"); - - let response = extensions.into_response().unwrap(); - - assert_eq!(response.status(), StatusCode::OK); - assert!(response.headers().is_empty()); - assert_eq!( - response.extensions().get::<&str>(), - Some(&"My Extension Data") - ); - assert_eq!(response.into_body().into_bytes().await.unwrap().len(), 0); - } - - #[cot::test] - async fn test_parts_into_response() { - let mut response = Response::new(Body::empty()); - *response.status_mut() = StatusCode::ACCEPTED; - response - .headers_mut() - .insert("X-From-Parts", HeaderValue::from_static("yes")); - response.extensions_mut().insert(123usize); - let (head, _) = response.into_parts(); - - let new_response = head.into_response().unwrap(); - - assert_eq!(new_response.status(), StatusCode::ACCEPTED); - assert_eq!(new_response.headers().get("X-From-Parts").unwrap(), "yes"); - assert_eq!(new_response.extensions().get::(), Some(&123)); - assert_eq!( - new_response.into_body().into_bytes().await.unwrap().len(), - 0 - ); - } - - #[cot::test] - async fn test_html_into_response() { - let html = Html::new("

Test

"); - - let response = html.into_response().unwrap(); - - assert_eq!(response.status(), StatusCode::OK); - assert_eq!( - response.headers().get(http::header::CONTENT_TYPE).unwrap(), - "text/html; charset=utf-8" - ); - assert_eq!( - response.into_body().into_bytes().await.unwrap(), - "

Test

" - ); - } - - #[cot::test] - async fn test_body_into_response() { - let body = Body::from("body test"); - - let response = body.into_response().unwrap(); - - assert_eq!(response.status(), StatusCode::OK); - assert_eq!( - response.headers().get(http::header::CONTENT_TYPE), - None // Body itself doesn't set content-type - ); - assert_eq!( - response.into_body().into_bytes().await.unwrap(), - "body test" - ); - } - - #[cot::test] - async fn test_with_header() { - let response = "test" - .with_header("X-Custom", "HeaderValue") - .into_response() - .unwrap(); - assert_eq!(response.status(), StatusCode::OK); - assert_eq!(response.headers().get("X-Custom").unwrap(), "HeaderValue"); - assert_eq!(response.into_body().into_bytes().await.unwrap(), "test"); - } - - #[cot::test] - async fn test_with_content_type() { - let response = "test" - .with_content_type("application/json") - .into_response() - .unwrap(); - - assert_eq!(response.status(), StatusCode::OK); - assert_eq!( - response.headers().get(http::header::CONTENT_TYPE).unwrap(), - "application/json" - ); - assert_eq!(response.into_body().into_bytes().await.unwrap(), "test"); - } - - #[cot::test] - async fn test_with_status() { - let response = "test" - .with_status(StatusCode::CREATED) - .into_response() - .unwrap(); - - assert_eq!(response.status(), StatusCode::CREATED); - assert_eq!( - response.headers().get(http::header::CONTENT_TYPE).unwrap(), - "text/plain; charset=utf-8" - ); - assert_eq!(response.into_body().into_bytes().await.unwrap(), "test"); - } - - #[cot::test] - async fn test_with_body() { - let response = StatusCode::ACCEPTED - .with_body("new body") - .into_response() - .unwrap(); - - assert_eq!(response.status(), StatusCode::ACCEPTED); - assert_eq!(response.into_body().into_bytes().await.unwrap(), "new body"); - } - - #[cot::test] - async fn test_with_extension() { - #[derive(Clone, Debug, PartialEq)] - struct MyExt(String); - - let response = "test" - .with_extension(MyExt("data".to_string())) - .into_response() - .unwrap(); - - assert_eq!(response.status(), StatusCode::OK); - assert_eq!( - response.extensions().get::(), - Some(&MyExt("data".to_string())) - ); - assert_eq!(response.into_body().into_bytes().await.unwrap(), "test"); - } #[cfg(feature = "json")] - #[cot::test] + #[cot_macros::test] async fn test_json_struct_into_response() { use serde::Serialize; @@ -796,7 +79,7 @@ mod tests { } #[cfg(feature = "json")] - #[cot::test] + #[cot_macros::test] async fn test_json_hashmap_into_response() { use std::collections::HashMap; diff --git a/cot/src/router.rs b/cot/src/router.rs index c6fd91be..2f900df9 100644 --- a/cot/src/router.rs +++ b/cot/src/router.rs @@ -1,788 +1,11 @@ -//! Router for passing requests to their respective views. -//! -//! # Examples -//! -//! ``` -//! use cot::request::Request; -//! use cot::response::Response; -//! use cot::router::{Route, Router}; -//! -//! async fn home(request: Request) -> cot::Result { -//! Ok(cot::reverse_redirect!(request, "get_page", page = 123)?) -//! } -//! -//! async fn get_page(request: Request) -> cot::Result { -//! unimplemented!() -//! } -//! -//! let router = Router::with_urls([Route::with_handler_and_name( -//! "/{page}", get_page, "get_page", -//! )]); -//! ``` - -use std::collections::HashMap; -use std::fmt::Formatter; -use std::future::Future; -use std::pin::Pin; use std::sync::Arc; -use std::task::{Context, Poll}; - -use derive_more::with_trait::Debug; -use tracing::debug; - -use crate::error::NotFound; -use crate::error::error_impl::impl_into_cot_error; -use crate::handler::{BoxRequestHandler, RequestHandler, into_box_request_handler}; -use crate::request::{AppName, PathParams, Request, RequestExt, RequestHead, RouteName}; -use crate::response::Response; -use crate::router::path::{CaptureResult, PathMatcher, ReverseParamMap}; -use crate::{Error, Result}; - -pub mod method; -pub mod path; - -/// A router that can be used to route requests to their respective views. -/// -/// This struct is used to route requests to their respective views. It can be -/// created directly by calling the [`Router::with_urls`] method, and that's -/// what is typically done in [`cot::App::router`] implementations. -/// -/// # Examples -/// -/// ``` -/// use cot::request::Request; -/// use cot::response::Response; -/// use cot::router::{Route, Router}; -/// -/// async fn home(request: Request) -> cot::Result { -/// unimplemented!() -/// } -/// -/// let router = Router::with_urls([Route::with_handler_and_name("/", home, "home")]); -/// ``` -#[derive(Clone, Debug)] -pub struct Router { - app_name: Option, - urls: Vec, - names: HashMap>, -} - -impl Router { - /// Create an empty router. - /// - /// This router will not route any requests. - /// - /// # Examples - /// - /// ``` - /// use cot::router::Router; - /// - /// let router = Router::empty(); - /// ``` - #[must_use] - pub fn empty() -> Self { - Self::with_urls(&[]) - } - - /// Create a router with the given routes. - /// - /// # Examples - /// - /// ``` - /// use cot::request::Request; - /// use cot::response::Response; - /// use cot::router::{Route, Router}; - /// - /// async fn home(request: Request) -> cot::Result { - /// unimplemented!() - /// } - /// - /// let router = Router::with_urls([Route::with_handler_and_name("/", home, "home")]); - /// ``` - #[must_use] - pub fn with_urls>>(urls: T) -> Self { - let urls = urls.into(); - let mut names = HashMap::new(); - - for url in &urls { - if let Some(name) = &url.name { - names.insert(name.clone(), url.url.clone()); - } - } - - Self { - app_name: None, - urls, - names, - } - } - - pub(crate) fn set_app_name(&mut self, app_name: AppName) { - self.app_name = Some(app_name); - } - - async fn route(&self, mut request: Request, request_path: &str) -> Result { - debug!("Routing request to {}", request_path); - - if let Some(result) = self.get_handler(request_path) { - let mut path_params = PathParams::new(); - for (key, value) in result.params.iter().rev() { - path_params.insert(key.clone(), value.clone()); - } - request.extensions_mut().insert(path_params); - if let Some(app_name) = result.app_name { - request.extensions_mut().insert(app_name); - } - if let Some(name) = result.name { - request.extensions_mut().insert(name); - } - result.handler.handle(request).await - } else { - debug!("Not found: {}", request_path); - Err(Error::from(NotFound::router())) - } - } - - fn get_handler(&self, request_path: &str) -> Option> { - for route in &self.urls { - if let Some(matches) = route.url.capture(request_path) { - let matches_fully = matches.matches_fully(); - - match &route.view { - RouteInner::Handler(handler) => { - if matches_fully { - return Some(HandlerFound { - handler: &**handler, - app_name: self.app_name.clone(), - name: route.name.clone(), - params: Self::matches_to_path_params(&matches, Vec::new()), - }); - } - } - RouteInner::Router(router) => { - if let Some(result) = router.get_handler(matches.remaining_path) { - return Some(HandlerFound { - handler: result.handler, - app_name: result.app_name.or_else(|| self.app_name.clone()), - name: result.name, - params: Self::matches_to_path_params(&matches, result.params), - }); - } - } - #[cfg(feature = "openapi")] - RouteInner::ApiHandler(handler) => { - if matches_fully { - return Some(HandlerFound { - // TODO: consider removing this when Rust trait_upcasting is - // stabilized and we bump the MSRV (lands in Rust 1.86) - handler: handler.as_box_request_handler(), - app_name: self.app_name.clone(), - name: route.name.clone(), - params: Self::matches_to_path_params(&matches, Vec::new()), - }); - } - } - } - } - } - - None - } - - fn matches_to_path_params( - matches: &CaptureResult<'_, '_>, - mut path_params: Vec<(String, String)>, - ) -> Vec<(String, String)> { - // Adding in reverse order, since we're doing this from the bottom up (we're - // going to reverse the order before running the handler) - for param in matches.params.iter().rev() { - path_params.push((param.name.to_owned(), param.value.clone())); - } - path_params - } - - /// Handle a request. - /// - /// This method is called by the [`CotApp`](crate::App) to handle - /// a request. - /// - /// # Errors - /// - /// This method re-throws any errors that occur in the request handler. - pub async fn handle(&self, request: Request) -> Result { - let path = request.uri().path().to_owned(); - self.route(request, &path).await - } - - /// Get a URL for a view by name. - /// - /// Instead of using this method directly, consider using the - /// [`reverse!`](crate::reverse) macro which provides much more ergonomic - /// way to call this. - /// - /// `app_name` is the name of the app that the view should be found in. If - /// `app_name` is `None`, the view will be searched for in any app. - /// - /// # Errors - /// - /// This method returns an error if the view name is not found. - /// - /// This method returns an error if the URL cannot be generated because of - /// missing parameters. - pub fn reverse( - &self, - app_name: Option<&str>, - name: &str, - params: &ReverseParamMap, - ) -> Result { - Ok(self - .reverse_option(app_name, name, params)? - .ok_or_else(|| NoViewToReverse { - app_name: app_name.map(ToOwned::to_owned), - view_name: name.to_owned(), - })?) - } - - /// Get a URL for a view by name. - /// - /// `app_name` is the name of the app that the view should be found in. If - /// `app_name` is `None`, the view will be searched for in any app. - /// - /// Returns `None` if the view name is not found. - /// - /// # Errors - /// - /// This method returns an error if the URL cannot be generated because of - /// missing parameters. - pub fn reverse_option( - &self, - app_name: Option<&str>, - name: &str, - params: &ReverseParamMap, - ) -> Result> { - if app_name.is_none() - || self.app_name.is_none() - || app_name == self.app_name.as_ref().map(|name| name.0.as_str()) - { - self.reverse_option_impl(app_name, name, params) - } else { - Ok(None) - } - } - - fn reverse_option_impl( - &self, - app_name: Option<&str>, - name: &str, - params: &ReverseParamMap, - ) -> Result> { - let url = self - .names - .get(&RouteName(String::from(name))) - .map(|matcher| matcher.reverse(params)); - if let Some(url) = url { - return Ok(Some(url?)); - } - - for route in &self.urls { - if let RouteInner::Router(router) = &route.view { - if let Some(url) = router.reverse_option(app_name, name, params)? { - return Ok(Some(route.url.reverse(params)? + &url)); - } - } - } - Ok(None) - } - - /// Get the routes in this router. - /// - /// # Examples - /// - /// ``` - /// use cot::request::Request; - /// use cot::response::Response; - /// use cot::router::{Route, Router}; - /// - /// async fn home(request: Request) -> cot::Result { - /// unimplemented!() - /// } - /// - /// let router = Router::with_urls([Route::with_handler_and_name("/", home, "home")]); - /// assert_eq!(router.routes().len(), 1); - /// ``` - #[must_use] - pub fn routes(&self) -> &[Route] { - &self.urls - } - - /// Check if this router is empty. - /// - /// # Examples - /// - /// ``` - /// use cot::request::Request; - /// use cot::response::Response; - /// use cot::router::{Route, Router}; - /// - /// async fn home(request: Request) -> cot::Result { - /// unimplemented!() - /// } - /// - /// let router = Router::empty(); - /// assert!(router.is_empty()); - /// - /// let router = Router::with_urls([Route::with_handler_and_name("/", home, "home")]); - /// assert!(!router.is_empty()); - /// ``` - #[must_use] - pub fn is_empty(&self) -> bool { - self.urls.is_empty() - } - - /// Returns the OpenAPI paths for the router. - /// - /// This might be useful if you want to manually serve the generated OpenAPI - /// specs. - /// - /// # Panics - /// - /// Panics if invalid schemas are generated. This should not happen in - /// normal operation, but if it does, it indicates a bug in the - /// [`schemars`](https://docs.rs/schemars/latest/schemars/) library - /// or in the way the OpenAPI specs are generated. - #[cfg(feature = "openapi")] - #[must_use] - pub fn as_api(&self) -> aide::openapi::OpenApi { - let mut paths = aide::openapi::Paths::default(); - let mut schema_generator = - schemars::SchemaGenerator::new(schemars::generate::SchemaSettings::openapi3()); - - self.as_openapi_impl("", &[], &mut paths, &mut schema_generator); - - let component_schemas = schema_generator - .take_definitions(true) - .into_iter() - .map(|(name, json_schema)| { - ( - name, - aide::openapi::SchemaObject { - json_schema: schemars::Schema::try_from(json_schema).expect( - "SchemaGenerator::take_definitions should return valid schemas", - ), - example: None, - external_docs: None, - }, - ) - }) - .collect(); - aide::openapi::OpenApi { - paths: Some(paths), - components: Some(aide::openapi::Components { - schemas: component_schemas, - ..Default::default() - }), - ..Default::default() - } - } - - #[cfg(feature = "openapi")] - fn as_openapi_impl( - &self, - url: &str, - param_names: &[&str], - paths: &mut aide::openapi::Paths, - schema_generator: &mut schemars::SchemaGenerator, - ) { - for route in &self.urls { - Self::route_as_openapi(route, param_names, paths, schema_generator, url); - } - } - - #[cfg(feature = "openapi")] - fn route_as_openapi( - route: &Route, - param_names: &[&str], - paths: &mut aide::openapi::Paths, - schema_generator: &mut schemars::SchemaGenerator, - url: &str, - ) { - match &route.view { - RouteInner::Router(router) => { - let mut params = Vec::from(param_names); - params.extend(route.url.param_names()); - - let url = format!("{url}{}", route.url); - - router.as_openapi_impl(&url, ¶ms, paths, schema_generator); - } - RouteInner::ApiHandler(handler) => { - let mut params = Vec::from(param_names); - params.extend(route.url.param_names()); - - let url = format!("{url}{}", route.url); - - let mut route_context = crate::openapi::RouteContext::new(); - route_context.param_names = ¶ms; - - paths.paths.insert( - url, - aide::openapi::ReferenceOr::Item( - handler.as_api_route(&route_context, schema_generator), - ), - ); - } - RouteInner::Handler(_) => {} - } - } -} - -impl Default for Router { - fn default() -> Self { - Self::empty() - } -} - -#[derive(Debug, thiserror::Error)] -#[error("failed to reverse route `{view_name}` due to view not existing")] -struct NoViewToReverse { - app_name: Option, - view_name: String, -} -impl_into_cot_error!(NoViewToReverse); - -#[derive(Debug)] -struct HandlerFound<'a> { - #[debug("handler(...)")] - handler: &'a (dyn BoxRequestHandler + Send + Sync), - app_name: Option, - name: Option, - params: Vec<(String, String)>, -} - -/// A service that routes requests to their respective views. -/// -/// This is mostly an internal service used by the [`CotApp`](crate::App) to -/// route requests to their respective views with an interface that is -/// compatible with the [`tower::Service`] trait. -#[derive(Debug, Clone)] -pub struct RouterService { - router: Arc, -} - -impl RouterService { - /// Create a new router service. - /// - /// # Examples - /// - /// ``` - /// use std::sync::Arc; - /// - /// use cot::request::Request; - /// use cot::response::Response; - /// use cot::router::{Route, Router, RouterService}; - /// - /// async fn home(request: Request) -> cot::Result { - /// unimplemented!() - /// } - /// - /// let router = Router::with_urls([Route::with_handler_and_name("/", home, "home")]); - /// let service = RouterService::new(Arc::new(router)); - /// ``` - #[must_use] - pub fn new(router: Arc) -> Self { - Self { router } - } -} - -impl tower::Service for RouterService { - type Error = Error; - type Future = Pin> + Send>>; - type Response = Response; - - fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - - fn call(&mut self, req: Request) -> Self::Future { - let router = self.router.clone(); - Box::pin(async move { router.handle(req).await }) - } -} - -// used in the reverse! macro; not part of public API -#[doc(hidden)] -#[must_use] -pub fn split_view_name(view_name: &str) -> (Option<&str>, &str) { - let colon_pos = view_name.find(':'); - if let Some(colon_pos) = colon_pos { - let app_name = &view_name[..colon_pos]; - let view_name = &view_name[colon_pos + 1..]; - (Some(app_name), view_name) - } else { - (None, view_name) - } -} - -/// A route that can be used to route requests to their respective views. -/// -/// # Examples -/// -/// ``` -/// use cot::request::Request; -/// use cot::response::Response; -/// use cot::router::{Route, Router}; -/// -/// async fn home(request: Request) -> cot::Result { -/// unimplemented!() -/// } -/// -/// let router = Router::with_urls([Route::with_handler_and_name("/", home, "home")]); -/// ``` -#[derive(Debug, Clone)] -pub struct Route { - url: Arc, - view: RouteInner, - name: Option, -} - -impl Route { - /// Create a new route with the given handler. - /// - /// # Examples - /// - /// ``` - /// use cot::request::Request; - /// use cot::response::Response; - /// use cot::router::{Route, Router}; - /// - /// async fn home(request: Request) -> cot::Result { - /// // ... - /// # unimplemented!() - /// } - /// - /// let route = Route::with_handler("/", home); - /// ``` - #[must_use] - pub fn with_handler(url: &str, handler: H) -> Self - where - HandlerParams: 'static, - H: RequestHandler + Send + Sync + 'static, - { - Self { - url: Arc::new(PathMatcher::new(url)), - view: RouteInner::Handler(Arc::new(into_box_request_handler(handler))), - name: None, - } - } - - /// Create a new route with the given handler for inclusion in the OpenAPI - /// specs. - /// - /// See [`crate::openapi`] module documentation for more details on how to - /// generate OpenAPI specs automatically. - /// - /// # Examples - /// - /// ``` - /// use cot::request::Request; - /// use cot::response::Response; - /// use cot::router::method::openapi::api_get; - /// use cot::router::{Route, Router}; - /// - /// async fn home(request: Request) -> cot::Result { - /// // ... - /// # unimplemented!() - /// } - /// - /// let route = Route::with_api_handler("/", api_get(home)); - /// ``` - #[must_use] - #[cfg(feature = "openapi")] - pub fn with_api_handler(url: &str, handler: H) -> Self - where - HandlerParams: 'static, - H: RequestHandler + crate::openapi::AsApiRoute + Send + Sync + 'static, - { - Self { - url: Arc::new(PathMatcher::new(url)), - view: RouteInner::ApiHandler(Arc::new( - crate::openapi::into_box_api_endpoint_request_handler(handler), - )), - name: None, - } - } - - /// Create a new route with the given handler and name. - /// - /// # Examples - /// - /// ``` - /// use cot::request::Request; - /// use cot::response::Response; - /// use cot::router::method::openapi::api_get; - /// use cot::router::{Route, Router}; - /// - /// async fn home(request: Request) -> cot::Result { - /// // ... - /// # unimplemented!() - /// } - /// - /// let route = Route::with_handler_and_name("/", api_get(home), "home"); - /// ``` - #[must_use] - pub fn with_handler_and_name(url: &str, handler: H, name: N) -> Self - where - N: Into, - HandlerParams: 'static, - H: RequestHandler + Send + Sync + 'static, - { - Self { - url: Arc::new(PathMatcher::new(url)), - view: RouteInner::Handler(Arc::new(into_box_request_handler(handler))), - name: Some(RouteName(name.into())), - } - } - - /// Create a new route with the given handler and name for inclusion in the - /// OpenAPI specs. - /// - /// See [`crate::openapi`] module documentation for more details on how to - /// generate OpenAPI specs automatically. - /// - /// # Examples - /// - /// ``` - /// use cot::request::Request; - /// use cot::response::Response; - /// use cot::router::method::openapi::api_post; - /// use cot::router::{Route, Router}; - /// - /// async fn home(request: Request) -> cot::Result { - /// // ... - /// # unimplemented!() - /// } - /// - /// let route = Route::with_api_handler_and_name("/", api_post(home), "home"); - /// ``` - #[must_use] - #[cfg(feature = "openapi")] - pub fn with_api_handler_and_name(url: &str, handler: H, name: N) -> Self - where - N: Into, - HandlerParams: 'static, - H: RequestHandler + crate::openapi::AsApiRoute + Send + Sync + 'static, - { - Self { - url: Arc::new(PathMatcher::new(url)), - view: RouteInner::ApiHandler(Arc::new( - crate::openapi::into_box_api_endpoint_request_handler(handler), - )), - name: Some(RouteName(name.into())), - } - } - - /// Create a new route with the given router. - /// - /// # Examples - /// - /// ``` - /// use cot::request::Request; - /// use cot::response::Response; - /// use cot::router::{Route, Router}; - /// - /// async fn home(request: Request) -> cot::Result { - /// unimplemented!() - /// } - /// - /// let router = Router::with_urls([Route::with_handler_and_name("/", home, "home")]); - /// let route = Route::with_router("/", router); - /// ``` - #[must_use] - pub fn with_router(url: &str, router: Router) -> Self { - Self { - url: Arc::new(PathMatcher::new(url)), - view: RouteInner::Router(router), - name: None, - } - } - - /// Get the URL for this route. - /// - /// # Examples - /// - /// ``` - /// use cot::request::Request; - /// use cot::response::Response; - /// use cot::router::{Route, Router}; - /// - /// async fn home(request: Request) -> cot::Result { - /// unimplemented!() - /// } - /// - /// let route = Route::with_handler("/test", home); - /// assert_eq!(route.url(), "/test"); - /// ``` - #[must_use] - pub fn url(&self) -> String { - self.url.to_string() - } - - /// Get the name of this route, if it was created with the - /// [`Self::with_handler_and_name`] function. - /// - /// # Examples - /// - /// ``` - /// use cot::request::Request; - /// use cot::response::Response; - /// use cot::router::{Route, Router}; - /// - /// async fn home(request: Request) -> cot::Result { - /// unimplemented!() - /// } - /// - /// let route = Route::with_handler_and_name("/", home, "home"); - /// assert_eq!(route.name(), Some("home")); - /// ``` - #[must_use] - pub fn name(&self) -> Option<&str> { - self.name.as_ref().map(|name| name.0.as_str()) - } - - #[must_use] - pub(crate) fn kind(&self) -> RouteKind { - match &self.view { - RouteInner::Handler(_) => RouteKind::Handler, - RouteInner::Router(_) => RouteKind::Router, - #[cfg(feature = "openapi")] - RouteInner::ApiHandler(_) => RouteKind::Handler, - } - } - - #[must_use] - pub(crate) fn router(&self) -> Option<&Router> { - match &self.view { - RouteInner::Router(router) => Some(router), - RouteInner::Handler(_) => None, - #[cfg(feature = "openapi")] - RouteInner::ApiHandler(_) => None, - } - } -} -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub(crate) enum RouteKind { - Handler, - Router, -} +use cot_core::request::{Request, RequestHead}; +pub use cot_core::reverse_param_map; +#[doc(inline)] +pub use cot_core::router::{Route, Router, RouterService, method}; -#[derive(Clone)] -enum RouteInner { - Handler(Arc), - Router(Router), - #[cfg(feature = "openapi")] - ApiHandler(Arc), -} +use crate::request::RequestExt; /// Get a URL for a view by its registered name and given params. /// @@ -794,7 +17,7 @@ enum RouteInner { /// /// # Return value /// -/// Returns a [`cot::Result`] that contains the URL for the view. You +/// Returns a [`crate::Result`] that contains the URL for the view. You /// will typically want to append `?` to the macro call to get the URL. /// /// # Examples @@ -802,9 +25,9 @@ enum RouteInner { /// ``` /// use cot::html::Html; /// use cot::project::RegisterAppsContext; -/// use cot::request::Request; -/// use cot::router::{Route, Router}; /// use cot::{App, AppBuilder, Project, StatusCode, reverse}; +/// use cot_core::request::Request; +/// use cot_core::router::{Route, Router}; /// /// async fn home(request: Request) -> cot::Result { /// // any of below two lines returns the same: @@ -852,10 +75,61 @@ macro_rules! reverse { let app_name = app_name.or_else(|| $request.app_name()); $request .router() - .reverse(app_name, view_name, &$crate::reverse_param_map!($( $($key = $value),* )?)) + .reverse(app_name, view_name, &$crate::router::reverse_param_map!($( $($key = $value),* )?)) }}; } +// used in the reverse! macro; not part of public API +#[doc(hidden)] +#[must_use] +pub fn split_view_name(view_name: &str) -> (Option<&str>, &str) { + let colon_pos = view_name.find(':'); + if let Some(colon_pos) = colon_pos { + let app_name = &view_name[..colon_pos]; + let view_name = &view_name[colon_pos + 1..]; + (Some(app_name), view_name) + } else { + (None, view_name) + } +} + +/// Get a URL for a view by its registered name and given params and return a +/// response with a redirect. +/// +/// This macro is a shorthand for creating a response with a redirect to a URL +/// generated by the [`reverse!`] macro. +/// +/// # Return value +/// +/// Returns a [`crate::Result`] that contains the URL for +/// the view. You will typically want to append `?` to the macro call to get the +/// [`Response`] object. +/// +/// # Examples +/// +/// ``` +/// use cot::reverse_redirect; +/// use cot_core::request::Request; +/// use cot_core::response::Response; +/// use cot_core::router::{Route, Router}; +/// +/// async fn infinite_loop(request: Request) -> cot::Result { +/// Ok(reverse_redirect!(request, "home")?) +/// } +/// +/// let router = Router::with_urls([Route::with_handler_and_name("/", infinite_loop, "home")]); +/// ``` +#[macro_export] +macro_rules! reverse_redirect { + ($request:expr, $view_name:literal $(, $($key:ident = $value:expr),*)?) => { + $crate::reverse!( + $request, + $view_name, + $( $($key = $value),* )? + ).map(|url| <$crate::response::Response as $crate::response::ResponseExt>::new_redirect(url)) + }; +} + /// A helper structure to allow reversing URLs from a request handler. /// /// This is mainly useful as an extractor to allow reversing URLs without @@ -865,9 +139,9 @@ macro_rules! reverse { /// /// ``` /// use cot::html::Html; -/// use cot::router::{Route, Router, Urls}; /// use cot::test::TestRequestBuilder; /// use cot::{RequestHandler, reverse}; +/// use cot_core::router::{Route, Router, Urls}; /// /// async fn my_handler(urls: Urls) -> cot::Result { /// let url = reverse!(urls, "home")?; @@ -904,10 +178,10 @@ impl Urls { /// /// ``` /// use cot::html::Html; - /// use cot::request::Request; - /// use cot::response::{Response, ResponseExt}; - /// use cot::router::Urls; /// use cot::{Body, StatusCode, reverse}; + /// use cot_core::request::Request; + /// use cot_core::response::{Response, ResponseExt}; + /// use cot_core::router::Urls; /// /// async fn my_handler(request: Request) -> cot::Result { /// let urls = Urls::from_request(&request); @@ -925,7 +199,7 @@ impl Urls { } } - pub(crate) fn from_parts(request_head: &RequestHead) -> Self { + pub fn from_parts(request_head: &RequestHead) -> Self { Self { app_name: request_head.app_name().map(ToOwned::to_owned), router: Arc::clone(request_head.router()), @@ -941,9 +215,9 @@ impl Urls { /// # Examples /// /// ``` - /// use cot::request::{Request, RequestExt}; - /// use cot::response::Response; - /// use cot::router::Urls; + /// use cot_core::request::{Request, RequestExt}; + /// use cot_core::response::Response; + /// use cot_core::router::Urls; /// /// async fn my_handler(urls: Urls) -> cot::Result { /// let app_name = urls.app_name(); @@ -961,9 +235,9 @@ impl Urls { /// # Examples /// /// ``` - /// use cot::request::{Request, RequestExt}; - /// use cot::response::Response; - /// use cot::router::Urls; + /// use cot_core::request::{Request, RequestExt}; + /// use cot_core::response::Response; + /// use cot_core::router::Urls; /// /// async fn my_handler(urls: Urls) -> cot::Result { /// let router = urls.router(); @@ -976,295 +250,3 @@ impl Urls { &self.router } } - -impl Debug for RouteInner { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match &self { - RouteInner::Handler(_) => f.debug_tuple("Handler").field(&"handler(...)").finish(), - RouteInner::Router(router) => f.debug_tuple("Router").field(router).finish(), - #[cfg(feature = "openapi")] - RouteInner::ApiHandler(_) => { - f.debug_tuple("ApiHandler").field(&"handler(...)").finish() - } - } - } -} - -/// Get a URL for a view by its registered name and given params and return a -/// response with a redirect. -/// -/// This macro is a shorthand for creating a response with a redirect to a URL -/// generated by the [`reverse!`] macro. -/// -/// # Return value -/// -/// Returns a [`cot::Result`] that contains the URL for -/// the view. You will typically want to append `?` to the macro call to get the -/// [`Response`] object. -/// -/// # Examples -/// -/// ``` -/// use cot::request::Request; -/// use cot::response::Response; -/// use cot::reverse_redirect; -/// use cot::router::{Route, Router}; -/// -/// async fn infinite_loop(request: Request) -> cot::Result { -/// Ok(reverse_redirect!(request, "home")?) -/// } -/// -/// let router = Router::with_urls([Route::with_handler_and_name("/", infinite_loop, "home")]); -/// ``` -#[macro_export] -macro_rules! reverse_redirect { - ($request:expr, $view_name:literal $(, $($key:ident = $value:expr),*)?) => { - $crate::reverse!( - $request, - $view_name, - $( $($key = $value),* )? - ).map(|url| <$crate::response::Response as $crate::response::ResponseExt>::new_redirect(url)) - }; -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::StatusCode; - use crate::html::Html; - use crate::request::Request; - use crate::response::{IntoResponse, Response}; - use crate::test::TestRequestBuilder; - - struct MockHandler; - - impl RequestHandler for MockHandler { - async fn handle(&self, _request: Request) -> Result { - Html::new("OK").into_response() - } - } - - #[cfg(feature = "openapi")] - impl crate::openapi::AsApiRoute for MockHandler { - fn as_api_route( - &self, - _route_context: &cot::openapi::RouteContext<'_>, - _schema_generator: &mut schemars::SchemaGenerator, - ) -> aide::openapi::PathItem { - aide::openapi::PathItem::default() - } - } - - #[test] - #[cfg(feature = "openapi")] - fn route_inner_debug() { - let route = Route::with_handler("/test", MockHandler); - assert!(format!("{route:?}").contains("Handler(\"handler(...)\")")); - - let route = Route::with_router("/test", Router::empty()); - assert!(format!("{route:?}").contains("Router(Router {")); - - let route = Route::with_api_handler("/test", MockHandler); - assert!(format!("{route:?}").contains("ApiHandler(\"handler(...)\")")); - } - - #[test] - #[cfg(feature = "openapi")] - fn route_kind() { - let handler_route = Route::with_handler("/test", MockHandler); - assert_eq!(handler_route.kind(), RouteKind::Handler); - - let router_route = Route::with_router("/test", Router::empty()); - assert_eq!(router_route.kind(), RouteKind::Router); - - let api_route = Route::with_api_handler("/test", MockHandler); - assert_eq!(api_route.kind(), RouteKind::Handler); - } - - #[test] - #[cfg(feature = "openapi")] - fn route_router() { - let router = Router::empty(); - let route = Route::with_router("/test", router.clone()); - assert!(route.router().is_some()); - - let route = Route::with_handler("/test", MockHandler); - assert!(route.router().is_none()); - - let route = Route::with_api_handler("/test", MockHandler); - assert!(route.router().is_none()); - } - - #[test] - fn router_with_urls() { - let route = Route::with_handler("/test", MockHandler); - let router = Router::with_urls(vec![route.clone()]); - assert_eq!(router.routes().len(), 1); - } - - #[cot::test] - async fn router_route() { - let route = Route::with_handler("/test", MockHandler); - let router = Router::with_urls(vec![route.clone()]); - let response = router.route(test_request(), "/test").await.unwrap(); - assert_eq!(response.status(), StatusCode::OK); - } - - #[cot::test] - async fn router_handle() { - let route = Route::with_handler("/test", MockHandler); - let router = Router::with_urls(vec![route.clone()]); - let response = router.handle(test_request()).await.unwrap(); - assert_eq!(response.status(), StatusCode::OK); - } - - #[cot::test] - async fn sub_router_handle() { - let route_1 = Route::with_handler("/test", MockHandler); - let sub_router_1 = Router::with_urls(vec![route_1.clone()]); - let route_2 = Route::with_handler("/test", MockHandler); - let sub_router_2 = Router::with_urls(vec![route_2.clone()]); - - let router = Router::with_urls(vec![ - Route::with_router("/", sub_router_1), - Route::with_router("/sub", sub_router_2), - ]); - let response = router - .handle(TestRequestBuilder::get("/sub/test").build()) - .await - .unwrap(); - assert_eq!(response.status(), StatusCode::OK); - } - - #[test] - fn router_reverse() { - let route = Route::with_handler_and_name("/test", MockHandler, "test"); - let router = Router::with_urls(vec![route.clone()]); - let params = ReverseParamMap::new(); - let url = router.reverse(None, "test", ¶ms).unwrap(); - assert_eq!(url, "/test"); - } - - #[test] - fn router_reverse_with_param() { - let route = Route::with_handler_and_name("/test/{id}", MockHandler, "test"); - let router = Router::with_urls(vec![route.clone()]); - let mut params = ReverseParamMap::new(); - params.insert("id", "123"); - let url = router.reverse(None, "test", ¶ms).unwrap(); - assert_eq!(url, "/test/123"); - } - - #[test] - fn router_reverse_app_name() { - let route = Route::with_handler_and_name("/test", MockHandler, "test"); - let mut router_1 = Router::with_urls(vec![route.clone()]); - router_1.set_app_name(AppName("app_1".to_string())); - let mut router_2 = Router::with_urls(vec![route.clone()]); - router_2.set_app_name(AppName("app_2".to_string())); - let root_router = Router::with_urls(vec![ - Route::with_router("/", router_1), - Route::with_router("/sub", router_2), - ]); - - let params = ReverseParamMap::new(); - let url = root_router.reverse(Some("app_2"), "test", ¶ms).unwrap(); - - assert_eq!(url, "/sub/test"); - } - - #[test] - fn router_reverse_app_name_nested() { - let route = Route::with_handler_and_name("/test", MockHandler, "test"); - let router = Router::with_urls(vec![route.clone()]); - let sub_router = Router::with_urls(vec![Route::with_router("/sub", router)]); - let mut root_router = Router::with_urls(vec![Route::with_router("/subsub", sub_router)]); - root_router.set_app_name(AppName("app_root".to_string())); - - let params = ReverseParamMap::new(); - let url = root_router - .reverse(Some("app_root"), "test", ¶ms) - .unwrap(); - - assert_eq!(url, "/subsub/sub/test"); - } - - #[test] - fn router_reverse_option() { - let route = Route::with_handler_and_name("/test", MockHandler, "test"); - let router = Router::with_urls(vec![route.clone()]); - let params = ReverseParamMap::new(); - let url = router - .reverse_option(None, "test", ¶ms) - .unwrap() - .unwrap(); - assert_eq!(url, "/test"); - } - - #[test] - fn router_routes() { - let route = Route::with_handler("/test", MockHandler); - let router = Router::with_urls(vec![route.clone()]); - assert_eq!(router.routes().len(), 1); - } - - #[test] - fn router_is_empty() { - let router = Router::with_urls(vec![]); - assert!(router.is_empty()); - } - - #[test] - fn route_with_handler() { - let route = Route::with_handler("/test", MockHandler); - assert_eq!(route.url.to_string(), "/test"); - } - - #[test] - fn route_with_handler_and_params() { - let route = Route::with_handler("/test/{id}", MockHandler); - assert_eq!(route.url.to_string(), "/test/{id}"); - } - - #[test] - fn route_with_handler_and_name() { - let route = Route::with_handler_and_name("/test", MockHandler, "test"); - assert_eq!(route.url.to_string(), "/test"); - assert_eq!(route.name, Some(RouteName("test".to_string()))); - } - - #[test] - fn route_with_router() { - let sub_route = Route::with_handler("/sub", MockHandler); - let sub_router = Router::with_urls(vec![sub_route]); - let route = Route::with_router("/test", sub_router); - assert_eq!(route.url.to_string(), "/test"); - } - - #[test] - fn test_reverse_macro() { - let route = Route::with_handler_and_name("/test/{id}", MockHandler, "test"); - let router = Router::with_urls(vec![route]); - - let request = TestRequestBuilder::get("/").router(router).build(); - let url = reverse!(request, "test", id = 123).unwrap(); - - assert_eq!(url, "/test/123"); - } - - #[test] - fn test_reverse_redirect_macro() { - let route = Route::with_handler_and_name("/test/{id}", MockHandler, "test"); - let router = Router::with_urls(vec![route]); - - let request = TestRequestBuilder::get("/").router(router).build(); - let response = cot::reverse_redirect!(request, "test", id = 123).unwrap(); - - assert_eq!(response.status(), StatusCode::SEE_OTHER); - assert_eq!(response.headers().get("location").unwrap(), "/test/123"); - } - - fn test_request() -> Request { - TestRequestBuilder::get("/test").build() - } -} diff --git a/cot/src/session.rs b/cot/src/session.rs index d506527c..3e28d10a 100644 --- a/cot/src/session.rs +++ b/cot/src/session.rs @@ -7,10 +7,10 @@ //! //! ``` //! use cot::RequestHandler; -//! use cot::html::Html; -//! use cot::router::{Route, Router}; //! use cot::session::Session; //! use cot::test::TestRequestBuilder; +//! use cot_core::html::Html; +//! use cot_core::router::{Route, Router}; //! //! async fn my_handler(session: Session) -> cot::Result { //! session.insert("user_name", "world".to_string()).await?; @@ -51,11 +51,11 @@ use std::ops::{Deref, DerefMut}; /// /// ``` /// use cot::RequestHandler; -/// use cot::html::Html; /// use cot::request::Request; -/// use cot::router::{Route, Router}; /// use cot::session::Session; /// use cot::test::TestRequestBuilder; +/// use cot_core::html::Html; +/// use cot_core::router::{Route, Router}; /// /// async fn my_handler(session: Session) -> cot::Result { /// session.insert("user_name", "world".to_string()).await?; @@ -99,11 +99,11 @@ impl Session { /// /// ``` /// use cot::RequestHandler; - /// use cot::html::Html; /// use cot::request::Request; - /// use cot::router::{Route, Router}; /// use cot::session::Session; /// use cot::test::TestRequestBuilder; + /// use cot_core::html::Html; + /// use cot_core::router::{Route, Router}; /// /// async fn my_handler(request: Request) -> cot::Result { /// let session = Session::from_request(&request); diff --git a/cot/src/static_files.rs b/cot/src/static_files.rs index 26948f1f..21040d78 100644 --- a/cot/src/static_files.rs +++ b/cot/src/static_files.rs @@ -12,6 +12,8 @@ use std::task::{Context, Poll}; use std::time::Duration; use bytes::Bytes; +use cot_core::error::error_impl::impl_into_cot_error; +use cot_core::response::{Response, ResponseExt}; use digest::Digest; use futures_core::ready; use http::{Request, header}; @@ -21,9 +23,7 @@ use tower::Service; use crate::Body; use crate::config::{StaticFilesConfig, StaticFilesPathRewriteMode}; -use crate::error::error_impl::impl_into_cot_error; use crate::project::MiddlewareContext; -use crate::response::{Response, ResponseExt}; /// Macro to define static files by specifying their paths. /// diff --git a/cot/src/test.rs b/cot/src/test.rs index d01fa3b5..7ec490cd 100644 --- a/cot/src/test.rs +++ b/cot/src/test.rs @@ -7,6 +7,9 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::sync::Arc; use async_trait::async_trait; +use cot_core::handler::BoxedHandler; +use cot_core::response::Response; +use cot_core::router::Router; use derive_more::Debug; use tokio::net::TcpListener; use tokio::sync::oneshot; @@ -23,11 +26,8 @@ use crate::db::Database; use crate::db::migrations::{ DynMigration, MigrationDependency, MigrationEngine, MigrationWrapper, Operation, }; -use crate::handler::BoxedHandler; use crate::project::{prepare_request, prepare_request_for_error_handler, run_at_with_shutdown}; use crate::request::Request; -use crate::response::Response; -use crate::router::Router; use crate::session::Session; use crate::static_files::{StaticFile, StaticFiles}; use crate::{Body, Bootstrapper, Project, ProjectContext, Result}; @@ -185,9 +185,9 @@ impl Client { /// # Examples /// /// ``` -/// use cot::html::Html; /// use cot::request::Request; /// use cot::test::TestRequestBuilder; +/// use cot_core::html::Html; /// /// # #[tokio::main] /// # async fn main() -> cot::Result<()> { @@ -280,8 +280,8 @@ impl TestRequestBuilder { /// /// ``` /// use cot::RequestHandler; - /// use cot::html::Html; /// use cot::test::TestRequestBuilder; + /// use cot_core::html::Html; /// /// # #[tokio::main] /// # async fn main() -> cot::Result<()> { @@ -314,8 +314,8 @@ impl TestRequestBuilder { /// /// ``` /// use cot::RequestHandler; - /// use cot::html::Html; /// use cot::test::TestRequestBuilder; + /// use cot_core::html::Html; /// /// # #[tokio::main] /// # async fn main() -> cot::Result<()> { @@ -348,9 +348,9 @@ impl TestRequestBuilder { /// /// ``` /// use cot::RequestHandler; - /// use cot::html::Html; /// use cot::http::Method; /// use cot::test::TestRequestBuilder; + /// use cot_core::html::Html; /// /// # #[tokio::main] /// # async fn main() -> cot::Result<()> { @@ -403,9 +403,9 @@ impl TestRequestBuilder { /// # Examples /// /// ``` - /// use cot::html::Html; /// use cot::request::Request; /// use cot::test::TestRequestBuilder; + /// use cot_core::html::Html; /// /// # #[tokio::main] /// # async fn main() -> cot::Result<()> { @@ -447,9 +447,9 @@ impl TestRequestBuilder { /// /// ``` /// use cot::request::Request; - /// use cot::response::Response; - /// use cot::router::{Route, Router}; /// use cot::test::TestRequestBuilder; + /// use cot_core::response::Response; + /// use cot_core::router::{Route, Router}; /// /// async fn index(request: Request) -> cot::Result { /// unimplemented!() @@ -541,7 +541,7 @@ impl TestRequestBuilder { /// ``` /// use cot::RequestHandler; /// use cot::db::Database; - /// use cot::html::Html; + /// use cot_core::html::Html; /// use cot::test::TestRequestBuilder; /// use cot::request::extractors::RequestDb; /// @@ -704,8 +704,8 @@ impl TestRequestBuilder { /// /// ``` /// use cot::RequestHandler; - /// use cot::html::Html; /// use cot::test::TestRequestBuilder; + /// use cot_core::html::Html; /// /// # #[tokio::main] /// # async fn main() -> cot::Result<()> { diff --git a/cot/tests/auth.rs b/cot/tests/auth.rs index 12bacb30..f37888fe 100644 --- a/cot/tests/auth.rs +++ b/cot/tests/auth.rs @@ -1,7 +1,7 @@ use std::borrow::Cow; -use cot::auth::Auth; use cot::auth::db::{DatabaseUser, DatabaseUserCredentials}; +use cot::auth::Auth; use cot::common_types::Password; use cot::request::RequestExt; use cot::test::{TestDatabase, TestRequestBuilder}; diff --git a/cot/tests/from_request.rs b/cot/tests/from_request.rs index 560a9f32..ed397c81 100644 --- a/cot/tests/from_request.rs +++ b/cot/tests/from_request.rs @@ -1,6 +1,6 @@ use cot::http::Request; -use cot::request::RequestHead; -use cot::request::extractors::FromRequestHead; +use cot_core::request::RequestHead; +use cot_core::request::extractors::FromRequestHead; #[derive(FromRequestHead)] #[expect(dead_code)] diff --git a/cot/tests/openapi.rs b/cot/tests/openapi.rs index 23a06914..b0a5547e 100644 --- a/cot/tests/openapi.rs +++ b/cot/tests/openapi.rs @@ -1,10 +1,10 @@ use aide::openapi::{Parameter, PathItem, ReferenceOr}; use cot::html::Html; use cot::json::Json; +use cot::openapi::method::{api_get, api_post, ApiMethodRouter}; use cot::openapi::{AsApiRoute, NoApi, RouteContext}; use cot::request::extractors::{Path, UrlQuery}; use cot::response::{IntoResponse, Response}; -use cot::router::method::openapi::{ApiMethodRouter, api_get, api_post}; use cot::router::{Route, Router}; use cot::test::TestRequestBuilder; use cot::{RequestHandler, StatusCode}; diff --git a/cot/tests/project.rs b/cot/tests/project.rs index ba96670c..4963a94e 100644 --- a/cot/tests/project.rs +++ b/cot/tests/project.rs @@ -1,11 +1,11 @@ use bytes::Bytes; use cot::config::ProjectConfig; -use cot::html::Html; use cot::project::RegisterAppsContext; -use cot::request::Request; -use cot::router::{Route, Router}; use cot::test::Client; use cot::{App, AppBuilder, Project, StatusCode, reverse}; +use cot_core::html::Html; +use cot_core::request::Request; +use cot_core::router::{Route, Router}; #[cot::test] #[cfg_attr( diff --git a/examples/json/src/main.rs b/examples/json/src/main.rs index 50767bd0..56de1a69 100644 --- a/examples/json/src/main.rs +++ b/examples/json/src/main.rs @@ -2,10 +2,10 @@ use cot::cli::CliMetadata; use cot::config::ProjectConfig; use cot::error::handler::{DynErrorPageHandler, RequestError}; use cot::json::Json; +use cot::openapi::method::api_post; use cot::openapi::swagger_ui::SwaggerUi; use cot::project::{MiddlewareContext, RegisterAppsContext, RootHandler, RootHandlerBuilder}; use cot::response::IntoResponse; -use cot::router::method::openapi::api_post; use cot::router::{Route, Router}; use cot::static_files::StaticFilesMiddleware; use cot::{App, AppBuilder, Project};