From 2be19f8d605ace3d749b6b90adde0e5abea22897 Mon Sep 17 00:00:00 2001 From: Marek 'seqre' Grzelak Date: Sat, 4 Oct 2025 21:14:10 +0200 Subject: [PATCH 01/18] create cot-core crate --- Cargo.lock | 57 +++++++++++++++++++++++++++++---------------- Cargo.toml | 1 + cot-core/Cargo.toml | 18 ++++++++++++++ cot-core/src/lib.rs | 1 + cot/Cargo.toml | 1 + 5 files changed, 58 insertions(+), 20 deletions(-) create mode 100644 cot-core/Cargo.toml create mode 100644 cot-core/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 002acd30..7ba19441 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,10 @@ dependencies = [ "tracing", ] +[[package]] +name = "cot_core" +version = "0.4.0" + [[package]] name = "cot_macros" version = "0.4.0" @@ -1977,13 +1982,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 +3109,28 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +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 = "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 +3175,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 +3358,7 @@ dependencies = [ "serde_json", "sha2", "smallvec", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tokio-stream", "tracing", @@ -3424,7 +3441,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.16", + "thiserror 2.0.17", "tracing", "whoami", ] @@ -3462,7 +3479,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.16", + "thiserror 2.0.17", "tracing", "whoami", ] @@ -3487,7 +3504,7 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", - "thiserror 2.0.16", + "thiserror 2.0.17", "tracing", "url", ] @@ -3617,11 +3634,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 +3654,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 +3973,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..f42fb70a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,6 +71,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..8b31a441 --- /dev/null +++ b/cot-core/Cargo.toml @@ -0,0 +1,18 @@ +[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] diff --git a/cot-core/src/lib.rs b/cot-core/src/lib.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/cot-core/src/lib.rs @@ -0,0 +1 @@ + diff --git a/cot/Cargo.toml b/cot/Cargo.toml index 2fc057c6..8f3bc167 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 From 8a8fb2d9f7b46142384cc8b803032a7775234c1f Mon Sep 17 00:00:00 2001 From: Marek 'seqre' Grzelak Date: Sat, 4 Oct 2025 21:33:18 +0200 Subject: [PATCH 02/18] move error --- cot-core/Cargo.toml | 9 +++++++ {cot => cot-core}/src/error.rs | 4 ++-- {cot => cot-core}/src/error/backtrace.rs | 12 +++++----- {cot => cot-core}/src/error/error_impl.rs | 6 +++-- {cot => cot-core}/src/error/handler.rs | 24 +++++++++---------- .../src/error/method_not_allowed.rs | 4 ++-- {cot => cot-core}/src/error/not_found.rs | 8 +++---- {cot => cot-core}/src/error/uncaught_panic.rs | 6 ++--- cot-core/src/lib.rs | 16 +++++++++++++ cot/src/admin.rs | 2 +- cot/src/auth.rs | 2 +- cot/src/body.rs | 2 +- cot/src/config.rs | 2 +- cot/src/db.rs | 2 +- cot/src/error_page.rs | 6 ++--- cot/src/form.rs | 2 +- cot/src/form/field_value.rs | 3 +-- cot/src/lib.rs | 22 +++++------------ cot/src/project.rs | 10 ++++---- cot/src/request.rs | 2 +- cot/src/request/extractors.rs | 3 +-- cot/src/response/into_response.rs | 4 ++-- cot/src/router.rs | 4 ++-- cot/src/router/method.rs | 3 ++- cot/src/router/method/openapi.rs | 3 ++- cot/src/router/path.rs | 3 +-- cot/src/static_files.rs | 2 +- examples/custom-error-pages/Cargo.toml | 1 + examples/custom-error-pages/src/main.rs | 2 +- examples/json/Cargo.toml | 1 + examples/json/src/main.rs | 2 +- 31 files changed, 95 insertions(+), 77 deletions(-) rename {cot => cot-core}/src/error.rs (80%) rename {cot => cot-core}/src/error/backtrace.rs (94%) rename {cot => cot-core}/src/error/error_impl.rs (99%) rename {cot => cot-core}/src/error/handler.rs (93%) rename {cot => cot-core}/src/error/method_not_allowed.rs (93%) rename {cot => cot-core}/src/error/not_found.rs (96%) rename {cot => cot-core}/src/error/uncaught_panic.rs (95%) diff --git a/cot-core/Cargo.toml b/cot-core/Cargo.toml index 8b31a441..01ffeec2 100644 --- a/cot-core/Cargo.toml +++ b/cot-core/Cargo.toml @@ -16,3 +16,12 @@ authors.workspace = true workspace = true [dependencies] +askama = { workspace = true, features = ["derive", "std"] } +backtrace.workspace = true +derive_more = { workspace = true, features = ["debug", "deref", "display", "from"] } +http.workspace = true +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true, optional = true } +thiserror.workspace = true +tower = { workspace = true, features = ["util"] } +tower-sessions = { workspace = true, features = ["memory-store"] } 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 93% rename from cot/src/error/handler.rs rename to cot-core/src/error/handler.rs index 8dd03002..a8afa81f 100644 --- a/cot/src/error/handler.rs +++ b/cot-core/src/error/handler.rs @@ -7,13 +7,13 @@ use std::pin::Pin; use std::sync::Arc; use std::task::{Context, Poll}; +use cot::handle_all_parameters; +use cot::request::{Request, RequestHead}; +use cot::response::Response; 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; /// A trait for handling error pages in Cot applications. /// @@ -26,9 +26,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 +64,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 +87,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 +120,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 +134,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 +189,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 +278,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 +289,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-core/src/lib.rs b/cot-core/src/lib.rs index 8b137891..b28622ce 100644 --- a/cot-core/src/lib.rs +++ b/cot-core/src/lib.rs @@ -1 +1,17 @@ +use crate::error::error_impl::Error; +/// 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; + +/// 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; diff --git a/cot/src/admin.rs b/cot/src/admin.rs index 42f193a2..e8b14794 100644 --- a/cot/src/admin.rs +++ b/cot/src/admin.rs @@ -9,6 +9,7 @@ use std::marker::PhantomData; use askama::Template; use async_trait::async_trait; use bytes::Bytes; +use cot_core::error::NotFound; /// Implements the [`AdminModel`] trait for a struct. /// /// This is a simple method for adding a database model to the admin panel. @@ -22,7 +23,6 @@ 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, }; diff --git a/cot/src/auth.rs b/cot/src/auth.rs index 8dd86daf..4fa9405e 100644 --- a/cot/src/auth.rs +++ b/cot/src/auth.rs @@ -16,6 +16,7 @@ use std::sync::{Arc, Mutex, MutexGuard}; /// backwards compatible shim for form Password type. use async_trait::async_trait; use chrono::{DateTime, FixedOffset}; +use cot_core::error::error_impl::impl_into_cot_error; use derive_more::with_trait::Debug; #[cfg(test)] use mockall::automock; @@ -27,7 +28,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; diff --git a/cot/src/body.rs b/cot/src/body.rs index 874307c3..7e2a4400 100644 --- a/cot/src/body.rs +++ b/cot/src/body.rs @@ -4,12 +4,12 @@ use std::pin::Pin; use std::task::{Context, Poll}; use bytes::Bytes; +use cot_core::error::error_impl::impl_into_cot_error; use futures_core::Stream; 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}; /// A type that represents an HTTP request or response body. 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..3c26b493 100644 --- a/cot/src/error_page.rs +++ b/cot/src/error_page.rs @@ -3,11 +3,11 @@ 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}; @@ -71,7 +71,7 @@ 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 => { diff --git a/cot/src/form.rs b/cot/src/form.rs index 67d790f9..d72b1f76 100644 --- a/cot/src/form.rs +++ b/cot/src/form.rs @@ -31,6 +31,7 @@ use async_trait::async_trait; use bytes::Bytes; use chrono::NaiveDateTime; use chrono_tz::Tz; +use cot_core::error::error_impl::impl_into_cot_error; /// Derive the [`Form`] trait for a struct and create a [`FormContext`] for it. /// /// This macro will generate an implementation of the [`Form`] trait for the @@ -61,7 +62,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}; 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/lib.rs b/cot/src/lib.rs index 8e14b530..84c318dd 100644 --- a/cot/src/lib.rs +++ b/cot/src/lib.rs @@ -55,12 +55,6 @@ 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. @@ -96,6 +90,12 @@ pub(crate) mod utils; #[cfg(feature = "openapi")] pub use aide; pub use body::Body; +pub use cot_core::Method; +/// 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; /// An attribute macro that defines an end-to-end test function for a /// Cot-powered app. /// @@ -159,7 +159,6 @@ 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}; @@ -168,12 +167,3 @@ 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/project.rs b/cot/src/project.rs index 1ecaf462..51c265ff 100644 --- a/cot/src/project.rs +++ b/cot/src/project.rs @@ -28,6 +28,9 @@ use std::sync::Arc; use askama::Template; use async_trait::async_trait; use axum::handler::HandlerWithoutStateExt; +use cot_core::error::UncaughtPanic; +use cot_core::error::error_impl::impl_into_cot_error; +use cot_core::error::handler::{DynErrorPageHandler, RequestOuterError}; use derive_more::with_trait::Debug; use futures_util::FutureExt; use thiserror::Error; @@ -47,9 +50,6 @@ 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; @@ -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}; /// /// struct MyProject; /// impl Project for MyProject { @@ -2137,6 +2137,7 @@ async fn shutdown_signal() { mod tests { use std::task::{Context, Poll}; + use cot_core::error::handler::{RequestError, RequestOuterError}; use tower::util::MapResultLayer; use tower::{ServiceExt, service_fn}; @@ -2144,7 +2145,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; diff --git a/cot/src/request.rs b/cot/src/request.rs index 092e0d57..c2bf892b 100644 --- a/cot/src/request.rs +++ b/cot/src/request.rs @@ -15,12 +15,12 @@ use std::future::Future; use std::sync::Arc; +use cot_core::error::error_impl::impl_into_cot_error; 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}; diff --git a/cot/src/request/extractors.rs b/cot/src/request/extractors.rs index d4c0fc75..08f82311 100644 --- a/cot/src/request/extractors.rs +++ b/cot/src/request/extractors.rs @@ -546,6 +546,7 @@ impl FromRequestHead for Auth { } } +use cot_core::error::error_impl::impl_into_cot_error; /// A derive macro that automatically implements the [`FromRequestHead`] trait /// for structs. /// @@ -581,8 +582,6 @@ impl FromRequestHead for Auth { /// ``` pub use cot_macros::FromRequestHead; -use crate::error::error_impl::impl_into_cot_error; - #[cfg(test)] mod tests { use serde::Deserialize; diff --git a/cot/src/response/into_response.rs b/cot/src/response/into_response.rs index 1f1d1f90..4666fe26 100644 --- a/cot/src/response/into_response.rs +++ b/cot/src/response/into_response.rs @@ -1,8 +1,8 @@ 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 cot_core::error::error_impl::impl_into_cot_error; use http; #[cfg(feature = "json")] @@ -390,10 +390,10 @@ mod tests { use bytes::{Bytes, BytesMut}; use cot::response::Response; use cot::{Body, StatusCode}; + use cot_core::error::NotFound; use http::{self, HeaderMap, HeaderValue}; use super::*; - use crate::error::NotFound; use crate::html::Html; #[cot::test] diff --git a/cot/src/router.rs b/cot/src/router.rs index c6fd91be..a681aa06 100644 --- a/cot/src/router.rs +++ b/cot/src/router.rs @@ -27,11 +27,11 @@ use std::pin::Pin; use std::sync::Arc; use std::task::{Context, Poll}; +use cot_core::error::NotFound; +use cot_core::error::error_impl::impl_into_cot_error; 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; diff --git a/cot/src/router/method.rs b/cot/src/router/method.rs index c2c55ac9..94bd0a11 100644 --- a/cot/src/router/method.rs +++ b/cot/src/router/method.rs @@ -5,7 +5,8 @@ pub mod openapi; use std::fmt::{Debug, Formatter}; -use crate::error::MethodNotAllowed; +use cot_core::error::MethodNotAllowed; + use crate::handler::{BoxRequestHandler, into_box_request_handler}; use crate::request::Request; use crate::response::Response; diff --git a/cot/src/router/method/openapi.rs b/cot/src/router/method/openapi.rs index 078bc860..bf51bb8e 100644 --- a/cot/src/router/method/openapi.rs +++ b/cot/src/router/method/openapi.rs @@ -517,8 +517,9 @@ where #[cfg(test)] mod tests { + use cot_core::error::MethodNotAllowed; + use super::*; - use crate::error::MethodNotAllowed; use crate::html::Html; use crate::json::Json; use crate::request::extractors::Path; diff --git a/cot/src/router/path.rs b/cot/src/router/path.rs index 3b2e35d1..1cf30198 100644 --- a/cot/src/router/path.rs +++ b/cot/src/router/path.rs @@ -7,11 +7,10 @@ use std::collections::HashMap; use std::fmt::Display; +use cot_core::error::error_impl::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 { parts: Vec, diff --git a/cot/src/static_files.rs b/cot/src/static_files.rs index 26948f1f..d9dfa27b 100644 --- a/cot/src/static_files.rs +++ b/cot/src/static_files.rs @@ -12,6 +12,7 @@ use std::task::{Context, Poll}; use std::time::Duration; use bytes::Bytes; +use cot_core::error::error_impl::impl_into_cot_error; use digest::Digest; use futures_core::ready; use http::{Request, header}; @@ -21,7 +22,6 @@ 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}; diff --git a/examples/custom-error-pages/Cargo.toml b/examples/custom-error-pages/Cargo.toml index 47606c2e..1b7b8d00 100644 --- a/examples/custom-error-pages/Cargo.toml +++ b/examples/custom-error-pages/Cargo.toml @@ -9,3 +9,4 @@ edition = "2024" [dependencies] askama = "0.14" cot = { path = "../../cot" } +cot_core = { path = "../../cot-core" } diff --git a/examples/custom-error-pages/src/main.rs b/examples/custom-error-pages/src/main.rs index 8c4872f5..f7d3fbaf 100644 --- a/examples/custom-error-pages/src/main.rs +++ b/examples/custom-error-pages/src/main.rs @@ -1,12 +1,12 @@ use askama::Template; use cot::cli::CliMetadata; use cot::config::ProjectConfig; -use cot::error::handler::{DynErrorPageHandler, RequestError}; use cot::html::Html; use cot::project::RegisterAppsContext; use cot::response::{IntoResponse, Response}; use cot::router::{Route, Router}; use cot::{App, AppBuilder, Project}; +use cot_core::error::handler::{DynErrorPageHandler, RequestError}; async fn return_hello() -> cot::Result { panic!() diff --git a/examples/json/Cargo.toml b/examples/json/Cargo.toml index 8f921cdd..154814a6 100644 --- a/examples/json/Cargo.toml +++ b/examples/json/Cargo.toml @@ -8,5 +8,6 @@ edition = "2024" [dependencies] cot = { path = "../../cot", features = ["openapi", "swagger-ui"] } +cot_core = { path = "../../cot-core" } serde = "1" schemars = "0.9" diff --git a/examples/json/src/main.rs b/examples/json/src/main.rs index 50767bd0..56182a62 100644 --- a/examples/json/src/main.rs +++ b/examples/json/src/main.rs @@ -1,6 +1,5 @@ use cot::cli::CliMetadata; use cot::config::ProjectConfig; -use cot::error::handler::{DynErrorPageHandler, RequestError}; use cot::json::Json; use cot::openapi::swagger_ui::SwaggerUi; use cot::project::{MiddlewareContext, RegisterAppsContext, RootHandler, RootHandlerBuilder}; @@ -9,6 +8,7 @@ use cot::router::method::openapi::api_post; use cot::router::{Route, Router}; use cot::static_files::StaticFilesMiddleware; use cot::{App, AppBuilder, Project}; +use cot_core::error::handler::{DynErrorPageHandler, RequestError}; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Deserialize, schemars::JsonSchema)] From d7027bc37003087d71a4a46efccc5a19baa630f8 Mon Sep 17 00:00:00 2001 From: Marek 'seqre' Grzelak Date: Sat, 4 Oct 2025 21:46:20 +0200 Subject: [PATCH 03/18] move body --- cot-core/Cargo.toml | 13 ++++++++++ {cot => cot-core}/src/body.rs | 45 +++++++++++++++++------------------ cot-core/src/lib.rs | 3 +++ cot/src/lib.rs | 4 +--- cot/src/response.rs | 3 ++- 5 files changed, 41 insertions(+), 27 deletions(-) rename {cot => cot-core}/src/body.rs (91%) diff --git a/cot-core/Cargo.toml b/cot-core/Cargo.toml index 01ffeec2..1b5ac98a 100644 --- a/cot-core/Cargo.toml +++ b/cot-core/Cargo.toml @@ -17,11 +17,24 @@ workspace = true [dependencies] 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"] } +futures-core.workspace = true +futures-util.workspace = true +http-body-util.workspace = true +http-body.workspace = true http.workspace = true serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true, optional = true } +sync_wrapper.workspace = true thiserror.workspace = true tower = { workspace = true, features = ["util"] } tower-sessions = { workspace = true, features = ["memory-store"] } + +[dev-dependencies] +async-stream.workspace = true +futures.workspace = true +tokio = { workspace = true, features = ["macros"] } 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 7e2a4400..952f4b39 100644 --- a/cot/src/body.rs +++ b/cot-core/src/body.rs @@ -4,13 +4,12 @@ use std::pin::Pin; use std::task::{Context, Poll}; use bytes::Bytes; -use cot_core::error::error_impl::impl_into_cot_error; use futures_core::Stream; use http_body::{Frame, SizeHint}; use http_body_util::combinators::BoxBody; use sync_wrapper::SyncWrapper; -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-core/src/lib.rs b/cot-core/src/lib.rs index b28622ce..c5444db2 100644 --- a/cot-core/src/lib.rs +++ b/cot-core/src/lib.rs @@ -1,5 +1,6 @@ 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 @@ -15,3 +16,5 @@ 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/src/lib.rs b/cot/src/lib.rs index 84c318dd..6739213a 100644 --- a/cot/src/lib.rs +++ b/cot/src/lib.rs @@ -63,7 +63,6 @@ mod headers; pub mod __private; pub mod admin; pub mod auth; -mod body; pub mod cli; pub mod common_types; pub mod config; @@ -89,13 +88,12 @@ pub(crate) mod utils; #[cfg(feature = "openapi")] pub use aide; -pub use body::Body; -pub use cot_core::Method; /// 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::{Body, Method}; /// An attribute macro that defines an end-to-end test function for a /// Cot-powered app. /// diff --git a/cot/src/response.rs b/cot/src/response.rs index e6233a73..d5fbb812 100644 --- a/cot/src/response.rs +++ b/cot/src/response.rs @@ -146,8 +146,9 @@ impl ResponseExt for Response { #[cfg(test)] mod tests { + use cot_core::body::BodyInner; + use super::*; - use crate::body::BodyInner; use crate::headers::JSON_CONTENT_TYPE; use crate::response::{Response, ResponseExt}; From 35268a33b3c78d8cbbfba73fe80e443dd5919cad Mon Sep 17 00:00:00 2001 From: Marek 'seqre' Grzelak Date: Tue, 7 Oct 2025 20:14:01 +0200 Subject: [PATCH 04/18] move requests --- Cargo.lock | 26 + cot-core/Cargo.toml | 4 + cot-core/src/lib.rs | 1 + cot-core/src/request.rs | 855 +++++++++++++++++ cot-core/src/request/extractors.rs | 473 ++++++++++ .../src/request/path_params_deserializer.rs | 6 +- cot/src/admin.rs | 4 +- cot/src/auth.rs | 4 +- cot/src/form.rs | 2 +- cot/src/handler.rs | 4 +- cot/src/lib.rs | 2 +- cot/src/project.rs | 16 +- cot/src/request.rs | 862 ------------------ cot/src/request/extractors.rs | 554 +---------- cot/src/router.rs | 38 +- cot/tests/auth.rs | 2 +- cot/tests/from_request.rs | 4 +- cot/tests/openapi.rs | 2 +- cot/tests/project.rs | 2 +- cot/tests/router.rs | 2 +- examples/file-upload/src/main.rs | 2 +- examples/forms/src/main.rs | 5 +- examples/sessions/src/main.rs | 2 +- examples/todo-list/src/main.rs | 3 +- 24 files changed, 1438 insertions(+), 1437 deletions(-) create mode 100644 cot-core/src/request.rs create mode 100644 cot-core/src/request/extractors.rs rename {cot => cot-core}/src/request/path_params_deserializer.rs (99%) diff --git a/Cargo.lock b/Cargo.lock index 7ba19441..36accc5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -768,6 +768,32 @@ dependencies = [ [[package]] name = "cot_core" version = "0.4.0" +dependencies = [ + "askama", + "async-stream", + "axum", + "backtrace", + "bytes", + "cot_macros", + "derive_more", + "form_urlencoded", + "futures", + "futures-core", + "futures-util", + "http 1.3.1", + "http-body", + "http-body-util", + "indexmap", + "serde", + "serde_html_form", + "serde_json", + "serde_path_to_error", + "sync_wrapper", + "thiserror 2.0.17", + "tokio", + "tower", + "tower-sessions", +] [[package]] name = "cot_macros" diff --git a/cot-core/Cargo.toml b/cot-core/Cargo.toml index 1b5ac98a..887d1296 100644 --- a/cot-core/Cargo.toml +++ b/cot-core/Cargo.toml @@ -22,13 +22,17 @@ 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 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"] } diff --git a/cot-core/src/lib.rs b/cot-core/src/lib.rs index c5444db2..09870d5f 100644 --- a/cot-core/src/lib.rs +++ b/cot-core/src/lib.rs @@ -7,6 +7,7 @@ pub mod body; /// 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 request; /// A type alias for a result that can return a [`cot_core::Error`]. pub type Result = std::result::Result; diff --git a/cot-core/src/request.rs b/cot-core/src/request.rs new file mode 100644 index 00000000..32833291 --- /dev/null +++ b/cot-core/src/request.rs @@ -0,0 +1,855 @@ +//! 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 std::future::Future; +use std::sync::Arc; + +#[cfg(feature = "db")] +use cot::db::Database; +use cot::router::Router; +use http::Extensions; +use indexmap::IndexMap; + +use crate::request::extractors::FromRequestHead; +use crate::{Result, 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; + +mod private { + pub trait Sealed {} +} + +/// Extension trait for [`http::Request`] that provides helper methods for +/// working with HTTP requests. +/// +/// # Sealed +/// +/// This trait is sealed since it doesn't make sense to be implemented for types +/// outside the context of Cot. +pub trait RequestExt: private::Sealed { + /// Runs an extractor implementing [`FromRequestHead`] on the request. + /// + /// # Examples + /// + /// ``` + /// use cot::response::Response; + /// use cot_core::request::extractors::Path; + /// use cot_core::request::{Request, RequestExt}; + /// + /// 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 + where + E: FromRequestHead + 'static; + + /// Get the application context. + /// + /// # Examples + /// + /// ``` + /// use cot::response::Response; + /// use cot_core::request::{Request, RequestExt}; + /// + /// 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) -> &cot::project::ProjectContext; + + /// Get the project configuration. + /// + /// # Examples + /// + /// ``` + /// use cot::response::Response; + /// use cot_core::request::{Request, RequestExt}; + /// + /// 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) -> &cot::config::ProjectConfig; + + /// Get the router. + /// + /// # Examples + /// + /// ``` + /// use cot::response::Response; + /// use cot_core::request::{Request, RequestExt}; + /// + /// async fn my_handler(mut request: Request) -> cot_core::Result { + /// let router = request.router(); + /// // ... do something with the router + /// # unimplemented!() + /// } + /// ``` + #[must_use] + fn router(&self) -> &Arc; + + /// Get the app name the current route belongs to, or [`None`] if the + /// request is not routed. + /// + /// This is mainly useful for providing context to reverse redirects, where + /// you want to redirect to a route in the same app. + /// + /// # Examples + /// + /// ``` + /// use cot::response::Response; + /// use cot_core::request::{Request, RequestExt}; + /// + /// async fn my_handler(mut request: Request) -> cot_core::Result { + /// let app_name = request.app_name(); + /// // ... do something with the app name + /// # unimplemented!() + /// } + /// ``` + fn app_name(&self) -> Option<&str>; + + /// Get the route name, or [`None`] if the request is not routed or doesn't + /// have a route name. + /// + /// This is mainly useful for use in templates, where you want to know which + /// route is being rendered, for instance to mark the active tab. + /// + /// # Examples + /// + /// ``` + /// use cot::response::Response; + /// use cot_core::request::{Request, RequestExt}; + /// + /// async fn my_handler(mut request: Request) -> cot_core::Result { + /// let route_name = request.route_name(); + /// // ... do something with the route name + /// # unimplemented!() + /// } + /// ``` + #[must_use] + fn route_name(&self) -> Option<&str>; + + /// Get the path parameters. + /// + /// # Examples + /// + /// ``` + /// use cot::response::Response; + /// use cot_core::request::{Request, RequestExt}; + /// + /// async fn my_handler(mut request: Request) -> cot_core::Result { + /// let path_params = request.path_params(); + /// // ... do something with the path params + /// # unimplemented!() + /// } + /// ``` + #[must_use] + fn path_params(&self) -> &PathParams; + + /// Get the path parameters mutably. + /// + /// # Examples + /// + /// ``` + /// use cot::response::Response; + /// use cot_core::request::{Request, RequestExt}; + /// + /// async fn my_handler(mut request: Request) -> cot_core::Result { + /// let path_params = request.path_params_mut(); + /// // ... do something with the path params + /// # unimplemented!() + /// } + /// ``` + #[must_use] + fn path_params_mut(&mut self) -> &mut PathParams; + + /// Get the database. + /// + /// # Examples + /// + /// ``` + /// use cot::response::Response; + /// use cot_core::request::{Request, RequestExt}; + /// + /// async fn my_handler(mut request: Request) -> cot_core::Result { + /// let db = request.db(); + /// // ... do something with the database + /// # unimplemented!() + /// } + /// ``` + #[cfg(feature = "db")] + #[must_use] + fn db(&self) -> &Arc; + + /// Get the content type of the request. + /// + /// # Examples + /// + /// ``` + /// use cot::response::Response; + /// use cot_core::request::{Request, RequestExt}; + /// + /// async fn my_handler(mut request: Request) -> cot_core::Result { + /// let content_type = request.content_type(); + /// // ... do something with the content type + /// # unimplemented!() + /// } + /// ``` + #[must_use] + fn content_type(&self) -> Option<&http::HeaderValue>; + + /// Expect the content type of the request to be the given value. + /// + /// # Errors + /// + /// Throws an error if the content type is not the expected value. + /// + /// # Examples + /// + /// ``` + /// use cot::response::Response; + /// use cot_core::request::{Request, RequestExt}; + /// + /// 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<()> { + let content_type = self + .content_type() + .map_or("".into(), |value| String::from_utf8_lossy(value.as_bytes())); + if content_type == expected { + Ok(()) + } else { + Err(InvalidContentType { + expected, + actual: content_type.into_owned(), + } + .into()) + } + } + + #[doc(hidden)] + fn extensions(&self) -> &Extensions; +} + +impl private::Sealed for Request {} + +impl RequestExt for Request { + async fn extract_from_head(&mut self) -> Result + where + E: FromRequestHead + 'static, + { + let request = std::mem::take(self); + + let (head, body) = request.into_parts(); + let result = E::from_request_head(&head).await; + + *self = Request::from_parts(head, body); + result + } + + #[track_caller] + fn context(&self) -> &cot::project::ProjectContext { + self.extensions() + .get::>() + .expect("AppContext extension missing") + } + + fn project_config(&self) -> &cot::config::ProjectConfig { + self.context().config() + } + + fn router(&self) -> &Arc { + self.context().router() + } + + fn app_name(&self) -> Option<&str> { + self.extensions() + .get::() + .map(|AppName(name)| name.as_str()) + } + + fn route_name(&self) -> Option<&str> { + self.extensions() + .get::() + .map(|RouteName(name)| name.as_str()) + } + + #[track_caller] + fn path_params(&self) -> &PathParams { + self.extensions() + .get::() + .expect("PathParams extension missing") + } + + fn path_params_mut(&mut self) -> &mut PathParams { + self.extensions_mut().get_or_insert_default::() + } + + #[cfg(feature = "db")] + fn db(&self) -> &Arc { + self.context().database() + } + + fn content_type(&self) -> Option<&http::HeaderValue> { + self.headers().get(http::header::CONTENT_TYPE) + } + + fn extensions(&self) -> &Extensions { + self.extensions() + } +} + +impl private::Sealed for RequestHead {} + +impl RequestExt for RequestHead { + async fn extract_from_head(&mut self) -> Result + where + E: FromRequestHead + 'static, + { + E::from_request_head(self).await + } + + fn context(&self) -> &cot::project::ProjectContext { + self.extensions + .get::>() + .expect("AppContext extension missing") + } + + fn project_config(&self) -> &cot::config::ProjectConfig { + self.context().config() + } + + fn router(&self) -> &Arc { + self.context().router() + } + + fn app_name(&self) -> Option<&str> { + self.extensions + .get::() + .map(|AppName(name)| name.as_str()) + } + + fn route_name(&self) -> Option<&str> { + self.extensions + .get::() + .map(|RouteName(name)| name.as_str()) + } + + fn path_params(&self) -> &PathParams { + self.extensions + .get::() + .expect("PathParams extension missing") + } + + fn path_params_mut(&mut self) -> &mut PathParams { + self.extensions.get_or_insert_default::() + } + + #[cfg(feature = "db")] + fn db(&self) -> &Arc { + self.context().database() + } + + fn content_type(&self) -> Option<&http::HeaderValue> { + self.headers.get(http::header::CONTENT_TYPE) + } + + fn extensions(&self) -> &Extensions { + &self.extensions + } +} + +#[repr(transparent)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct AppName(pub String); + +#[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 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::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, + ) -> 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 cot::response::Response; + use cot::router::{Route, Router}; + use cot::test::TestRequestBuilder; + + use super::*; + use crate::request::extractors::Path; + + #[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(), + } + ); + } + + #[test] + fn request_ext_app_name() { + let mut request = TestRequestBuilder::get("/").build(); + assert_eq!(request.app_name(), None); + + request + .extensions_mut() + .insert(AppName("test_app".to_string())); + assert_eq!(request.app_name(), Some("test_app")); + } + + #[test] + fn request_ext_route_name() { + let mut request = TestRequestBuilder::get("/").build(); + assert_eq!(request.route_name(), None); + + request + .extensions_mut() + .insert(RouteName("test_route".to_string())); + assert_eq!(request.route_name(), Some("test_route")); + } + + #[test] + fn request_ext_parts_route_name() { + let request = TestRequestBuilder::get("/").build(); + let (mut head, _body) = request.into_parts(); + assert_eq!(head.route_name(), None); + + head.extensions.insert(RouteName("test_route".to_string())); + assert_eq!(head.route_name(), Some("test_route")); + } + + #[test] + fn request_ext_path_params() { + let mut request = TestRequestBuilder::get("/").build(); + + let mut params = PathParams::new(); + params.insert("id".to_string(), "42".to_string()); + request.extensions_mut().insert(params); + + assert_eq!(request.path_params().get("id"), Some("42")); + } + + #[test] + fn request_ext_path_params_mut() { + let mut request = TestRequestBuilder::get("/").build(); + + request + .path_params_mut() + .insert("id".to_string(), "42".to_string()); + + assert_eq!(request.path_params().get("id"), Some("42")); + } + + #[test] + fn request_ext_content_type() { + let mut request = TestRequestBuilder::get("/").build(); + assert_eq!(request.content_type(), None); + + request.headers_mut().insert( + http::header::CONTENT_TYPE, + http::HeaderValue::from_static("text/plain"), + ); + + assert_eq!( + request.content_type(), + Some(&http::HeaderValue::from_static("text/plain")) + ); + } + + #[test] + fn request_ext_expect_content_type() { + let mut request = TestRequestBuilder::get("/").build(); + + // Should fail with no content type + assert!(request.expect_content_type("text/plain").is_err()); + + request.headers_mut().insert( + http::header::CONTENT_TYPE, + http::HeaderValue::from_static("text/plain"), + ); + + // Should succeed with matching content type + assert!(request.expect_content_type("text/plain").is_ok()); + + // Should fail with non-matching content type + assert!(request.expect_content_type("application/json").is_err()); + } + + #[cot_macros::test] + async fn request_ext_extract_from_head() { + async fn handler(mut request: Request) -> Result { + let Path(id): Path = request.extract_from_head().await?; + assert_eq!(id, "42"); + + Ok(Response::new(Body::empty())) + } + + let router = Router::with_urls([Route::with_handler("/{id}/", handler)]); + + let request = TestRequestBuilder::get("/42/") + .router(router.clone()) + .build(); + + router.handle(request).await.unwrap(); + } + + #[test] + fn parts_ext_path_params() { + 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); + + assert_eq!(head.path_params().get("id"), Some("42")); + } + + #[test] + fn parts_ext_mutating_path_params() { + let (mut head, _) = Request::new(crate::Body::empty()).into_parts(); + head.path_params_mut() + .insert("page".to_string(), "1".to_string()); + + assert_eq!(head.path_params().get("page"), Some("1")); + } + + #[test] + fn parts_ext_app_name() { + 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")); + } + + #[test] + fn parts_ext_route_name() { + 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")); + } + + #[test] + fn parts_ext_content_type() { + let (mut head, _) = Request::new(crate::Body::empty()).into_parts(); + head.headers.insert( + http::header::CONTENT_TYPE, + http::HeaderValue::from_static("text/plain"), + ); + + assert_eq!( + head.content_type(), + Some(&http::HeaderValue::from_static("text/plain")) + ); + } + + #[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..8c8d338d --- /dev/null +++ b/cot-core/src/request/extractors.rs @@ -0,0 +1,473 @@ +//! 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 std::sync::Arc; + +use cot::auth::Auth; +use cot::form::{Form, FormResult}; +use cot::router::Urls; +use cot::session::Session; +use serde::de::DeserializeOwned; + +use crate::Body; +pub use crate::request::{PathParams, Request, RequestExt, RequestHead}; + +/// 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; +} + +impl FromRequestHead for Urls { + 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::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); + +/// An extractor that gets the request body as form data and deserializes it +/// into a type `F` implementing `cot::form::Form`. +/// +/// The content type of the request must be `application/x-www-form-urlencoded`. +/// +/// # Errors +/// +/// Throws an error if the content type is not +/// `application/x-www-form-urlencoded`. 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 form data is invalid or because the +/// deserialization to the target structure failed. +/// +/// # Example +/// +/// ``` +/// use cot::form::{Form, FormResult}; +/// use cot::html::Html; +/// use cot::test::TestRequestBuilder; +/// use cot_core::request::extractors::RequestForm; +/// +/// #[derive(Form)] +/// struct MyForm { +/// hello: String, +/// } +/// +/// async fn my_handler(RequestForm(form): RequestForm) -> Html { +/// let form = match form { +/// FormResult::Ok(form) => form, +/// FormResult::ValidationError(error) => { +/// panic!("Form validation error!") +/// } +/// }; +/// +/// Html::new(format!("Hello {}!", form.hello)) +/// } +/// +/// # #[tokio::main] +/// # async fn main() -> cot::Result<()> { +/// # use cot::RequestHandler; +/// # let request = TestRequestBuilder::post("/").form_data(&[("hello", "world")]).build(); +/// # my_handler.handle(request).await?; +/// # Ok(()) +/// # } +/// ``` +#[derive(Debug)] +pub struct RequestForm(pub FormResult); + +impl FromRequest for RequestForm { + 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?)) + } +} + +// 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()) + } +} + +impl FromRequestHead for Session { + async fn from_request_head(head: &RequestHead) -> crate::Result { + Ok(Session::from_extensions(&head.extensions).clone()) + } +} + +impl FromRequestHead for Auth { + async fn from_request_head(head: &RequestHead) -> crate::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::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/src/admin.rs b/cot/src/admin.rs index e8b14794..c17d4ddb 100644 --- a/cot/src/admin.rs +++ b/cot/src/admin.rs @@ -10,6 +10,8 @@ use askama::Template; use async_trait::async_trait; use bytes::Bytes; use cot_core::error::NotFound; +use cot_core::request::extractors::{FromRequestHead, Path, StaticFiles, UrlQuery}; +use cot_core::request::{Request, RequestExt, RequestHead}; /// Implements the [`AdminModel`] trait for a struct. /// /// This is a simple method for adding a database model to the admin panel. @@ -27,8 +29,6 @@ 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; diff --git a/cot/src/auth.rs b/cot/src/auth.rs index 4fa9405e..a71626ea 100644 --- a/cot/src/auth.rs +++ b/cot/src/auth.rs @@ -17,6 +17,7 @@ use std::sync::{Arc, Mutex, MutexGuard}; use async_trait::async_trait; use chrono::{DateTime, FixedOffset}; use cot_core::error::error_impl::impl_into_cot_error; +use cot_core::request::{Request, RequestExt}; use derive_more::with_trait::Debug; #[cfg(test)] use mockall::automock; @@ -28,7 +29,6 @@ use thiserror::Error; use crate::config::SecretKey; #[cfg(feature = "db")] use crate::db::{ColumnType, DatabaseField, DbValue, FromDbValue, SqlxValueRef, ToDbValue}; -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/form.rs b/cot/src/form.rs index d72b1f76..7543f000 100644 --- a/cot/src/form.rs +++ b/cot/src/form.rs @@ -32,6 +32,7 @@ use bytes::Bytes; use chrono::NaiveDateTime; use chrono_tz::Tz; use cot_core::error::error_impl::impl_into_cot_error; +use cot_core::request::{Request, RequestExt}; /// Derive the [`Form`] trait for a struct and create a [`FormContext`] for it. /// /// This macro will generate an implementation of the [`Form`] trait for the @@ -63,7 +64,6 @@ use http_body_util::BodyExt; use thiserror::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. diff --git a/cot/src/handler.rs b/cot/src/handler.rs index be8f6423..ba16bb40 100644 --- a/cot/src/handler.rs +++ b/cot/src/handler.rs @@ -2,10 +2,10 @@ use std::future::Future; use std::marker::PhantomData; use std::pin::Pin; +use cot_core::request::Request; +use cot_core::request::extractors::{FromRequest, FromRequestHead}; use tower::util::BoxCloneSyncService; -use crate::request::Request; -use crate::request::extractors::{FromRequest, FromRequestHead}; use crate::response::{IntoResponse, Response}; use crate::{Error, Result}; diff --git a/cot/src/lib.rs b/cot/src/lib.rs index 6739213a..4eabe46c 100644 --- a/cot/src/lib.rs +++ b/cot/src/lib.rs @@ -159,7 +159,7 @@ pub use cot_macros::main; pub use cot_macros::test; #[cfg(feature = "openapi")] pub use schemars; -pub use {bytes, http}; +pub use {bytes, cot_core as core, http}; pub use crate::handler::{BoxedHandler, RequestHandler}; pub use crate::project::{ diff --git a/cot/src/project.rs b/cot/src/project.rs index 51c265ff..0e26503e 100644 --- a/cot/src/project.rs +++ b/cot/src/project.rs @@ -31,6 +31,7 @@ use axum::handler::HandlerWithoutStateExt; use cot_core::error::UncaughtPanic; use cot_core::error::error_impl::impl_into_cot_error; use cot_core::error::handler::{DynErrorPageHandler, RequestOuterError}; +use cot_core::request::{AppName, Request, RequestExt, RequestHead}; use derive_more::with_trait::Debug; use futures_util::FutureExt; use thiserror::Error; @@ -54,7 +55,6 @@ 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; @@ -1580,8 +1580,8 @@ impl>> ProjectContext { /// # Examples /// /// ``` - /// use cot::request::{Request, RequestExt}; /// use cot::response::Response; + /// use cot_core::request::{Request, RequestExt}; /// /// 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}; /// /// 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}; /// /// 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}; /// /// 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}; /// /// 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}; /// /// async fn index(request: Request) -> cot::Result { /// let database = request.context().database(); @@ -2138,6 +2138,7 @@ mod tests { use std::task::{Context, Poll}; use cot_core::error::handler::{RequestError, RequestOuterError}; + use cot_core::request::extractors::FromRequestHead; use tower::util::MapResultLayer; use tower::{ServiceExt, service_fn}; @@ -2146,7 +2147,6 @@ mod tests { use crate::auth::UserId; use crate::config::SecretKey; 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 c2bf892b..a3e753ae 100644 --- a/cot/src/request.rs +++ b/cot/src/request.rs @@ -1,863 +1 @@ -//! 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_core::error::error_impl::impl_into_cot_error; -use http::Extensions; -use indexmap::IndexMap; - -#[cfg(feature = "db")] -use crate::db::Database; -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; - -/// HTTP request head type. -pub type RequestHead = http::request::Parts; - -mod private { - pub trait Sealed {} -} - -/// Extension trait for [`http::Request`] that provides helper methods for -/// working with HTTP requests. -/// -/// # Sealed -/// -/// This trait is sealed since it doesn't make sense to be implemented for types -/// outside the context of Cot. -pub trait RequestExt: private::Sealed { - /// Runs an extractor implementing [`FromRequestHead`] on the request. - /// - /// # Examples - /// - /// ``` - /// use cot::request::extractors::Path; - /// use cot::request::{Request, RequestExt}; - /// use cot::response::Response; - /// - /// async fn my_handler(mut request: Request) -> cot::Result { - /// let path_params = request.extract_from_head::>().await?; - /// // ... - /// # unimplemented!() - /// } - /// ``` - fn extract_from_head(&mut self) -> impl Future> + Send - where - E: FromRequestHead + 'static; - - /// Get the application context. - /// - /// # Examples - /// - /// ``` - /// use cot::request::{Request, RequestExt}; - /// use cot::response::Response; - /// - /// async fn my_handler(mut request: Request) -> cot::Result { - /// let context = request.context(); - /// // ... do something with the context - /// # unimplemented!() - /// } - /// ``` - #[must_use] - fn context(&self) -> &crate::ProjectContext; - - /// Get the project configuration. - /// - /// # Examples - /// - /// ``` - /// use cot::request::{Request, RequestExt}; - /// use cot::response::Response; - /// - /// async fn my_handler(mut request: Request) -> cot::Result { - /// let config = request.project_config(); - /// // ... do something with the config - /// # unimplemented!() - /// } - /// ``` - #[must_use] - fn project_config(&self) -> &crate::config::ProjectConfig; - - /// Get the router. - /// - /// # Examples - /// - /// ``` - /// use cot::request::{Request, RequestExt}; - /// use cot::response::Response; - /// - /// async fn my_handler(mut request: Request) -> cot::Result { - /// let router = request.router(); - /// // ... do something with the router - /// # unimplemented!() - /// } - /// ``` - #[must_use] - fn router(&self) -> &Arc; - - /// Get the app name the current route belongs to, or [`None`] if the - /// request is not routed. - /// - /// This is mainly useful for providing context to reverse redirects, where - /// you want to redirect to a route in the same app. - /// - /// # Examples - /// - /// ``` - /// use cot::request::{Request, RequestExt}; - /// use cot::response::Response; - /// - /// async fn my_handler(mut request: Request) -> cot::Result { - /// let app_name = request.app_name(); - /// // ... do something with the app name - /// # unimplemented!() - /// } - /// ``` - fn app_name(&self) -> Option<&str>; - - /// Get the route name, or [`None`] if the request is not routed or doesn't - /// have a route name. - /// - /// This is mainly useful for use in templates, where you want to know which - /// route is being rendered, for instance to mark the active tab. - /// - /// # Examples - /// - /// ``` - /// use cot::request::{Request, RequestExt}; - /// use cot::response::Response; - /// - /// async fn my_handler(mut request: Request) -> cot::Result { - /// let route_name = request.route_name(); - /// // ... do something with the route name - /// # unimplemented!() - /// } - /// ``` - #[must_use] - fn route_name(&self) -> Option<&str>; - - /// Get the path parameters. - /// - /// # Examples - /// - /// ``` - /// use cot::request::{Request, RequestExt}; - /// use cot::response::Response; - /// - /// async fn my_handler(mut request: Request) -> cot::Result { - /// let path_params = request.path_params(); - /// // ... do something with the path params - /// # unimplemented!() - /// } - /// ``` - #[must_use] - fn path_params(&self) -> &PathParams; - - /// Get the path parameters mutably. - /// - /// # Examples - /// - /// ``` - /// use cot::request::{Request, RequestExt}; - /// use cot::response::Response; - /// - /// async fn my_handler(mut request: Request) -> cot::Result { - /// let path_params = request.path_params_mut(); - /// // ... do something with the path params - /// # unimplemented!() - /// } - /// ``` - #[must_use] - fn path_params_mut(&mut self) -> &mut PathParams; - - /// Get the database. - /// - /// # Examples - /// - /// ``` - /// use cot::request::{Request, RequestExt}; - /// use cot::response::Response; - /// - /// async fn my_handler(mut request: Request) -> cot::Result { - /// let db = request.db(); - /// // ... do something with the database - /// # unimplemented!() - /// } - /// ``` - #[cfg(feature = "db")] - #[must_use] - fn db(&self) -> &Arc; - - /// Get the content type of the request. - /// - /// # Examples - /// - /// ``` - /// use cot::request::{Request, RequestExt}; - /// use cot::response::Response; - /// - /// async fn my_handler(mut request: Request) -> cot::Result { - /// let content_type = request.content_type(); - /// // ... do something with the content type - /// # unimplemented!() - /// } - /// ``` - #[must_use] - fn content_type(&self) -> Option<&http::HeaderValue>; - - /// Expect the content type of the request to be the given value. - /// - /// # Errors - /// - /// Throws an error if the content type is not the expected value. - /// - /// # Examples - /// - /// ``` - /// use cot::request::{Request, RequestExt}; - /// use cot::response::Response; - /// - /// async fn my_handler(mut request: Request) -> cot::Result { - /// request.expect_content_type("application/json")?; - /// // ... - /// # unimplemented!() - /// } - /// ``` - fn expect_content_type(&mut self, expected: &'static str) -> Result<()> { - let content_type = self - .content_type() - .map_or("".into(), |value| String::from_utf8_lossy(value.as_bytes())); - if content_type == expected { - Ok(()) - } else { - Err(InvalidContentType { - expected, - actual: content_type.into_owned(), - } - .into()) - } - } - - #[doc(hidden)] - fn extensions(&self) -> &Extensions; -} - -impl private::Sealed for Request {} - -impl RequestExt for Request { - async fn extract_from_head(&mut self) -> Result - where - E: FromRequestHead + 'static, - { - let request = std::mem::take(self); - - let (head, body) = request.into_parts(); - let result = E::from_request_head(&head).await; - - *self = Request::from_parts(head, body); - result - } - - #[track_caller] - fn context(&self) -> &crate::ProjectContext { - self.extensions() - .get::>() - .expect("AppContext extension missing") - } - - fn project_config(&self) -> &crate::config::ProjectConfig { - self.context().config() - } - - fn router(&self) -> &Arc { - self.context().router() - } - - fn app_name(&self) -> Option<&str> { - self.extensions() - .get::() - .map(|AppName(name)| name.as_str()) - } - - fn route_name(&self) -> Option<&str> { - self.extensions() - .get::() - .map(|RouteName(name)| name.as_str()) - } - - #[track_caller] - fn path_params(&self) -> &PathParams { - self.extensions() - .get::() - .expect("PathParams extension missing") - } - - fn path_params_mut(&mut self) -> &mut PathParams { - self.extensions_mut().get_or_insert_default::() - } - - #[cfg(feature = "db")] - fn db(&self) -> &Arc { - self.context().database() - } - - fn content_type(&self) -> Option<&http::HeaderValue> { - self.headers().get(http::header::CONTENT_TYPE) - } - - fn extensions(&self) -> &Extensions { - self.extensions() - } -} - -impl private::Sealed for RequestHead {} - -impl RequestExt for RequestHead { - async fn extract_from_head(&mut self) -> Result - where - E: FromRequestHead + 'static, - { - E::from_request_head(self).await - } - - fn context(&self) -> &crate::ProjectContext { - self.extensions - .get::>() - .expect("AppContext extension missing") - } - - fn project_config(&self) -> &crate::config::ProjectConfig { - self.context().config() - } - - fn router(&self) -> &Arc { - self.context().router() - } - - fn app_name(&self) -> Option<&str> { - self.extensions - .get::() - .map(|AppName(name)| name.as_str()) - } - - fn route_name(&self) -> Option<&str> { - self.extensions - .get::() - .map(|RouteName(name)| name.as_str()) - } - - fn path_params(&self) -> &PathParams { - self.extensions - .get::() - .expect("PathParams extension missing") - } - - fn path_params_mut(&mut self) -> &mut PathParams { - self.extensions.get_or_insert_default::() - } - - #[cfg(feature = "db")] - fn db(&self) -> &Arc { - self.context().database() - } - - fn content_type(&self) -> Option<&http::HeaderValue> { - self.headers.get(http::header::CONTENT_TYPE) - } - - fn extensions(&self) -> &Extensions { - &self.extensions - } -} - -#[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); - } - - #[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(), - } - ); - } - - #[test] - fn request_ext_app_name() { - let mut request = TestRequestBuilder::get("/").build(); - assert_eq!(request.app_name(), None); - - request - .extensions_mut() - .insert(AppName("test_app".to_string())); - assert_eq!(request.app_name(), Some("test_app")); - } - - #[test] - fn request_ext_route_name() { - let mut request = TestRequestBuilder::get("/").build(); - assert_eq!(request.route_name(), None); - - request - .extensions_mut() - .insert(RouteName("test_route".to_string())); - assert_eq!(request.route_name(), Some("test_route")); - } - - #[test] - fn request_ext_parts_route_name() { - let request = TestRequestBuilder::get("/").build(); - let (mut head, _body) = request.into_parts(); - assert_eq!(head.route_name(), None); - - head.extensions.insert(RouteName("test_route".to_string())); - assert_eq!(head.route_name(), Some("test_route")); - } - - #[test] - fn request_ext_path_params() { - let mut request = TestRequestBuilder::get("/").build(); - - let mut params = PathParams::new(); - params.insert("id".to_string(), "42".to_string()); - request.extensions_mut().insert(params); - - assert_eq!(request.path_params().get("id"), Some("42")); - } - - #[test] - fn request_ext_path_params_mut() { - let mut request = TestRequestBuilder::get("/").build(); - - request - .path_params_mut() - .insert("id".to_string(), "42".to_string()); - - assert_eq!(request.path_params().get("id"), Some("42")); - } - - #[test] - fn request_ext_content_type() { - let mut request = TestRequestBuilder::get("/").build(); - assert_eq!(request.content_type(), None); - - request.headers_mut().insert( - http::header::CONTENT_TYPE, - http::HeaderValue::from_static("text/plain"), - ); - - assert_eq!( - request.content_type(), - Some(&http::HeaderValue::from_static("text/plain")) - ); - } - - #[test] - fn request_ext_expect_content_type() { - let mut request = TestRequestBuilder::get("/").build(); - - // Should fail with no content type - assert!(request.expect_content_type("text/plain").is_err()); - - request.headers_mut().insert( - http::header::CONTENT_TYPE, - http::HeaderValue::from_static("text/plain"), - ); - - // Should succeed with matching content type - assert!(request.expect_content_type("text/plain").is_ok()); - - // Should fail with non-matching content type - assert!(request.expect_content_type("application/json").is_err()); - } - - #[cot::test] - async fn request_ext_extract_from_head() { - async fn handler(mut request: Request) -> Result { - let Path(id): Path = request.extract_from_head().await?; - assert_eq!(id, "42"); - - Ok(Response::new(Body::empty())) - } - - let router = Router::with_urls([Route::with_handler("/{id}/", handler)]); - - let request = TestRequestBuilder::get("/42/") - .router(router.clone()) - .build(); - - router.handle(request).await.unwrap(); - } - - #[test] - fn parts_ext_path_params() { - 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); - - assert_eq!(head.path_params().get("id"), Some("42")); - } - - #[test] - fn parts_ext_mutating_path_params() { - let (mut head, _) = Request::new(Body::empty()).into_parts(); - head.path_params_mut() - .insert("page".to_string(), "1".to_string()); - - assert_eq!(head.path_params().get("page"), Some("1")); - } - - #[test] - fn parts_ext_app_name() { - let (mut head, _) = Request::new(Body::empty()).into_parts(); - head.extensions.insert(AppName("test_app".to_string())); - - assert_eq!(head.app_name(), Some("test_app")); - } - - #[test] - fn parts_ext_route_name() { - let (mut head, _) = Request::new(Body::empty()).into_parts(); - head.extensions.insert(RouteName("test_route".to_string())); - - assert_eq!(head.route_name(), Some("test_route")); - } - - #[test] - fn parts_ext_content_type() { - let (mut head, _) = Request::new(Body::empty()).into_parts(); - head.headers.insert( - http::header::CONTENT_TYPE, - http::HeaderValue::from_static("text/plain"), - ); - - assert_eq!( - head.content_type(), - 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 08f82311..907f10a3 100644 --- a/cot/src/request/extractors.rs +++ b/cot/src/request/extractors.rs @@ -1,291 +1,23 @@ -//! 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 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}; - -/// 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) -> cot::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; -} - -impl FromRequestHead for Urls { - async fn from_request_head(head: &RequestHead) -> cot::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 - .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)) - } -} +use cot::json::Json; +use cot_core::request::RequestExt; +use cot_core::request::extractors::{FromRequest, FromRequestHead, RequestHead}; +use cot_core::{Body, impl_into_cot_error}; +use serde::de::DeserializeOwned; #[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); +#[error("invalid content type; expected `{expected}`, found `{actual}`")] +pub struct InvalidContentType { + expected: &'static str, + actual: String, +} +impl_into_cot_error!(InvalidContentType, 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 { + async fn from_request(head: &RequestHead, body: Body) -> crate::Result { let content_type = head .headers .get(http::header::CONTENT_TYPE) @@ -315,61 +47,6 @@ 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`. -/// -/// The content type of the request must be `application/x-www-form-urlencoded`. -/// -/// # Errors -/// -/// Throws an error if the content type is not -/// `application/x-www-form-urlencoded`. 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 form data is invalid or because the -/// deserialization to the target structure failed. -/// -/// # Example -/// -/// ``` -/// use cot::form::{Form, FormResult}; -/// use cot::html::Html; -/// use cot::request::extractors::RequestForm; -/// use cot::test::TestRequestBuilder; -/// -/// #[derive(Form)] -/// struct MyForm { -/// hello: String, -/// } -/// -/// async fn my_handler(RequestForm(form): RequestForm) -> Html { -/// let form = match form { -/// FormResult::Ok(form) => form, -/// FormResult::ValidationError(error) => { -/// panic!("Form validation error!") -/// } -/// }; -/// -/// Html::new(format!("Hello {}!", form.hello)) -/// } -/// -/// # #[tokio::main] -/// # async fn main() -> cot::Result<()> { -/// # use cot::RequestHandler; -/// # let request = TestRequestBuilder::post("/").form_data(&[("hello", "world")]).build(); -/// # my_handler.handle(request).await?; -/// # Ok(()) -/// # } -/// ``` -#[derive(Debug)] -pub struct RequestForm(pub FormResult); - -impl FromRequest for RequestForm { - async fn from_request(head: &RequestHead, body: Body) -> cot::Result { - let mut request = Request::from_parts(head.clone(), body); - Ok(Self(F::from_request(&mut request).await?)) - } -} - /// An extractor that gets the database from the request extensions. /// /// # Example @@ -394,13 +71,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())) } } @@ -412,9 +90,9 @@ impl FromRequestHead for RequestDb { /// /// ``` /// use cot::html::Html; -/// use cot::request::Request; /// use cot::request::extractors::StaticFiles; /// use cot::test::TestRequestBuilder; +/// 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 +114,7 @@ impl FromRequestHead for RequestDb { /// ``` #[derive(Debug, Clone, PartialEq, Eq)] pub struct StaticFiles { - inner: Arc, + inner: Arc, } impl StaticFiles { @@ -504,97 +182,28 @@ 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) - } -} - -use cot_core::error::error_impl::impl_into_cot_error; -/// 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; - #[cfg(test)] mod tests { + use cot::request::extractors::Json; + use cot::test::TestRequestBuilder; + use cot_core::request::extractors::{FromRequest, Path, UrlQuery}; 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) @@ -608,7 +217,7 @@ mod tests { } #[cfg(feature = "json")] - #[cot::test] + #[cot_macros::test] async fn json_empty() { #[derive(Debug, Deserialize, PartialEq, Eq)] struct TestData {} @@ -625,7 +234,7 @@ mod tests { } #[cfg(feature = "json")] - #[cot::test] + #[cot_macros::test] async fn json_struct() { #[derive(Debug, Deserialize, PartialEq, Eq)] struct TestDataInner { @@ -655,62 +264,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) @@ -723,67 +278,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/router.rs b/cot/src/router.rs index a681aa06..f947e3c0 100644 --- a/cot/src/router.rs +++ b/cot/src/router.rs @@ -3,9 +3,9 @@ //! # Examples //! //! ``` -//! use cot::request::Request; //! use cot::response::Response; //! use cot::router::{Route, Router}; +//! use cot_core::request::Request; //! //! async fn home(request: Request) -> cot::Result { //! Ok(cot::reverse_redirect!(request, "get_page", page = 123)?) @@ -50,9 +50,9 @@ pub mod path; /// # Examples /// /// ``` -/// use cot::request::Request; /// use cot::response::Response; /// use cot::router::{Route, Router}; +/// use cot_core::request::Request; /// /// async fn home(request: Request) -> cot::Result { /// unimplemented!() @@ -89,9 +89,9 @@ impl Router { /// # Examples /// /// ``` - /// use cot::request::Request; /// use cot::response::Response; /// use cot::router::{Route, Router}; + /// use cot_core::request::Request; /// /// async fn home(request: Request) -> cot::Result { /// unimplemented!() @@ -299,9 +299,9 @@ impl Router { /// # Examples /// /// ``` - /// use cot::request::Request; /// use cot::response::Response; /// use cot::router::{Route, Router}; + /// use cot_core::request::Request; /// /// async fn home(request: Request) -> cot::Result { /// unimplemented!() @@ -320,9 +320,9 @@ impl Router { /// # Examples /// /// ``` - /// use cot::request::Request; /// use cot::response::Response; /// use cot::router::{Route, Router}; + /// use cot_core::request::Request; /// /// async fn home(request: Request) -> cot::Result { /// unimplemented!() @@ -477,9 +477,9 @@ impl RouterService { /// ``` /// use std::sync::Arc; /// - /// use cot::request::Request; /// use cot::response::Response; /// use cot::router::{Route, Router, RouterService}; + /// use cot_core::request::Request; /// /// async fn home(request: Request) -> cot::Result { /// unimplemented!() @@ -528,9 +528,9 @@ pub fn split_view_name(view_name: &str) -> (Option<&str>, &str) { /// # Examples /// /// ``` -/// use cot::request::Request; /// use cot::response::Response; /// use cot::router::{Route, Router}; +/// use cot_core::request::Request; /// /// async fn home(request: Request) -> cot::Result { /// unimplemented!() @@ -551,9 +551,9 @@ impl Route { /// # Examples /// /// ``` - /// use cot::request::Request; /// use cot::response::Response; /// use cot::router::{Route, Router}; + /// use cot_core::request::Request; /// /// async fn home(request: Request) -> cot::Result { /// // ... @@ -584,10 +584,10 @@ impl Route { /// # Examples /// /// ``` - /// use cot::request::Request; /// use cot::response::Response; /// use cot::router::method::openapi::api_get; /// use cot::router::{Route, Router}; + /// use cot_core::request::Request; /// /// async fn home(request: Request) -> cot::Result { /// // ... @@ -617,10 +617,10 @@ impl Route { /// # Examples /// /// ``` - /// use cot::request::Request; /// use cot::response::Response; /// use cot::router::method::openapi::api_get; /// use cot::router::{Route, Router}; + /// use cot_core::request::Request; /// /// async fn home(request: Request) -> cot::Result { /// // ... @@ -652,10 +652,10 @@ impl Route { /// # Examples /// /// ``` - /// use cot::request::Request; /// use cot::response::Response; /// use cot::router::method::openapi::api_post; /// use cot::router::{Route, Router}; + /// use cot_core::request::Request; /// /// async fn home(request: Request) -> cot::Result { /// // ... @@ -686,9 +686,9 @@ impl Route { /// # Examples /// /// ``` - /// use cot::request::Request; /// use cot::response::Response; /// use cot::router::{Route, Router}; + /// use cot_core::request::Request; /// /// async fn home(request: Request) -> cot::Result { /// unimplemented!() @@ -711,9 +711,9 @@ impl Route { /// # Examples /// /// ``` - /// use cot::request::Request; /// use cot::response::Response; /// use cot::router::{Route, Router}; + /// use cot_core::request::Request; /// /// async fn home(request: Request) -> cot::Result { /// unimplemented!() @@ -733,9 +733,9 @@ impl Route { /// # Examples /// /// ``` - /// use cot::request::Request; /// use cot::response::Response; /// use cot::router::{Route, Router}; + /// use cot_core::request::Request; /// /// async fn home(request: Request) -> cot::Result { /// unimplemented!() @@ -802,9 +802,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; /// /// async fn home(request: Request) -> cot::Result { /// // any of below two lines returns the same: @@ -904,10 +904,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; /// /// async fn my_handler(request: Request) -> cot::Result { /// let urls = Urls::from_request(&request); @@ -941,9 +941,9 @@ impl Urls { /// # Examples /// /// ``` - /// use cot::request::{Request, RequestExt}; /// use cot::response::Response; /// use cot::router::Urls; + /// use cot_core::request::{Request, RequestExt}; /// /// async fn my_handler(urls: Urls) -> cot::Result { /// let app_name = urls.app_name(); @@ -961,9 +961,9 @@ impl Urls { /// # Examples /// /// ``` - /// use cot::request::{Request, RequestExt}; /// use cot::response::Response; /// use cot::router::Urls; + /// use cot_core::request::{Request, RequestExt}; /// /// async fn my_handler(urls: Urls) -> cot::Result { /// let router = urls.router(); @@ -1005,10 +1005,10 @@ impl Debug for RouteInner { /// # Examples /// /// ``` -/// use cot::request::Request; /// use cot::response::Response; /// use cot::reverse_redirect; /// use cot::router::{Route, Router}; +/// use cot_core::request::Request; /// /// async fn infinite_loop(request: Request) -> cot::Result { /// Ok(reverse_redirect!(request, "home")?) diff --git a/cot/tests/auth.rs b/cot/tests/auth.rs index 12bacb30..3e40b0f1 100644 --- a/cot/tests/auth.rs +++ b/cot/tests/auth.rs @@ -3,8 +3,8 @@ use std::borrow::Cow; use cot::auth::Auth; use cot::auth::db::{DatabaseUser, DatabaseUserCredentials}; use cot::common_types::Password; -use cot::request::RequestExt; use cot::test::{TestDatabase, TestRequestBuilder}; +use cot_core::request::RequestExt; #[cot_macros::dbtest] async fn database_user(test_db: &mut TestDatabase) { 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..905e2c90 100644 --- a/cot/tests/openapi.rs +++ b/cot/tests/openapi.rs @@ -2,12 +2,12 @@ use aide::openapi::{Parameter, PathItem, ReferenceOr}; use cot::html::Html; use cot::json::Json; 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}; +use cot_core::request::extractors::{Path, UrlQuery}; use schemars::SchemaGenerator; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; diff --git a/cot/tests/project.rs b/cot/tests/project.rs index ba96670c..90cbdbf2 100644 --- a/cot/tests/project.rs +++ b/cot/tests/project.rs @@ -2,10 +2,10 @@ 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::request::Request; #[cot::test] #[cfg_attr( diff --git a/cot/tests/router.rs b/cot/tests/router.rs index cd598cf4..3c266004 100644 --- a/cot/tests/router.rs +++ b/cot/tests/router.rs @@ -2,10 +2,10 @@ use bytes::Bytes; use cot::config::ProjectConfig; use cot::html::Html; use cot::project::RegisterAppsContext; -use cot::request::{Request, RequestExt}; use cot::router::{Route, Router}; use cot::test::Client; use cot::{App, AppBuilder, Project, StatusCode}; +use cot_core::request::{Request, RequestExt}; async fn index() -> Html { Html::new("Hello world!") diff --git a/examples/file-upload/src/main.rs b/examples/file-upload/src/main.rs index 6c6f9164..587ec3e0 100644 --- a/examples/file-upload/src/main.rs +++ b/examples/file-upload/src/main.rs @@ -2,11 +2,11 @@ use askama::Template; use base64::Engine; use cot::cli::CliMetadata; use cot::config::ProjectConfig; +use cot::core::request::extractors::RequestForm; use cot::form::fields::InMemoryUploadedFile; use cot::form::{Form, FormContext}; use cot::html::Html; use cot::project::{MiddlewareContext, RegisterAppsContext, RootHandler}; -use cot::request::extractors::RequestForm; use cot::router::{Route, Router, Urls}; use cot::static_files::StaticFilesMiddleware; use cot::{App, AppBuilder, Project}; diff --git a/examples/forms/src/main.rs b/examples/forms/src/main.rs index fa63c7a5..6988432e 100644 --- a/examples/forms/src/main.rs +++ b/examples/forms/src/main.rs @@ -5,6 +5,8 @@ use chrono::{DateTime, Duration, FixedOffset, NaiveDate, NaiveDateTime, NaiveTim use chrono_tz::Tz; use cot::cli::CliMetadata; use cot::config::ProjectConfig; +use cot::core::request::Request; +use cot::core::request::extractors::RequestForm; use cot::db::migrations::SyncDynMigration; use cot::db::{Auto, Model, model}; use cot::form::Form; @@ -12,8 +14,7 @@ use cot::form::fields::Step; use cot::html::Html; use cot::middleware::{AuthMiddleware, LiveReloadMiddleware, SessionMiddleware}; use cot::project::{MiddlewareContext, RegisterAppsContext, RootHandler, RootHandlerBuilder}; -use cot::request::Request; -use cot::request::extractors::{RequestDb, RequestForm, StaticFiles}; +use cot::request::extractors::{RequestDb, StaticFiles}; use cot::response::Response; use cot::router::{Route, Router, Urls}; use cot::static_files::{StaticFile, StaticFilesMiddleware}; diff --git a/examples/sessions/src/main.rs b/examples/sessions/src/main.rs index 6e69cea3..7aea2fa0 100644 --- a/examples/sessions/src/main.rs +++ b/examples/sessions/src/main.rs @@ -4,11 +4,11 @@ use cot::config::{ DatabaseConfig, MiddlewareConfig, ProjectConfig, SessionMiddlewareConfig, SessionStoreConfig, SessionStoreTypeConfig, }; +use cot::core::request::Request; use cot::form::Form; use cot::html::Html; use cot::middleware::SessionMiddleware; use cot::project::{MiddlewareContext, RegisterAppsContext, RootHandler}; -use cot::request::Request; use cot::response::{IntoResponse, Response}; use cot::router::{Route, Router, Urls}; use cot::session::Session; diff --git a/examples/todo-list/src/main.rs b/examples/todo-list/src/main.rs index 28837978..a156f090 100644 --- a/examples/todo-list/src/main.rs +++ b/examples/todo-list/src/main.rs @@ -4,12 +4,13 @@ use askama::Template; use cot::auth::db::DatabaseUserApp; use cot::cli::CliMetadata; use cot::config::{DatabaseConfig, ProjectConfig}; +use cot::core::request::extractors::{Path, RequestForm}; use cot::db::migrations::SyncDynMigration; use cot::db::{Auto, Model, model, query}; use cot::form::Form; use cot::html::Html; use cot::project::{MiddlewareContext, RegisterAppsContext, RootHandler}; -use cot::request::extractors::{Path, RequestDb, RequestForm}; +use cot::request::extractors::RequestDb; use cot::response::Response; use cot::router::{Route, Router, Urls}; use cot::static_files::StaticFilesMiddleware; From fc5e71b51557180ca4d5dc061258220c1375e7c8 Mon Sep 17 00:00:00 2001 From: Marek 'seqre' Grzelak Date: Tue, 7 Oct 2025 20:27:50 +0200 Subject: [PATCH 05/18] move responses and headers --- cot-core/src/headers.rs | 7 + cot-core/src/lib.rs | 2 + cot-core/src/response.rs | 192 +++++++ cot-core/src/response/into_response.rs | 724 +++++++++++++++++++++++ cot/src/admin.rs | 2 +- cot/src/error_page.rs | 2 +- cot/src/form.rs | 5 +- cot/src/handler.rs | 2 +- cot/src/headers.rs | 7 - cot/src/lib.rs | 3 +- cot/src/middleware.rs | 10 +- cot/src/project.rs | 16 +- cot/src/request/extractors.rs | 19 +- cot/src/response.rs | 191 ------ cot/src/response/into_response.rs | 734 +----------------------- cot/src/router.rs | 41 +- cot/src/router/method.rs | 4 +- cot/src/router/method/openapi.rs | 4 +- cot/src/static_files.rs | 2 +- cot/src/test.rs | 4 +- cot/tests/openapi.rs | 2 +- examples/custom-error-pages/src/main.rs | 2 +- examples/forms/src/main.rs | 2 +- examples/json/src/main.rs | 2 +- examples/sessions/src/main.rs | 2 +- examples/todo-list/src/main.rs | 2 +- 26 files changed, 1000 insertions(+), 983 deletions(-) create mode 100644 cot-core/src/headers.rs create mode 100644 cot-core/src/response.rs create mode 100644 cot-core/src/response/into_response.rs delete mode 100644 cot/src/headers.rs 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-core/src/lib.rs b/cot-core/src/lib.rs index 09870d5f..7e552f66 100644 --- a/cot-core/src/lib.rs +++ b/cot-core/src/lib.rs @@ -7,7 +7,9 @@ pub mod body; /// 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; /// A type alias for a result that can return a [`cot_core::Error`]. pub type Result = std::result::Result; 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..9c38e03b --- /dev/null +++ b/cot-core/src/response/into_response.rs @@ -0,0 +1,724 @@ +use bytes::{Bytes, BytesMut}; +use cot::html::Html; +use http; + +use crate::headers::{HTML_CONTENT_TYPE, OCTET_STREAM_CONTENT_TYPE, PLAIN_TEXT_CONTENT_TYPE}; +use crate::response::Response; +use crate::{Body, Error, StatusCode, impl_into_cot_error}; + +/// 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) -> cot::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())) + } +} + +// 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_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() + } +} + +// Shortcuts for common uses + +impl IntoResponse for Body { + fn into_response(self) -> crate::Result { + Ok(Response::new(self)) + } +} + +#[cfg(test)] +mod tests { + use bytes::{Bytes, BytesMut}; + use cot::html::Html; + 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/src/admin.rs b/cot/src/admin.rs index c17d4ddb..d3c1ee0f 100644 --- a/cot/src/admin.rs +++ b/cot/src/admin.rs @@ -12,6 +12,7 @@ use bytes::Bytes; use cot_core::error::NotFound; use cot_core::request::extractors::{FromRequestHead, Path, StaticFiles, UrlQuery}; use cot_core::request::{Request, RequestExt, RequestHead}; +use cot_core::response::{IntoResponse, Response}; /// Implements the [`AdminModel`] trait for a struct. /// /// This is a simple method for adding a database model to the admin panel. @@ -29,7 +30,6 @@ use crate::form::{ Form, FormContext, FormErrorTarget, FormField, FormFieldValidationError, FormResult, }; use crate::html::Html; -use crate::response::{IntoResponse, Response}; use crate::router::{Router, Urls}; use crate::static_files::StaticFile; use crate::{App, Error, Method, RequestHandler, reverse_redirect}; diff --git a/cot/src/error_page.rs b/cot/src/error_page.rs index 3c26b493..e9a897fe 100644 --- a/cot/src/error_page.rs +++ b/cot/src/error_page.rs @@ -327,7 +327,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()), diff --git a/cot/src/form.rs b/cot/src/form.rs index 7543f000..6cb3340d 100644 --- a/cot/src/form.rs +++ b/cot/src/form.rs @@ -32,6 +32,7 @@ use bytes::Bytes; use chrono::NaiveDateTime; use chrono_tz::Tz; use cot_core::error::error_impl::impl_into_cot_error; +use cot_core::headers::{MULTIPART_FORM_CONTENT_TYPE, URLENCODED_FORM_CONTENT_TYPE}; use cot_core::request::{Request, RequestExt}; /// Derive the [`Form`] trait for a struct and create a [`FormContext`] for it. /// @@ -63,8 +64,6 @@ pub use field_value::{FormFieldValue, FormFieldValueError}; use http_body_util::BodyExt; use thiserror::Error; -use crate::headers::{MULTIPART_FORM_CONTENT_TYPE, URLENCODED_FORM_CONTENT_TYPE}; - 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/handler.rs b/cot/src/handler.rs index ba16bb40..e428191f 100644 --- a/cot/src/handler.rs +++ b/cot/src/handler.rs @@ -4,9 +4,9 @@ use std::pin::Pin; use cot_core::request::Request; use cot_core::request::extractors::{FromRequest, FromRequestHead}; +use cot_core::response::{IntoResponse, Response}; use tower::util::BoxCloneSyncService; -use crate::response::{IntoResponse, Response}; use crate::{Error, Result}; /// A function that takes a request and returns a response. 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 4eabe46c..5ea8c263 100644 --- a/cot/src/lib.rs +++ b/cot/src/lib.rs @@ -56,7 +56,6 @@ extern crate self as cot; #[cfg(feature = "db")] pub mod db; pub mod form; -mod headers; // Not public API. Referenced by macro-generated code. #[doc(hidden)] #[path = "private.rs"] @@ -77,7 +76,7 @@ pub mod middleware; pub mod openapi; pub mod project; pub mod request; -pub mod response; +mod response; pub mod router; mod serializers; pub mod session; diff --git a/cot/src/middleware.rs b/cot/src/middleware.rs index 5ce0427f..27cd215e 100644 --- a/cot/src/middleware.rs +++ b/cot/src/middleware.rs @@ -10,6 +10,8 @@ use std::sync::Arc; use std::task::{Context, Poll}; use bytes::Bytes; +use cot_core::request::Request; +use cot_core::response::Response; use futures_core::future::BoxFuture; use futures_util::TryFutureExt; use http_body_util::BodyExt; @@ -22,8 +24,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; @@ -41,7 +41,7 @@ mod live_reload; pub use live_reload::LiveReloadMiddleware; /// Middleware that converts a any [`http::Response`] generic type to a -/// [`cot::response::Response`]. +/// [`cot_core::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 @@ -104,7 +104,7 @@ impl tower::Layer for IntoCotResponseLayer { } /// Service struct that converts any [`http::Response`] generic type to -/// [`cot::response::Response`]. +/// [`cot_core::response::Response`]. /// /// Used by [`IntoCotResponseLayer`]. /// @@ -722,6 +722,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 +734,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/project.rs b/cot/src/project.rs index 0e26503e..384d3268 100644 --- a/cot/src/project.rs +++ b/cot/src/project.rs @@ -32,6 +32,7 @@ use cot_core::error::UncaughtPanic; use cot_core::error::error_impl::impl_into_cot_error; use cot_core::error::handler::{DynErrorPageHandler, RequestOuterError}; use cot_core::request::{AppName, Request, RequestExt, RequestHead}; +use cot_core::response::{IntoResponse, Response}; use derive_more::with_trait::Debug; use futures_util::FutureExt; use thiserror::Error; @@ -55,7 +56,6 @@ use crate::error_page::Diagnostics; use crate::handler::BoxedHandler; use crate::html::Html; use crate::middleware::{IntoCotError, IntoCotErrorLayer, IntoCotResponse, IntoCotResponseLayer}; -use crate::response::{IntoResponse, Response}; use crate::router::{Route, Router, RouterService}; use crate::static_files::StaticFile; use crate::utils::accept_header_parser::AcceptHeaderParser; @@ -408,8 +408,8 @@ pub trait Project { /// ``` /// use cot::Project; /// use cot::html::Html; - /// use cot::response::IntoResponse; /// use cot_core::error::handler::{DynErrorPageHandler, RequestError}; + /// use cot_core::response::IntoResponse; /// /// struct MyProject; /// impl Project for MyProject { @@ -1580,8 +1580,8 @@ impl>> ProjectContext { /// # Examples /// /// ``` - /// 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::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::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::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::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::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(); diff --git a/cot/src/request/extractors.rs b/cot/src/request/extractors.rs index 907f10a3..851ec049 100644 --- a/cot/src/request/extractors.rs +++ b/cot/src/request/extractors.rs @@ -22,9 +22,9 @@ impl FromRequest for Json { .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 { + if content_type != cot_core::headers::JSON_CONTENT_TYPE { return Err(InvalidContentType { - expected: cot::headers::JSON_CONTENT_TYPE, + expected: cot_core::headers::JSON_CONTENT_TYPE, actual: content_type.into_owned(), } .into()); @@ -207,7 +207,10 @@ mod tests { 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(); @@ -224,7 +227,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("{}")) .unwrap(); @@ -248,7 +254,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(); diff --git a/cot/src/response.rs b/cot/src/response.rs index d5fbb812..63fb6c45 100644 --- a/cot/src/response.rs +++ b/cot/src/response.rs @@ -1,192 +1 @@ -//! 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, -}; - -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 cot_core::body::BodyInner; - - use super::*; - 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 4666fe26..2b173424 100644 --- a/cot/src/response/into_response.rs +++ b/cot/src/response/into_response.rs @@ -1,341 +1,7 @@ -use bytes::{Bytes, BytesMut}; -use cot::headers::{HTML_CONTENT_TYPE, OCTET_STREAM_CONTENT_TYPE, PLAIN_TEXT_CONTENT_TYPE}; -use cot::response::Response; -use cot::{Body, Error, StatusCode}; -use cot_core::error::error_impl::impl_into_cot_error; -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 cot::core::headers::JSON_CONTENT_TYPE; +use cot::core::impl_into_cot_error; +use cot::core::response::{IntoResponse, Response}; #[cfg(feature = "json")] impl IntoResponse for cot::json::Json { @@ -350,14 +16,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 +43,12 @@ 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 cot_core::error::NotFound; - use http::{self, HeaderMap, HeaderValue}; - - use super::*; - 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"); - } + use cot_core::StatusCode; #[cfg(feature = "json")] - #[cot::test] + #[cot_macros::test] async fn test_json_struct_into_response() { use serde::Serialize; @@ -796,7 +78,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 f947e3c0..1fb0dc02 100644 --- a/cot/src/router.rs +++ b/cot/src/router.rs @@ -3,9 +3,9 @@ //! # Examples //! //! ``` -//! use cot::response::Response; //! use cot::router::{Route, Router}; //! use cot_core::request::Request; +//! use cot_core::response::Response; //! //! async fn home(request: Request) -> cot::Result { //! Ok(cot::reverse_redirect!(request, "get_page", page = 123)?) @@ -29,12 +29,12 @@ use std::task::{Context, Poll}; use cot_core::error::NotFound; use cot_core::error::error_impl::impl_into_cot_error; +use cot_core::response::Response; use derive_more::with_trait::Debug; use tracing::debug; 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}; @@ -50,9 +50,9 @@ pub mod path; /// # Examples /// /// ``` -/// use cot::response::Response; /// use cot::router::{Route, Router}; /// use cot_core::request::Request; +/// use cot_core::response::Response; /// /// async fn home(request: Request) -> cot::Result { /// unimplemented!() @@ -89,9 +89,9 @@ impl Router { /// # Examples /// /// ``` - /// use cot::response::Response; /// use cot::router::{Route, Router}; /// use cot_core::request::Request; + /// use cot_core::response::Response; /// /// async fn home(request: Request) -> cot::Result { /// unimplemented!() @@ -299,9 +299,9 @@ impl Router { /// # Examples /// /// ``` - /// use cot::response::Response; /// use cot::router::{Route, Router}; /// use cot_core::request::Request; + /// use cot_core::response::Response; /// /// async fn home(request: Request) -> cot::Result { /// unimplemented!() @@ -320,9 +320,9 @@ impl Router { /// # Examples /// /// ``` - /// use cot::response::Response; /// use cot::router::{Route, Router}; /// use cot_core::request::Request; + /// use cot_core::response::Response; /// /// async fn home(request: Request) -> cot::Result { /// unimplemented!() @@ -477,9 +477,9 @@ impl RouterService { /// ``` /// use std::sync::Arc; /// - /// use cot::response::Response; /// use cot::router::{Route, Router, RouterService}; /// use cot_core::request::Request; + /// use cot_core::response::Response; /// /// async fn home(request: Request) -> cot::Result { /// unimplemented!() @@ -528,9 +528,9 @@ pub fn split_view_name(view_name: &str) -> (Option<&str>, &str) { /// # Examples /// /// ``` -/// use cot::response::Response; /// use cot::router::{Route, Router}; /// use cot_core::request::Request; +/// use cot_core::response::Response; /// /// async fn home(request: Request) -> cot::Result { /// unimplemented!() @@ -551,9 +551,9 @@ impl Route { /// # Examples /// /// ``` - /// use cot::response::Response; /// use cot::router::{Route, Router}; /// use cot_core::request::Request; + /// use cot_core::response::Response; /// /// async fn home(request: Request) -> cot::Result { /// // ... @@ -584,10 +584,10 @@ impl Route { /// # Examples /// /// ``` - /// use cot::response::Response; /// use cot::router::method::openapi::api_get; /// use cot::router::{Route, Router}; /// use cot_core::request::Request; + /// use cot_core::response::Response; /// /// async fn home(request: Request) -> cot::Result { /// // ... @@ -617,10 +617,10 @@ impl Route { /// # Examples /// /// ``` - /// use cot::response::Response; /// use cot::router::method::openapi::api_get; /// use cot::router::{Route, Router}; /// use cot_core::request::Request; + /// use cot_core::response::Response; /// /// async fn home(request: Request) -> cot::Result { /// // ... @@ -652,10 +652,10 @@ impl Route { /// # Examples /// /// ``` - /// use cot::response::Response; /// use cot::router::method::openapi::api_post; /// use cot::router::{Route, Router}; /// use cot_core::request::Request; + /// use cot_core::response::Response; /// /// async fn home(request: Request) -> cot::Result { /// // ... @@ -686,9 +686,9 @@ impl Route { /// # Examples /// /// ``` - /// use cot::response::Response; /// use cot::router::{Route, Router}; /// use cot_core::request::Request; + /// use cot_core::response::Response; /// /// async fn home(request: Request) -> cot::Result { /// unimplemented!() @@ -711,9 +711,9 @@ impl Route { /// # Examples /// /// ``` - /// use cot::response::Response; /// use cot::router::{Route, Router}; /// use cot_core::request::Request; + /// use cot_core::response::Response; /// /// async fn home(request: Request) -> cot::Result { /// unimplemented!() @@ -733,9 +733,9 @@ impl Route { /// # Examples /// /// ``` - /// use cot::response::Response; /// use cot::router::{Route, Router}; /// use cot_core::request::Request; + /// use cot_core::response::Response; /// /// async fn home(request: Request) -> cot::Result { /// unimplemented!() @@ -904,10 +904,10 @@ impl Urls { /// /// ``` /// use cot::html::Html; - /// 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}; /// /// async fn my_handler(request: Request) -> cot::Result { /// let urls = Urls::from_request(&request); @@ -941,9 +941,9 @@ impl Urls { /// # Examples /// /// ``` - /// use cot::response::Response; /// use cot::router::Urls; /// use cot_core::request::{Request, RequestExt}; + /// use cot_core::response::Response; /// /// async fn my_handler(urls: Urls) -> cot::Result { /// let app_name = urls.app_name(); @@ -961,9 +961,9 @@ impl Urls { /// # Examples /// /// ``` - /// use cot::response::Response; /// use cot::router::Urls; /// use cot_core::request::{Request, RequestExt}; + /// use cot_core::response::Response; /// /// async fn my_handler(urls: Urls) -> cot::Result { /// let router = urls.router(); @@ -1005,10 +1005,10 @@ impl Debug for RouteInner { /// # Examples /// /// ``` -/// use cot::response::Response; /// use cot::reverse_redirect; /// use cot::router::{Route, Router}; /// use cot_core::request::Request; +/// use cot_core::response::Response; /// /// async fn infinite_loop(request: Request) -> cot::Result { /// Ok(reverse_redirect!(request, "home")?) @@ -1029,11 +1029,12 @@ macro_rules! reverse_redirect { #[cfg(test)] mod tests { + use cot_core::response::{IntoResponse, Response}; + use super::*; use crate::StatusCode; use crate::html::Html; use crate::request::Request; - use crate::response::{IntoResponse, Response}; use crate::test::TestRequestBuilder; struct MockHandler; diff --git a/cot/src/router/method.rs b/cot/src/router/method.rs index 94bd0a11..02050e30 100644 --- a/cot/src/router/method.rs +++ b/cot/src/router/method.rs @@ -6,10 +6,10 @@ pub mod openapi; use std::fmt::{Debug, Formatter}; use cot_core::error::MethodNotAllowed; +use cot_core::response::Response; use crate::handler::{BoxRequestHandler, 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. @@ -206,10 +206,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; /// /// async fn fallback_handler() -> impl IntoResponse { /// Html::new("Method Not Allowed").with_status(StatusCode::METHOD_NOT_ALLOWED) diff --git a/cot/src/router/method/openapi.rs b/cot/src/router/method/openapi.rs index bf51bb8e..8738a5fe 100644 --- a/cot/src/router/method/openapi.rs +++ b/cot/src/router/method/openapi.rs @@ -9,8 +9,8 @@ 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 cot_core::response::Response; use schemars::SchemaGenerator; use crate::RequestHandler; @@ -518,12 +518,12 @@ where #[cfg(test)] mod tests { use cot_core::error::MethodNotAllowed; + use cot_core::response::{IntoResponse, Response}; use super::*; 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}; diff --git a/cot/src/static_files.rs b/cot/src/static_files.rs index d9dfa27b..21040d78 100644 --- a/cot/src/static_files.rs +++ b/cot/src/static_files.rs @@ -13,6 +13,7 @@ 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}; @@ -23,7 +24,6 @@ use tower::Service; use crate::Body; use crate::config::{StaticFilesConfig, StaticFilesPathRewriteMode}; 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..402c3809 100644 --- a/cot/src/test.rs +++ b/cot/src/test.rs @@ -7,6 +7,7 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::sync::Arc; use async_trait::async_trait; +use cot_core::response::Response; use derive_more::Debug; use tokio::net::TcpListener; use tokio::sync::oneshot; @@ -26,7 +27,6 @@ use crate::db::migrations::{ 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}; @@ -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; /// /// async fn index(request: Request) -> cot::Result { /// unimplemented!() diff --git a/cot/tests/openapi.rs b/cot/tests/openapi.rs index 905e2c90..399d015a 100644 --- a/cot/tests/openapi.rs +++ b/cot/tests/openapi.rs @@ -2,12 +2,12 @@ use aide::openapi::{Parameter, PathItem, ReferenceOr}; use cot::html::Html; use cot::json::Json; use cot::openapi::{AsApiRoute, NoApi, RouteContext}; -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}; use cot_core::request::extractors::{Path, UrlQuery}; +use cot_core::response::{IntoResponse, Response}; use schemars::SchemaGenerator; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; diff --git a/examples/custom-error-pages/src/main.rs b/examples/custom-error-pages/src/main.rs index f7d3fbaf..fed37ee8 100644 --- a/examples/custom-error-pages/src/main.rs +++ b/examples/custom-error-pages/src/main.rs @@ -3,10 +3,10 @@ use cot::cli::CliMetadata; use cot::config::ProjectConfig; use cot::html::Html; use cot::project::RegisterAppsContext; -use cot::response::{IntoResponse, Response}; use cot::router::{Route, Router}; use cot::{App, AppBuilder, Project}; use cot_core::error::handler::{DynErrorPageHandler, RequestError}; +use cot_core::response::{IntoResponse, Response}; async fn return_hello() -> cot::Result { panic!() diff --git a/examples/forms/src/main.rs b/examples/forms/src/main.rs index 6988432e..96503267 100644 --- a/examples/forms/src/main.rs +++ b/examples/forms/src/main.rs @@ -15,10 +15,10 @@ use cot::html::Html; use cot::middleware::{AuthMiddleware, LiveReloadMiddleware, SessionMiddleware}; use cot::project::{MiddlewareContext, RegisterAppsContext, RootHandler, RootHandlerBuilder}; use cot::request::extractors::{RequestDb, StaticFiles}; -use cot::response::Response; use cot::router::{Route, Router, Urls}; use cot::static_files::{StaticFile, StaticFilesMiddleware}; use cot::{App, AppBuilder, Project, reverse_redirect, static_files}; +use cot_core::response::Response; #[derive(Debug, Clone)] #[model] diff --git a/examples/json/src/main.rs b/examples/json/src/main.rs index 56182a62..34d7bbfa 100644 --- a/examples/json/src/main.rs +++ b/examples/json/src/main.rs @@ -3,12 +3,12 @@ use cot::config::ProjectConfig; use cot::json::Json; 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}; use cot_core::error::handler::{DynErrorPageHandler, RequestError}; +use cot_core::response::IntoResponse; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Deserialize, schemars::JsonSchema)] diff --git a/examples/sessions/src/main.rs b/examples/sessions/src/main.rs index 7aea2fa0..221dd35d 100644 --- a/examples/sessions/src/main.rs +++ b/examples/sessions/src/main.rs @@ -5,11 +5,11 @@ use cot::config::{ SessionStoreTypeConfig, }; use cot::core::request::Request; +use cot::core::response::{IntoResponse, Response}; use cot::form::Form; use cot::html::Html; use cot::middleware::SessionMiddleware; use cot::project::{MiddlewareContext, RegisterAppsContext, RootHandler}; -use cot::response::{IntoResponse, Response}; use cot::router::{Route, Router, Urls}; use cot::session::Session; use cot::session::db::SessionApp; diff --git a/examples/todo-list/src/main.rs b/examples/todo-list/src/main.rs index a156f090..381b6a24 100644 --- a/examples/todo-list/src/main.rs +++ b/examples/todo-list/src/main.rs @@ -5,13 +5,13 @@ use cot::auth::db::DatabaseUserApp; use cot::cli::CliMetadata; use cot::config::{DatabaseConfig, ProjectConfig}; use cot::core::request::extractors::{Path, RequestForm}; +use cot::core::response::Response; use cot::db::migrations::SyncDynMigration; use cot::db::{Auto, Model, model, query}; use cot::form::Form; use cot::html::Html; use cot::project::{MiddlewareContext, RegisterAppsContext, RootHandler}; use cot::request::extractors::RequestDb; -use cot::response::Response; use cot::router::{Route, Router, Urls}; use cot::static_files::StaticFilesMiddleware; use cot::{App, AppBuilder, Project, reverse_redirect}; From c767551968f3d7474bf63cda3fd5a3f58fa2cfcb Mon Sep 17 00:00:00 2001 From: Marek 'seqre' Grzelak Date: Sun, 28 Dec 2025 13:49:36 +0100 Subject: [PATCH 06/18] move handler --- Cargo.lock | 2 ++ {cot => cot-core}/src/handler.rs | 25 +++++++++++++------------ cot-core/src/lib.rs | 2 ++ cot/src/lib.rs | 6 ++---- cot/src/openapi.rs | 2 +- cot/src/project.rs | 2 +- cot/src/router.rs | 2 +- cot/src/router/method.rs | 2 +- cot/src/test.rs | 2 +- 9 files changed, 24 insertions(+), 21 deletions(-) rename {cot => cot-core}/src/handler.rs (93%) diff --git a/Cargo.lock b/Cargo.lock index 36accc5d..db164da3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1255,6 +1255,7 @@ version = "0.1.0" dependencies = [ "askama", "cot", + "cot_core", ] [[package]] @@ -1287,6 +1288,7 @@ name = "example-json" version = "0.1.0" dependencies = [ "cot", + "cot_core", "schemars", "serde", ] diff --git a/cot/src/handler.rs b/cot-core/src/handler.rs similarity index 93% rename from cot/src/handler.rs rename to cot-core/src/handler.rs index e428191f..1733037c 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 cot_core::request::Request; -use cot_core::request::extractors::{FromRequest, FromRequestHead}; -use cot_core::response::{IntoResponse, Response}; +use crate::request::extractors::{FromRequest, FromRequestHead}; +use crate::request::Request; +use crate::response::{IntoResponse, Response}; +use crate::Error; +use crate::Result; use tower::util::BoxCloneSyncService; -use crate::{Error, Result}; - /// 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/lib.rs b/cot-core/src/lib.rs index 7e552f66..b6c57aef 100644 --- a/cot-core/src/lib.rs +++ b/cot-core/src/lib.rs @@ -10,6 +10,8 @@ pub mod error; pub mod headers; pub mod request; pub mod response; +#[macro_use] +pub mod handler; /// A type alias for a result that can return a [`cot_core::Error`]. pub type Result = std::result::Result; diff --git a/cot/src/lib.rs b/cot/src/lib.rs index 5ea8c263..f261c866 100644 --- a/cot/src/lib.rs +++ b/cot/src/lib.rs @@ -66,8 +66,6 @@ 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; @@ -160,7 +158,7 @@ pub use cot_macros::test; pub use schemars; pub use {bytes, cot_core as core, http}; -pub use crate::handler::{BoxedHandler, RequestHandler}; +pub use cot_core::handler::{BoxedHandler, RequestHandler}; pub use crate::project::{ - App, AppBuilder, Bootstrapper, Project, ProjectContext, run, run_at, run_cli, + run, run_at, run_cli, App, AppBuilder, Bootstrapper, Project, ProjectContext, }; diff --git a/cot/src/openapi.rs b/cot/src/openapi.rs index f6cde8c0..93eec449 100644 --- a/cot/src/openapi.rs +++ b/cot/src/openapi.rs @@ -207,7 +207,7 @@ use serde_json::Value; use crate::auth::Auth; use crate::form::Form; -use crate::handler::BoxRequestHandler; +use cot_core::handler::BoxRequestHandler; use crate::json::Json; use crate::request::extractors::{FromRequest, FromRequestHead, Path, RequestForm, UrlQuery}; use crate::request::{Request, RequestHead}; diff --git a/cot/src/project.rs b/cot/src/project.rs index 384d3268..99d09eea 100644 --- a/cot/src/project.rs +++ b/cot/src/project.rs @@ -53,7 +53,7 @@ use crate::db::Database; #[cfg(feature = "db")] use crate::db::migrations::{MigrationEngine, SyncDynMigration}; use crate::error_page::Diagnostics; -use crate::handler::BoxedHandler; +use cot_core::handler::BoxedHandler; use crate::html::Html; use crate::middleware::{IntoCotError, IntoCotErrorLayer, IntoCotResponse, IntoCotResponseLayer}; use crate::router::{Route, Router, RouterService}; diff --git a/cot/src/router.rs b/cot/src/router.rs index 1fb0dc02..28ea818b 100644 --- a/cot/src/router.rs +++ b/cot/src/router.rs @@ -33,7 +33,7 @@ use cot_core::response::Response; use derive_more::with_trait::Debug; use tracing::debug; -use crate::handler::{BoxRequestHandler, RequestHandler, into_box_request_handler}; +use cot_core::handler::{BoxRequestHandler, RequestHandler, into_box_request_handler}; use crate::request::{AppName, PathParams, Request, RequestExt, RequestHead, RouteName}; use crate::router::path::{CaptureResult, PathMatcher, ReverseParamMap}; use crate::{Error, Result}; diff --git a/cot/src/router/method.rs b/cot/src/router/method.rs index 02050e30..f42553bd 100644 --- a/cot/src/router/method.rs +++ b/cot/src/router/method.rs @@ -8,7 +8,7 @@ use std::fmt::{Debug, Formatter}; use cot_core::error::MethodNotAllowed; use cot_core::response::Response; -use crate::handler::{BoxRequestHandler, into_box_request_handler}; +use cot_core::handler::{BoxRequestHandler, into_box_request_handler}; use crate::request::Request; use crate::{Method, RequestHandler}; diff --git a/cot/src/test.rs b/cot/src/test.rs index 402c3809..d462c73a 100644 --- a/cot/src/test.rs +++ b/cot/src/test.rs @@ -24,7 +24,7 @@ use crate::db::Database; use crate::db::migrations::{ DynMigration, MigrationDependency, MigrationEngine, MigrationWrapper, Operation, }; -use crate::handler::BoxedHandler; +use cot_core::handler::BoxedHandler; use crate::project::{prepare_request, prepare_request_for_error_handler, run_at_with_shutdown}; use crate::request::Request; use crate::router::Router; From 0ba71122a8856202ea8ce72f324d88dafb5a9313 Mon Sep 17 00:00:00 2001 From: Marek 'seqre' Grzelak Date: Sun, 28 Dec 2025 14:09:56 +0100 Subject: [PATCH 07/18] move router --- Cargo.lock | 1 + cot-core/Cargo.toml | 1 + cot-core/src/lib.rs | 1 + cot-core/src/request/extractors.rs | 5 +- cot-core/src/response/into_response.rs | 4 +- {cot => cot-core}/src/router.rs | 112 +++++++++--------- {cot => cot-core}/src/router/method.rs | 73 ++++++------ .../src/router/method/openapi.rs | 75 ++++++------ {cot => cot-core}/src/router/path.rs | 34 +++--- cot/benches/bench_utils.rs | 2 +- cot/benches/router.rs | 2 +- cot/src/admin.rs | 14 +-- cot/src/error_page.rs | 8 +- cot/src/lib.rs | 5 +- cot/src/openapi.rs | 42 +++---- cot/src/openapi/swagger_ui.rs | 2 +- cot/src/project.rs | 6 +- cot/src/session.rs | 6 +- cot/src/test.rs | 6 +- cot/tests/openapi.rs | 4 +- cot/tests/project.rs | 2 +- cot/tests/router.rs | 2 +- examples/admin/src/main.rs | 4 +- examples/custom-error-pages/src/main.rs | 2 +- examples/file-upload/src/main.rs | 2 +- examples/forms/src/main.rs | 2 +- examples/hello-world/src/main.rs | 2 +- examples/json/src/main.rs | 4 +- examples/sessions/src/main.rs | 6 +- examples/todo-list/src/main.rs | 2 +- 30 files changed, 217 insertions(+), 214 deletions(-) rename {cot => cot-core}/src/router.rs (92%) rename {cot => cot-core}/src/router/method.rs (92%) rename {cot => cot-core}/src/router/method/openapi.rs (92%) rename {cot => cot-core}/src/router/path.rs (94%) diff --git a/Cargo.lock b/Cargo.lock index db164da3..e11911f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -793,6 +793,7 @@ dependencies = [ "tokio", "tower", "tower-sessions", + "tracing", ] [[package]] diff --git a/cot-core/Cargo.toml b/cot-core/Cargo.toml index 887d1296..702914a7 100644 --- a/cot-core/Cargo.toml +++ b/cot-core/Cargo.toml @@ -37,6 +37,7 @@ 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 diff --git a/cot-core/src/lib.rs b/cot-core/src/lib.rs index b6c57aef..9aad3753 100644 --- a/cot-core/src/lib.rs +++ b/cot-core/src/lib.rs @@ -12,6 +12,7 @@ pub mod request; pub mod response; #[macro_use] pub mod handler; +pub mod router; /// A type alias for a result that can return a [`cot_core::Error`]. pub type Result = std::result::Result; diff --git a/cot-core/src/request/extractors.rs b/cot-core/src/request/extractors.rs index 8c8d338d..0c1c3098 100644 --- a/cot-core/src/request/extractors.rs +++ b/cot-core/src/request/extractors.rs @@ -49,16 +49,16 @@ //! ``` use std::future::Future; -use std::sync::Arc; use cot::auth::Auth; use cot::form::{Form, FormResult}; use cot::router::Urls; use cot::session::Session; use serde::de::DeserializeOwned; +use tower_sessions::Session; -use crate::Body; pub use crate::request::{PathParams, Request, RequestExt, RequestHead}; +use crate::{Body, Method}; /// Trait for extractors that consume the request body. /// @@ -352,6 +352,7 @@ impl FromRequestHead for Auth { /// ``` pub use cot_macros::FromRequestHead; + use crate::impl_into_cot_error; #[cfg(test)] diff --git a/cot-core/src/response/into_response.rs b/cot-core/src/response/into_response.rs index 9c38e03b..1f281df9 100644 --- a/cot-core/src/response/into_response.rs +++ b/cot-core/src/response/into_response.rs @@ -4,7 +4,7 @@ use http; use crate::headers::{HTML_CONTENT_TYPE, OCTET_STREAM_CONTENT_TYPE, PLAIN_TEXT_CONTENT_TYPE}; use crate::response::Response; -use crate::{Body, Error, StatusCode, impl_into_cot_error}; +use crate::{Body, Error, StatusCode}; /// Trait for generating responses. /// Types that implement `IntoResponse` can be returned from handlers. @@ -193,7 +193,7 @@ where macro_rules! impl_into_response_for_type_and_mime { ($ty:ty, $mime:expr) => { impl IntoResponse for $ty { - fn into_response(self) -> cot::Result { + fn into_response(self) -> cot_core::Result { Body::from(self) .with_header(http::header::CONTENT_TYPE, $mime) .into_response() diff --git a/cot/src/router.rs b/cot-core/src/router.rs similarity index 92% rename from cot/src/router.rs rename to cot-core/src/router.rs index 28ea818b..65ef738e 100644 --- a/cot/src/router.rs +++ b/cot-core/src/router.rs @@ -3,9 +3,9 @@ //! # Examples //! //! ``` -//! use cot::router::{Route, Router}; //! 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)?) @@ -27,16 +27,16 @@ use std::pin::Pin; use std::sync::Arc; use std::task::{Context, Poll}; -use cot_core::error::NotFound; -use cot_core::error::error_impl::impl_into_cot_error; -use cot_core::response::Response; +use cot::project; use derive_more::with_trait::Debug; use tracing::debug; -use cot_core::handler::{BoxRequestHandler, RequestHandler, into_box_request_handler}; +use crate::error::NotFound; +use crate::handler::{into_box_request_handler, BoxRequestHandler, RequestHandler}; use crate::request::{AppName, PathParams, Request, RequestExt, RequestHead, RouteName}; +use crate::response::Response; use crate::router::path::{CaptureResult, PathMatcher, ReverseParamMap}; -use crate::{Error, Result}; +use crate::{impl_into_cot_error, Error, Result}; pub mod method; pub mod path; @@ -45,14 +45,14 @@ pub mod path; /// /// 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. +/// what is typically done in [`project::App::router`] implementations. /// /// # Examples /// /// ``` -/// use cot::router::{Route, Router}; /// use cot_core::request::Request; /// use cot_core::response::Response; +/// use cot_core::router::{Route, Router}; /// /// async fn home(request: Request) -> cot::Result { /// unimplemented!() @@ -75,7 +75,7 @@ impl Router { /// # Examples /// /// ``` - /// use cot::router::Router; + /// use cot_core::router::Router; /// /// let router = Router::empty(); /// ``` @@ -89,9 +89,9 @@ impl Router { /// # Examples /// /// ``` - /// use cot::router::{Route, Router}; /// use cot_core::request::Request; /// use cot_core::response::Response; + /// use cot_core::router::{Route, Router}; /// /// async fn home(request: Request) -> cot::Result { /// unimplemented!() @@ -117,7 +117,7 @@ impl Router { } } - pub(crate) fn set_app_name(&mut self, app_name: AppName) { + pub fn set_app_name(&mut self, app_name: AppName) { self.app_name = Some(app_name); } @@ -203,7 +203,7 @@ impl Router { /// Handle a request. /// - /// This method is called by the [`CotApp`](crate::App) to handle + /// This method is called by the [`CotApp`](cot::project::App) to handle /// a request. /// /// # Errors @@ -299,9 +299,9 @@ impl Router { /// # Examples /// /// ``` - /// use cot::router::{Route, Router}; /// use cot_core::request::Request; /// use cot_core::response::Response; + /// use cot_core::router::{Route, Router}; /// /// async fn home(request: Request) -> cot::Result { /// unimplemented!() @@ -320,9 +320,9 @@ impl Router { /// # Examples /// /// ``` - /// use cot::router::{Route, Router}; /// use cot_core::request::Request; /// use cot_core::response::Response; + /// use cot_core::router::{Route, Router}; /// /// async fn home(request: Request) -> cot::Result { /// unimplemented!() @@ -421,7 +421,7 @@ impl Router { let url = format!("{url}{}", route.url); - let mut route_context = crate::openapi::RouteContext::new(); + let mut route_context = cot::openapi::RouteContext::new(); route_context.param_names = ¶ms; paths.paths.insert( @@ -461,8 +461,8 @@ struct HandlerFound<'a> { /// 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 +/// 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 { @@ -477,9 +477,9 @@ impl RouterService { /// ``` /// use std::sync::Arc; /// - /// use cot::router::{Route, Router, RouterService}; /// 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!() @@ -528,9 +528,9 @@ pub fn split_view_name(view_name: &str) -> (Option<&str>, &str) { /// # Examples /// /// ``` -/// use cot::router::{Route, Router}; /// use cot_core::request::Request; /// use cot_core::response::Response; +/// use cot_core::router::{Route, Router}; /// /// async fn home(request: Request) -> cot::Result { /// unimplemented!() @@ -551,9 +551,9 @@ impl Route { /// # Examples /// /// ``` - /// use cot::router::{Route, Router}; /// use cot_core::request::Request; /// use cot_core::response::Response; + /// use cot_core::router::{Route, Router}; /// /// async fn home(request: Request) -> cot::Result { /// // ... @@ -578,16 +578,16 @@ impl Route { /// 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 + /// See [`cot::openapi`] module documentation for more details on how to /// generate OpenAPI specs automatically. /// /// # Examples /// /// ``` - /// use cot::router::method::openapi::api_get; - /// use cot::router::{Route, Router}; /// 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 { /// // ... @@ -601,12 +601,12 @@ impl Route { pub fn with_api_handler(url: &str, handler: H) -> Self where HandlerParams: 'static, - H: RequestHandler + crate::openapi::AsApiRoute + Send + Sync + 'static, + H: RequestHandler + cot::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), + cot::openapi::into_box_api_endpoint_request_handler(handler), )), name: None, } @@ -617,10 +617,10 @@ impl Route { /// # Examples /// /// ``` - /// use cot::router::method::openapi::api_get; - /// use cot::router::{Route, Router}; /// 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 { /// // ... @@ -646,16 +646,16 @@ impl Route { /// 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 + /// See [`cot::openapi`] module documentation for more details on how to /// generate OpenAPI specs automatically. /// /// # Examples /// /// ``` - /// use cot::router::method::openapi::api_post; - /// use cot::router::{Route, Router}; /// 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 { /// // ... @@ -670,12 +670,12 @@ impl Route { where N: Into, HandlerParams: 'static, - H: RequestHandler + crate::openapi::AsApiRoute + Send + Sync + 'static, + H: RequestHandler + cot::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), + cot::openapi::into_box_api_endpoint_request_handler(handler), )), name: Some(RouteName(name.into())), } @@ -686,9 +686,9 @@ impl Route { /// # Examples /// /// ``` - /// use cot::router::{Route, Router}; /// use cot_core::request::Request; /// use cot_core::response::Response; + /// use cot_core::router::{Route, Router}; /// /// async fn home(request: Request) -> cot::Result { /// unimplemented!() @@ -711,9 +711,9 @@ impl Route { /// # Examples /// /// ``` - /// use cot::router::{Route, Router}; /// use cot_core::request::Request; /// use cot_core::response::Response; + /// use cot_core::router::{Route, Router}; /// /// async fn home(request: Request) -> cot::Result { /// unimplemented!() @@ -733,9 +733,9 @@ impl Route { /// # Examples /// /// ``` - /// use cot::router::{Route, Router}; /// use cot_core::request::Request; /// use cot_core::response::Response; + /// use cot_core::router::{Route, Router}; /// /// async fn home(request: Request) -> cot::Result { /// unimplemented!() @@ -750,7 +750,7 @@ impl Route { } #[must_use] - pub(crate) fn kind(&self) -> RouteKind { + pub fn kind(&self) -> RouteKind { match &self.view { RouteInner::Handler(_) => RouteKind::Handler, RouteInner::Router(_) => RouteKind::Router, @@ -760,7 +760,7 @@ impl Route { } #[must_use] - pub(crate) fn router(&self) -> Option<&Router> { + pub fn router(&self) -> Option<&Router> { match &self.view { RouteInner::Router(router) => Some(router), RouteInner::Handler(_) => None, @@ -771,7 +771,7 @@ impl Route { } #[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub(crate) enum RouteKind { +pub enum RouteKind { Handler, Router, } @@ -781,7 +781,7 @@ enum RouteInner { Handler(Arc), Router(Router), #[cfg(feature = "openapi")] - ApiHandler(Arc), + ApiHandler(Arc), } /// Get a URL for a view by its registered name and given params. @@ -794,7 +794,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 +802,9 @@ enum RouteInner { /// ``` /// use cot::html::Html; /// use cot::project::RegisterAppsContext; -/// 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: @@ -865,9 +865,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 +904,10 @@ impl Urls { /// /// ``` /// use cot::html::Html; - /// 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 +925,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 +941,9 @@ impl Urls { /// # Examples /// /// ``` - /// 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 +961,9 @@ impl Urls { /// # Examples /// /// ``` - /// 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(); @@ -998,7 +998,7 @@ impl Debug for RouteInner { /// /// # Return value /// -/// Returns a [`cot::Result`] that contains the URL for +/// 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. /// @@ -1006,9 +1006,9 @@ impl Debug for RouteInner { /// /// ``` /// use cot::reverse_redirect; -/// use cot::router::{Route, Router}; /// 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")?) @@ -1029,13 +1029,13 @@ macro_rules! reverse_redirect { #[cfg(test)] mod tests { - use cot_core::response::{IntoResponse, Response}; + use cot::html::Html; + use cot::test::TestRequestBuilder; use super::*; - use crate::StatusCode; - use crate::html::Html; use crate::request::Request; - use crate::test::TestRequestBuilder; + use crate::response::{IntoResponse, Response}; + use crate::StatusCode; struct MockHandler; @@ -1046,7 +1046,7 @@ mod tests { } #[cfg(feature = "openapi")] - impl crate::openapi::AsApiRoute for MockHandler { + impl cot::openapi::AsApiRoute for MockHandler { fn as_api_route( &self, _route_context: &cot::openapi::RouteContext<'_>, @@ -1103,7 +1103,7 @@ mod tests { assert_eq!(router.routes().len(), 1); } - #[cot::test] + #[cot_macros::test] async fn router_route() { let route = Route::with_handler("/test", MockHandler); let router = Router::with_urls(vec![route.clone()]); @@ -1111,7 +1111,7 @@ mod tests { assert_eq!(response.status(), StatusCode::OK); } - #[cot::test] + #[cot_macros::test] async fn router_handle() { let route = Route::with_handler("/test", MockHandler); let router = Router::with_urls(vec![route.clone()]); @@ -1119,7 +1119,7 @@ mod tests { assert_eq!(response.status(), StatusCode::OK); } - #[cot::test] + #[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()]); diff --git a/cot/src/router/method.rs b/cot-core/src/router/method.rs similarity index 92% rename from cot/src/router/method.rs rename to cot-core/src/router/method.rs index f42553bd..b598aa8d 100644 --- a/cot/src/router/method.rs +++ b/cot-core/src/router/method.rs @@ -5,12 +5,11 @@ pub mod openapi; use std::fmt::{Debug, Formatter}; -use cot_core::error::MethodNotAllowed; -use cot_core::response::Response; - -use cot_core::handler::{BoxRequestHandler, into_box_request_handler}; +use crate::error::MethodNotAllowed; +use crate::handler::{into_box_request_handler, BoxRequestHandler, RequestHandler}; use crate::request::Request; -use crate::{Method, RequestHandler}; +use crate::response::Response; +use crate::Method; /// A router that routes requests based on the HTTP method. /// @@ -31,9 +30,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") @@ -91,7 +90,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") @@ -149,16 +148,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") @@ -206,10 +206,10 @@ impl MethodRouter { /// ``` /// use cot::StatusCode; /// use cot::html::Html; - /// 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) @@ -245,7 +245,7 @@ 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) } } @@ -253,21 +253,21 @@ impl RequestHandler for MethodRouter { #[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 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, @@ -284,7 +284,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 { @@ -337,7 +337,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) } } @@ -420,10 +420,11 @@ async fn default_fallback(method: Method) -> crate::Error { #[cfg(test)] mod tests { + use cot::html::Html; + 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()) @@ -438,7 +439,7 @@ mod tests { assert_eq!(debug_str, "InnerHandler(..)"); } - #[cot::test] + #[cot_macros::test] async fn method_router_fallback() { let router = MethodRouter::new(); @@ -450,7 +451,7 @@ mod tests { assert!(inner.is::()); } - #[cot::test] + #[cot_macros::test] async fn method_router_default_fallback() { let router = MethodRouter::default(); @@ -462,7 +463,7 @@ mod tests { assert!(inner.is::()); } - #[cot::test] + #[cot_macros::test] async fn method_router_custom_fallback() { let router = MethodRouter::new().fallback(test_handler); @@ -473,7 +474,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); @@ -525,7 +526,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(); @@ -546,7 +547,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/method/openapi.rs b/cot-core/src/router/method/openapi.rs similarity index 92% rename from cot/src/router/method/openapi.rs rename to cot-core/src/router/method/openapi.rs index 8738a5fe..ffa43996 100644 --- a/cot/src/router/method/openapi.rs +++ b/cot-core/src/router/method/openapi.rs @@ -6,18 +6,18 @@ use std::fmt::{Debug, Formatter}; +use crate::response::Response; use aide::openapi::Operation; use cot::openapi::RouteContext; use cot::request::Request; -use cot::router::method::InnerHandler; -use cot_core::response::Response; +use cot_core::router::method::InnerHandler; use schemars::SchemaGenerator; -use crate::RequestHandler; -use crate::openapi::{ - AsApiOperation, AsApiRoute, BoxApiRequestHandler, into_box_api_request_handler, +use crate::handler::RequestHandler; +use cot::openapi::{ + into_box_api_request_handler, AsApiOperation, AsApiRoute, BoxApiRequestHandler, }; -use crate::router::method::InnerMethodRouter; +use cot_core::router::method::InnerMethodRouter; /// A version of [`MethodRouter`](crate::router::method::MethodRouter) that /// supports OpenAPI. @@ -36,7 +36,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,8 +47,8 @@ use crate::router::method::InnerMethodRouter; /// /// ``` /// use cot::json::Json; -/// use cot::router::method::openapi::api_post; -/// use cot::router::{Route, Router}; +/// use cot_core::router::method::openapi::api_post; +/// use cot_core::router::{Route, Router}; /// use cot::test::TestRequestBuilder; /// use serde::{Deserialize, Serialize}; /// @@ -106,7 +106,7 @@ macro_rules! define_method { /// /// ``` /// use cot::json::Json; - /// use cot::router::method::openapi::ApiMethodRouter; + /// use cot_core::router::method::openapi::ApiMethodRouter; /// /// async fn test_handler() -> Json<()> { /// Json(()) @@ -169,7 +169,7 @@ 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 + /// functions defined in [`cot_core::router::method::openapi`] which serve as /// convenient constructors for a [`ApiMethodRouter`] with a specific /// handler. /// @@ -177,9 +177,9 @@ impl ApiMethodRouter { /// /// ``` /// use cot::json::Json; - /// use cot::router::method::MethodRouter; - /// use cot::router::method::openapi::ApiMethodRouter; - /// use cot::router::{Route, Router}; + /// use cot_core::router::method::MethodRouter; + /// use cot_core::router::method::openapi::ApiMethodRouter; + /// use cot_core::router::{Route, Router}; /// use cot::test::TestRequestBuilder; /// /// async fn test_handler() -> Json<()> { @@ -232,7 +232,7 @@ impl ApiMethodRouter { /// /// ``` /// use cot::json::Json; - /// use cot::router::method::openapi::ApiMethodRouter; + /// use cot_core::router::method::openapi::ApiMethodRouter; /// /// async fn test_handler() -> Json<()> { /// Json(()) @@ -242,8 +242,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,8 +276,8 @@ impl ApiMethodRouter { /// /// ``` /// use cot::html::Html; - /// use cot::router::method::openapi::ApiMethodRouter; - /// use cot::router::{Route, Router}; + /// use cot_core::router::method::openapi::ApiMethodRouter; + /// use cot_core::router::{Route, Router}; /// use cot::test::TestRequestBuilder; /// use cot::{Body, StatusCode}; /// @@ -315,7 +315,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) } } @@ -372,7 +372,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,7 +477,7 @@ define_method_router!(api_trace, trace => TRACE); /// /// ``` /// use cot::html::Html; -/// use cot::router::method::openapi::api_connect; +/// use cot_core::router::method::openapi::api_connect; /// use cot::{Body, StatusCode}; /// /// async fn test_handler() -> Html { @@ -488,8 +488,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,17 +517,16 @@ where #[cfg(test)] mod tests { - use cot_core::error::MethodNotAllowed; - use cot_core::response::{IntoResponse, Response}; + use crate::error::MethodNotAllowed; + use crate::response::{IntoResponse, Response}; use super::*; - use crate::html::Html; - use crate::json::Json; use crate::request::extractors::Path; - use crate::test::TestRequestBuilder; - use crate::{Method, StatusCode}; + use cot::html::Html; + use cot::json::Json; + use cot::test::TestRequestBuilder; - async fn test_handler(method: Method) -> cot::Result { + async fn test_handler(method: Method) -> crate::Result { Html::new(method.as_str()).into_response() } @@ -540,7 +539,7 @@ mod tests { assert_eq!(debug_str, "InnerApiHandler(..)"); } - #[cot::test] + #[cot_macros::test] async fn api_method_router_fallback() { let router = ApiMethodRouter::new(); @@ -552,7 +551,7 @@ mod tests { assert!(inner.is::()); } - #[cot::test] + #[cot_macros::test] async fn api_method_router_default_fallback() { let router = ApiMethodRouter::default(); @@ -564,7 +563,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); @@ -575,7 +574,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); @@ -627,7 +626,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(); @@ -648,7 +647,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) @@ -686,7 +685,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/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 1cf30198..af027b66 100644 --- a/cot/src/router/path.rs +++ b/cot-core/src/router/path.rs @@ -7,18 +7,18 @@ use std::collections::HashMap; use std::fmt::Display; -use cot_core::error::error_impl::impl_into_cot_error; +use crate::impl_into_cot_error; use thiserror::Error; use tracing::debug; #[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 }, @@ -115,7 +115,7 @@ impl PathMatcher { } #[must_use] - pub(crate) fn capture<'matcher, 'path>( + pub fn capture<'matcher, 'path>( &'matcher self, path: &'path str, ) -> Option> { @@ -150,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 { @@ -173,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()), @@ -197,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"); @@ -220,7 +220,7 @@ impl ReverseParamMap { /// # Examples /// /// ``` - /// use cot::router::path::ReverseParamMap; + /// use cot_core::router::path::ReverseParamMap; /// /// let mut map = ReverseParamMap::new(); /// ``` @@ -237,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"); @@ -280,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> { @@ -295,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() } } @@ -319,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/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 d3c1ee0f..fc8e6505 100644 --- a/cot/src/admin.rs +++ b/cot/src/admin.rs @@ -30,9 +30,9 @@ use crate::form::{ Form, FormContext, FormErrorTarget, FormField, FormFieldValidationError, FormResult, }; use crate::html::Html; -use crate::router::{Router, Urls}; use crate::static_files::StaticFile; use crate::{App, Error, Method, RequestHandler, reverse_redirect}; +use cot_core::router::{Router, Urls}; struct AdminAuthenticated(H, PhantomData T>); @@ -682,28 +682,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/error_page.rs b/cot/src/error_page.rs index e9a897fe..6bcb2701 100644 --- a/cot/src/error_page.rs +++ b/cot/src/error_page.rs @@ -8,8 +8,8 @@ use cot_core::error::backtrace::{__cot_create_backtrace, Backtrace}; use tracing::{Level, error, warn}; use crate::config::ProjectConfig; -use crate::router::Router; use crate::{Error, Result, StatusCode}; +use cot_core::router::Router; #[derive(Debug)] pub(super) struct Diagnostics { @@ -142,13 +142,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(), }); @@ -456,8 +456,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/lib.rs b/cot/src/lib.rs index f261c866..78da9232 100644 --- a/cot/src/lib.rs +++ b/cot/src/lib.rs @@ -75,7 +75,6 @@ pub mod openapi; pub mod project; pub mod request; mod response; -pub mod router; mod serializers; pub mod session; pub mod static_files; @@ -158,7 +157,7 @@ pub use cot_macros::test; pub use schemars; pub use {bytes, cot_core as core, http}; -pub use cot_core::handler::{BoxedHandler, RequestHandler}; pub use crate::project::{ - run, run_at, run_cli, App, AppBuilder, Bootstrapper, Project, ProjectContext, + App, AppBuilder, Bootstrapper, Project, ProjectContext, run, run_at, run_cli, }; +pub use cot_core::handler::{BoxedHandler, RequestHandler}; diff --git a/cot/src/openapi.rs b/cot/src/openapi.rs index 93eec449..2052494b 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::openapi::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::openapi::api_get) or +//! [`api_post`](cot_core::router::method::openapi::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::openapi::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,8 +34,8 @@ //! 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_core::router::method::openapi::api_post; +//! use cot_core::router::{Route, Router}; //! use cot::static_files::StaticFilesMiddleware; //! use cot::{App, AppBuilder, Project, StatusCode}; //! use serde::{Deserialize, Serialize}; @@ -207,14 +207,14 @@ use serde_json::Value; use crate::auth::Auth; use crate::form::Form; -use cot_core::handler::BoxRequestHandler; use crate::json::Json; use crate::request::extractors::{FromRequest, FromRequestHead, Path, RequestForm, UrlQuery}; use crate::request::{Request, RequestHead}; use crate::response::{Response, WithExtension}; -use crate::router::Urls; use crate::session::Session; use crate::{Body, Method, RequestHandler}; +use cot_core::handler::BoxRequestHandler; +use cot_core::router::Urls; /// Context for API route generation. /// @@ -259,12 +259,12 @@ impl Default for RouteContext<'_> { /// 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). +/// to use [`ApiMethodRouter`](cot_core::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 +/// [`Route::with_api_handler`](cot_core::router::Route::with_api_handler) to /// generate the OpenAPI specs. /// /// # Examples @@ -290,7 +290,7 @@ impl Default for RouteContext<'_> { /// } /// /// # assert_eq!( -/// # RouteWrapper(cot::router::method::openapi::ApiMethodRouter::new()) +/// # RouteWrapper(cot_core::router::method::openapi::ApiMethodRouter::new()) /// # .as_api_route(&RouteContext::new(), &mut SchemaGenerator::default()) /// # .summary, /// # Some("This route was wrapped with RouteWrapper".to_owned()) @@ -322,7 +322,7 @@ pub trait AsApiRoute { /// } /// /// # assert_eq!( - /// # RouteWrapper(cot::router::method::openapi::ApiMethodRouter::new()) + /// # RouteWrapper(cot_core::router::method::openapi::ApiMethodRouter::new()) /// # .as_api_route(&RouteContext::new(), &mut SchemaGenerator::default()) /// # .summary, /// # Some("This route was wrapped with RouteWrapper".to_owned()) @@ -512,8 +512,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::openapi::api_get; /// /// struct MyExtractor; /// impl FromRequestHead for MyExtractor { @@ -530,14 +530,14 @@ where /// } /// /// let router = -/// cot::router::Router::with_urls([Route::with_api_handler("/with_api", api_get(handler))]); +/// 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::openapi::api_get; /// /// async fn handler_with_openapi() -> cot::Result { /// // ... @@ -548,7 +548,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)), diff --git a/cot/src/openapi/swagger_ui.rs b/cot/src/openapi/swagger_ui.rs index 02bd1784..a0190c34 100644 --- a/cot/src/openapi/swagger_ui.rs +++ b/cot/src/openapi/swagger_ui.rs @@ -13,8 +13,8 @@ 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; +use cot_core::router::{Route, Router}; /// A wrapper around the Swagger UI functionality. /// diff --git a/cot/src/project.rs b/cot/src/project.rs index 99d09eea..4b668051 100644 --- a/cot/src/project.rs +++ b/cot/src/project.rs @@ -53,13 +53,13 @@ use crate::db::Database; #[cfg(feature = "db")] use crate::db::migrations::{MigrationEngine, SyncDynMigration}; use crate::error_page::Diagnostics; -use cot_core::handler::BoxedHandler; use crate::html::Html; use crate::middleware::{IntoCotError, IntoCotErrorLayer, IntoCotResponse, IntoCotResponseLayer}; -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}; +use cot_core::handler::BoxedHandler; +use cot_core::router::{Route, Router, RouterService}; /// A building block for a Cot project. /// @@ -116,7 +116,7 @@ pub trait App: Send + Sync { /// ``` /// use cot::App; /// use cot::html::Html; - /// use cot::router::{Route, Router}; + /// use cot_core::router::{Route, Router}; /// /// async fn index() -> Html { /// Html::new("Hello world!") diff --git a/cot/src/session.rs b/cot/src/session.rs index d506527c..863fd8f8 100644 --- a/cot/src/session.rs +++ b/cot/src/session.rs @@ -8,7 +8,7 @@ //! ``` //! use cot::RequestHandler; //! use cot::html::Html; -//! use cot::router::{Route, Router}; +//! use cot_core::router::{Route, Router}; //! use cot::session::Session; //! use cot::test::TestRequestBuilder; //! @@ -53,7 +53,7 @@ use std::ops::{Deref, DerefMut}; /// use cot::RequestHandler; /// use cot::html::Html; /// use cot::request::Request; -/// use cot::router::{Route, Router}; +/// use cot_core::router::{Route, Router}; /// use cot::session::Session; /// use cot::test::TestRequestBuilder; /// @@ -101,7 +101,7 @@ impl Session { /// use cot::RequestHandler; /// use cot::html::Html; /// use cot::request::Request; - /// use cot::router::{Route, Router}; + /// use cot_core::router::{Route, Router}; /// use cot::session::Session; /// use cot::test::TestRequestBuilder; /// diff --git a/cot/src/test.rs b/cot/src/test.rs index d462c73a..9ef6c985 100644 --- a/cot/src/test.rs +++ b/cot/src/test.rs @@ -24,13 +24,13 @@ use crate::db::Database; use crate::db::migrations::{ DynMigration, MigrationDependency, MigrationEngine, MigrationWrapper, Operation, }; -use cot_core::handler::BoxedHandler; use crate::project::{prepare_request, prepare_request_for_error_handler, run_at_with_shutdown}; use crate::request::Request; -use crate::router::Router; use crate::session::Session; use crate::static_files::{StaticFile, StaticFiles}; use crate::{Body, Bootstrapper, Project, ProjectContext, Result}; +use cot_core::handler::BoxedHandler; +use cot_core::router::Router; /// A test client for making requests to a Cot project. /// @@ -447,7 +447,7 @@ impl TestRequestBuilder { /// /// ``` /// use cot::request::Request; - /// use cot::router::{Route, Router}; + /// use cot_core::router::{Route, Router}; /// use cot::test::TestRequestBuilder; /// use cot_core::response::Response; /// diff --git a/cot/tests/openapi.rs b/cot/tests/openapi.rs index 399d015a..cafc8d4e 100644 --- a/cot/tests/openapi.rs +++ b/cot/tests/openapi.rs @@ -2,12 +2,12 @@ use aide::openapi::{Parameter, PathItem, ReferenceOr}; use cot::html::Html; use cot::json::Json; use cot::openapi::{AsApiRoute, NoApi, RouteContext}; -use cot::router::method::openapi::{ApiMethodRouter, api_get, api_post}; -use cot::router::{Route, Router}; use cot::test::TestRequestBuilder; use cot::{RequestHandler, StatusCode}; use cot_core::request::extractors::{Path, UrlQuery}; use cot_core::response::{IntoResponse, Response}; +use cot_core::router::method::openapi::{api_get, api_post, ApiMethodRouter}; +use cot_core::router::{Route, Router}; use schemars::SchemaGenerator; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; diff --git a/cot/tests/project.rs b/cot/tests/project.rs index 90cbdbf2..64151a92 100644 --- a/cot/tests/project.rs +++ b/cot/tests/project.rs @@ -2,10 +2,10 @@ use bytes::Bytes; use cot::config::ProjectConfig; use cot::html::Html; use cot::project::RegisterAppsContext; -use cot::router::{Route, Router}; use cot::test::Client; use cot::{App, AppBuilder, Project, StatusCode, reverse}; use cot_core::request::Request; +use cot_core::router::{Route, Router}; #[cot::test] #[cfg_attr( diff --git a/cot/tests/router.rs b/cot/tests/router.rs index 3c266004..44acc559 100644 --- a/cot/tests/router.rs +++ b/cot/tests/router.rs @@ -2,10 +2,10 @@ use bytes::Bytes; use cot::config::ProjectConfig; use cot::html::Html; use cot::project::RegisterAppsContext; -use cot::router::{Route, Router}; use cot::test::Client; use cot::{App, AppBuilder, Project, StatusCode}; use cot_core::request::{Request, RequestExt}; +use cot_core::router::{Route, Router}; async fn index() -> Html { Html::new("Hello world!") diff --git a/examples/admin/src/main.rs b/examples/admin/src/main.rs index 82e5447f..92c9bcc1 100644 --- a/examples/admin/src/main.rs +++ b/examples/admin/src/main.rs @@ -13,15 +13,15 @@ use cot::config::{ StaticFilesConfig, StaticFilesPathRewriteMode, }; use cot::db::migrations::SyncDynMigration; -use cot::db::{Auto, Model, model}; +use cot::db::{model, Auto, Model}; use cot::form::Form; use cot::html::Html; use cot::middleware::{AuthMiddleware, LiveReloadMiddleware, SessionMiddleware}; use cot::project::{MiddlewareContext, RegisterAppsContext, RootHandler}; use cot::request::extractors::RequestDb; -use cot::router::{Route, Router, Urls}; use cot::static_files::StaticFilesMiddleware; use cot::{App, AppBuilder, Project, ProjectContext}; +use cot_core::router::{Route, Router, Urls}; #[derive(Debug, Clone, Form, AdminModel)] #[model] diff --git a/examples/custom-error-pages/src/main.rs b/examples/custom-error-pages/src/main.rs index fed37ee8..59ecf62c 100644 --- a/examples/custom-error-pages/src/main.rs +++ b/examples/custom-error-pages/src/main.rs @@ -3,10 +3,10 @@ use cot::cli::CliMetadata; use cot::config::ProjectConfig; use cot::html::Html; use cot::project::RegisterAppsContext; -use cot::router::{Route, Router}; use cot::{App, AppBuilder, Project}; use cot_core::error::handler::{DynErrorPageHandler, RequestError}; use cot_core::response::{IntoResponse, Response}; +use cot_core::router::{Route, Router}; async fn return_hello() -> cot::Result { panic!() diff --git a/examples/file-upload/src/main.rs b/examples/file-upload/src/main.rs index 587ec3e0..5160a7ad 100644 --- a/examples/file-upload/src/main.rs +++ b/examples/file-upload/src/main.rs @@ -7,9 +7,9 @@ use cot::form::fields::InMemoryUploadedFile; use cot::form::{Form, FormContext}; use cot::html::Html; use cot::project::{MiddlewareContext, RegisterAppsContext, RootHandler}; -use cot::router::{Route, Router, Urls}; use cot::static_files::StaticFilesMiddleware; use cot::{App, AppBuilder, Project}; +use cot_core::router::{Route, Router, Urls}; #[derive(Debug, Template)] #[template(path = "index.html")] diff --git a/examples/forms/src/main.rs b/examples/forms/src/main.rs index 96503267..75c247bc 100644 --- a/examples/forms/src/main.rs +++ b/examples/forms/src/main.rs @@ -15,10 +15,10 @@ use cot::html::Html; use cot::middleware::{AuthMiddleware, LiveReloadMiddleware, SessionMiddleware}; use cot::project::{MiddlewareContext, RegisterAppsContext, RootHandler, RootHandlerBuilder}; use cot::request::extractors::{RequestDb, StaticFiles}; -use cot::router::{Route, Router, Urls}; use cot::static_files::{StaticFile, StaticFilesMiddleware}; use cot::{App, AppBuilder, Project, reverse_redirect, static_files}; use cot_core::response::Response; +use cot_core::router::{Route, Router, Urls}; #[derive(Debug, Clone)] #[model] diff --git a/examples/hello-world/src/main.rs b/examples/hello-world/src/main.rs index cfc3464d..12578116 100644 --- a/examples/hello-world/src/main.rs +++ b/examples/hello-world/src/main.rs @@ -1,8 +1,8 @@ use cot::cli::CliMetadata; use cot::config::ProjectConfig; use cot::project::RegisterAppsContext; -use cot::router::{Route, Router}; use cot::{App, AppBuilder, Project}; +use cot_core::router::{Route, Router}; async fn return_hello() -> &'static str { "Hello Cot!" diff --git a/examples/json/src/main.rs b/examples/json/src/main.rs index 34d7bbfa..c3828bdc 100644 --- a/examples/json/src/main.rs +++ b/examples/json/src/main.rs @@ -3,12 +3,12 @@ use cot::config::ProjectConfig; use cot::json::Json; use cot::openapi::swagger_ui::SwaggerUi; use cot::project::{MiddlewareContext, RegisterAppsContext, RootHandler, RootHandlerBuilder}; -use cot::router::method::openapi::api_post; -use cot::router::{Route, Router}; use cot::static_files::StaticFilesMiddleware; use cot::{App, AppBuilder, Project}; use cot_core::error::handler::{DynErrorPageHandler, RequestError}; use cot_core::response::IntoResponse; +use cot_core::router::method::openapi::api_post; +use cot_core::router::{Route, Router}; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Deserialize, schemars::JsonSchema)] diff --git a/examples/sessions/src/main.rs b/examples/sessions/src/main.rs index 221dd35d..280a48c5 100644 --- a/examples/sessions/src/main.rs +++ b/examples/sessions/src/main.rs @@ -10,10 +10,10 @@ use cot::form::Form; use cot::html::Html; use cot::middleware::SessionMiddleware; use cot::project::{MiddlewareContext, RegisterAppsContext, RootHandler}; -use cot::router::{Route, Router, Urls}; -use cot::session::Session; use cot::session::db::SessionApp; -use cot::{App, AppBuilder, Project, reverse_redirect}; +use cot::session::Session; +use cot::{reverse_redirect, App, AppBuilder, Project}; +use cot_core::router::{Route, Router, Urls}; #[derive(Debug, Template)] #[template(path = "index.html")] diff --git a/examples/todo-list/src/main.rs b/examples/todo-list/src/main.rs index 381b6a24..d3b7381e 100644 --- a/examples/todo-list/src/main.rs +++ b/examples/todo-list/src/main.rs @@ -12,9 +12,9 @@ use cot::form::Form; use cot::html::Html; use cot::project::{MiddlewareContext, RegisterAppsContext, RootHandler}; use cot::request::extractors::RequestDb; -use cot::router::{Route, Router, Urls}; use cot::static_files::StaticFilesMiddleware; use cot::{App, AppBuilder, Project, reverse_redirect}; +use cot_core::router::{Route, Router, Urls}; #[derive(Debug, Clone)] #[model] From 92406f2268fe1af8f1e4afddbc22e7563a0d3379 Mon Sep 17 00:00:00 2001 From: Marek 'seqre' Grzelak Date: Sun, 28 Dec 2025 19:07:35 +0100 Subject: [PATCH 08/18] move middleware --- cot-core/src/lib.rs | 1 + cot-core/src/middleware.rs | 237 ++++++++++++++++++++++++++++++ cot/src/middleware.rs | 230 +---------------------------- cot/src/middleware/live_reload.rs | 2 +- 4 files changed, 240 insertions(+), 230 deletions(-) create mode 100644 cot-core/src/middleware.rs diff --git a/cot-core/src/lib.rs b/cot-core/src/lib.rs index 9aad3753..8350de6a 100644 --- a/cot-core/src/lib.rs +++ b/cot-core/src/lib.rs @@ -12,6 +12,7 @@ pub mod request; pub mod response; #[macro_use] pub mod handler; +pub mod middleware; pub mod router; /// A type alias for a result that can return a [`cot_core::Error`]. diff --git a/cot-core/src/middleware.rs b/cot-core/src/middleware.rs new file mode 100644 index 00000000..758cace2 --- /dev/null +++ b/cot-core/src/middleware.rs @@ -0,0 +1,237 @@ +use std::task::{Context, Poll}; + +use bytes::Bytes; +use http_body_util::combinators::BoxBody; +use http_body_util::BodyExt; +use tower::Service; + +use crate::error::error_impl::Error; +use crate::request::Request; +use crate::response::Response; +use crate::Body; + +/// 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/src/middleware.rs b/cot/src/middleware.rs index 27cd215e..bf867f6a 100644 --- a/cot/src/middleware.rs +++ b/cot/src/middleware.rs @@ -9,13 +9,11 @@ use std::fmt::Debug; use std::sync::Arc; use std::task::{Context, Poll}; -use bytes::Bytes; 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}; @@ -24,7 +22,6 @@ use tower_sessions::{SessionManagerLayer, SessionStore}; use crate::config::CacheType; use crate::config::{Expiry, SameSite, SessionStoreTypeConfig}; use crate::project::MiddlewareContext; -use crate::session::store::SessionStoreWrapper; #[cfg(all(feature = "db", feature = "json"))] use crate::session::store::db::DbStore; #[cfg(feature = "json")] @@ -32,6 +29,7 @@ use crate::session::store::file::FileStore; use crate::session::store::memory::MemoryStore; #[cfg(feature = "redis")] use crate::session::store::redis::RedisStore; +use crate::session::store::SessionStoreWrapper; use crate::{Body, Error}; #[cfg(feature = "live-reload")] @@ -40,232 +38,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_core::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_core::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. 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< From b3f551d1c952666db7fb999e33375c4a918588e3 Mon Sep 17 00:00:00 2001 From: Marek 'seqre' Grzelak Date: Sun, 28 Dec 2025 19:16:13 +0100 Subject: [PATCH 09/18] move RequestExt back and other things --- cot-core/src/error/handler.rs | 7 +- cot-core/src/request.rs | 365 +-------------------------------- cot/src/project.rs | 20 +- cot/src/request.rs | 370 ++++++++++++++++++++++++++++++++++ 4 files changed, 385 insertions(+), 377 deletions(-) diff --git a/cot-core/src/error/handler.rs b/cot-core/src/error/handler.rs index a8afa81f..94fa879d 100644 --- a/cot-core/src/error/handler.rs +++ b/cot-core/src/error/handler.rs @@ -7,13 +7,12 @@ use std::pin::Pin; use std::sync::Arc; use std::task::{Context, Poll}; -use cot::handle_all_parameters; -use cot::request::{Request, RequestHead}; -use cot::response::Response; use derive_more::with_trait::Debug; -use crate::Error; use crate::request::extractors::FromRequestHead; +use crate::request::{Request, RequestHead}; +use crate::response::Response; +use crate::{handle_all_parameters, Error}; /// A trait for handling error pages in Cot applications. /// diff --git a/cot-core/src/request.rs b/cot-core/src/request.rs index 32833291..0524ab66 100644 --- a/cot-core/src/request.rs +++ b/cot-core/src/request.rs @@ -13,16 +13,14 @@ //! ``` use std::future::Future; -use std::sync::Arc; #[cfg(feature = "db")] use cot::db::Database; use cot::router::Router; -use http::Extensions; use indexmap::IndexMap; use crate::request::extractors::FromRequestHead; -use crate::{Result, impl_into_cot_error}; +use crate::{impl_into_cot_error, Result}; pub mod extractors; mod path_params_deserializer; @@ -33,367 +31,6 @@ pub type Request = http::Request; /// HTTP request head type. pub type RequestHead = http::request::Parts; -mod private { - pub trait Sealed {} -} - -/// Extension trait for [`http::Request`] that provides helper methods for -/// working with HTTP requests. -/// -/// # Sealed -/// -/// This trait is sealed since it doesn't make sense to be implemented for types -/// outside the context of Cot. -pub trait RequestExt: private::Sealed { - /// Runs an extractor implementing [`FromRequestHead`] on the request. - /// - /// # Examples - /// - /// ``` - /// use cot::response::Response; - /// use cot_core::request::extractors::Path; - /// use cot_core::request::{Request, RequestExt}; - /// - /// 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 - where - E: FromRequestHead + 'static; - - /// Get the application context. - /// - /// # Examples - /// - /// ``` - /// use cot::response::Response; - /// use cot_core::request::{Request, RequestExt}; - /// - /// 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) -> &cot::project::ProjectContext; - - /// Get the project configuration. - /// - /// # Examples - /// - /// ``` - /// use cot::response::Response; - /// use cot_core::request::{Request, RequestExt}; - /// - /// 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) -> &cot::config::ProjectConfig; - - /// Get the router. - /// - /// # Examples - /// - /// ``` - /// use cot::response::Response; - /// use cot_core::request::{Request, RequestExt}; - /// - /// async fn my_handler(mut request: Request) -> cot_core::Result { - /// let router = request.router(); - /// // ... do something with the router - /// # unimplemented!() - /// } - /// ``` - #[must_use] - fn router(&self) -> &Arc; - - /// Get the app name the current route belongs to, or [`None`] if the - /// request is not routed. - /// - /// This is mainly useful for providing context to reverse redirects, where - /// you want to redirect to a route in the same app. - /// - /// # Examples - /// - /// ``` - /// use cot::response::Response; - /// use cot_core::request::{Request, RequestExt}; - /// - /// async fn my_handler(mut request: Request) -> cot_core::Result { - /// let app_name = request.app_name(); - /// // ... do something with the app name - /// # unimplemented!() - /// } - /// ``` - fn app_name(&self) -> Option<&str>; - - /// Get the route name, or [`None`] if the request is not routed or doesn't - /// have a route name. - /// - /// This is mainly useful for use in templates, where you want to know which - /// route is being rendered, for instance to mark the active tab. - /// - /// # Examples - /// - /// ``` - /// use cot::response::Response; - /// use cot_core::request::{Request, RequestExt}; - /// - /// async fn my_handler(mut request: Request) -> cot_core::Result { - /// let route_name = request.route_name(); - /// // ... do something with the route name - /// # unimplemented!() - /// } - /// ``` - #[must_use] - fn route_name(&self) -> Option<&str>; - - /// Get the path parameters. - /// - /// # Examples - /// - /// ``` - /// use cot::response::Response; - /// use cot_core::request::{Request, RequestExt}; - /// - /// async fn my_handler(mut request: Request) -> cot_core::Result { - /// let path_params = request.path_params(); - /// // ... do something with the path params - /// # unimplemented!() - /// } - /// ``` - #[must_use] - fn path_params(&self) -> &PathParams; - - /// Get the path parameters mutably. - /// - /// # Examples - /// - /// ``` - /// use cot::response::Response; - /// use cot_core::request::{Request, RequestExt}; - /// - /// async fn my_handler(mut request: Request) -> cot_core::Result { - /// let path_params = request.path_params_mut(); - /// // ... do something with the path params - /// # unimplemented!() - /// } - /// ``` - #[must_use] - fn path_params_mut(&mut self) -> &mut PathParams; - - /// Get the database. - /// - /// # Examples - /// - /// ``` - /// use cot::response::Response; - /// use cot_core::request::{Request, RequestExt}; - /// - /// async fn my_handler(mut request: Request) -> cot_core::Result { - /// let db = request.db(); - /// // ... do something with the database - /// # unimplemented!() - /// } - /// ``` - #[cfg(feature = "db")] - #[must_use] - fn db(&self) -> &Arc; - - /// Get the content type of the request. - /// - /// # Examples - /// - /// ``` - /// use cot::response::Response; - /// use cot_core::request::{Request, RequestExt}; - /// - /// async fn my_handler(mut request: Request) -> cot_core::Result { - /// let content_type = request.content_type(); - /// // ... do something with the content type - /// # unimplemented!() - /// } - /// ``` - #[must_use] - fn content_type(&self) -> Option<&http::HeaderValue>; - - /// Expect the content type of the request to be the given value. - /// - /// # Errors - /// - /// Throws an error if the content type is not the expected value. - /// - /// # Examples - /// - /// ``` - /// use cot::response::Response; - /// use cot_core::request::{Request, RequestExt}; - /// - /// 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<()> { - let content_type = self - .content_type() - .map_or("".into(), |value| String::from_utf8_lossy(value.as_bytes())); - if content_type == expected { - Ok(()) - } else { - Err(InvalidContentType { - expected, - actual: content_type.into_owned(), - } - .into()) - } - } - - #[doc(hidden)] - fn extensions(&self) -> &Extensions; -} - -impl private::Sealed for Request {} - -impl RequestExt for Request { - async fn extract_from_head(&mut self) -> Result - where - E: FromRequestHead + 'static, - { - let request = std::mem::take(self); - - let (head, body) = request.into_parts(); - let result = E::from_request_head(&head).await; - - *self = Request::from_parts(head, body); - result - } - - #[track_caller] - fn context(&self) -> &cot::project::ProjectContext { - self.extensions() - .get::>() - .expect("AppContext extension missing") - } - - fn project_config(&self) -> &cot::config::ProjectConfig { - self.context().config() - } - - fn router(&self) -> &Arc { - self.context().router() - } - - fn app_name(&self) -> Option<&str> { - self.extensions() - .get::() - .map(|AppName(name)| name.as_str()) - } - - fn route_name(&self) -> Option<&str> { - self.extensions() - .get::() - .map(|RouteName(name)| name.as_str()) - } - - #[track_caller] - fn path_params(&self) -> &PathParams { - self.extensions() - .get::() - .expect("PathParams extension missing") - } - - fn path_params_mut(&mut self) -> &mut PathParams { - self.extensions_mut().get_or_insert_default::() - } - - #[cfg(feature = "db")] - fn db(&self) -> &Arc { - self.context().database() - } - - fn content_type(&self) -> Option<&http::HeaderValue> { - self.headers().get(http::header::CONTENT_TYPE) - } - - fn extensions(&self) -> &Extensions { - self.extensions() - } -} - -impl private::Sealed for RequestHead {} - -impl RequestExt for RequestHead { - async fn extract_from_head(&mut self) -> Result - where - E: FromRequestHead + 'static, - { - E::from_request_head(self).await - } - - fn context(&self) -> &cot::project::ProjectContext { - self.extensions - .get::>() - .expect("AppContext extension missing") - } - - fn project_config(&self) -> &cot::config::ProjectConfig { - self.context().config() - } - - fn router(&self) -> &Arc { - self.context().router() - } - - fn app_name(&self) -> Option<&str> { - self.extensions - .get::() - .map(|AppName(name)| name.as_str()) - } - - fn route_name(&self) -> Option<&str> { - self.extensions - .get::() - .map(|RouteName(name)| name.as_str()) - } - - fn path_params(&self) -> &PathParams { - self.extensions - .get::() - .expect("PathParams extension missing") - } - - fn path_params_mut(&mut self) -> &mut PathParams { - self.extensions.get_or_insert_default::() - } - - #[cfg(feature = "db")] - fn db(&self) -> &Arc { - self.context().database() - } - - fn content_type(&self) -> Option<&http::HeaderValue> { - self.headers.get(http::header::CONTENT_TYPE) - } - - fn extensions(&self) -> &Extensions { - &self.extensions - } -} - -#[repr(transparent)] -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct AppName(pub String); - #[repr(transparent)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct RouteName(pub String); diff --git a/cot/src/project.rs b/cot/src/project.rs index 4b668051..3bc24ed9 100644 --- a/cot/src/project.rs +++ b/cot/src/project.rs @@ -28,11 +28,16 @@ use std::sync::Arc; use askama::Template; use async_trait::async_trait; use axum::handler::HandlerWithoutStateExt; -use cot_core::error::UncaughtPanic; use cot_core::error::error_impl::impl_into_cot_error; use cot_core::error::handler::{DynErrorPageHandler, RequestOuterError}; +use cot_core::error::UncaughtPanic; +use cot_core::handler::BoxedHandler; +use cot_core::middleware::{ + IntoCotError, IntoCotErrorLayer, IntoCotResponse, IntoCotResponseLayer, +}; use cot_core::request::{AppName, Request, RequestExt, RequestHead}; use cot_core::response::{IntoResponse, Response}; +use cot_core::router::{Route, Router, RouterService}; use derive_more::with_trait::Debug; use futures_util::FutureExt; use thiserror::Error; @@ -49,17 +54,14 @@ use crate::cli::Cli; use crate::config::DatabaseConfig; use crate::config::{AuthBackendConfig, ProjectConfig}; #[cfg(feature = "db")] -use crate::db::Database; -#[cfg(feature = "db")] use crate::db::migrations::{MigrationEngine, SyncDynMigration}; +#[cfg(feature = "db")] +use crate::db::Database; use crate::error_page::Diagnostics; use crate::html::Html; -use crate::middleware::{IntoCotError, IntoCotErrorLayer, IntoCotResponse, IntoCotResponseLayer}; use crate::static_files::StaticFile; use crate::utils::accept_header_parser::AcceptHeaderParser; -use crate::{Body, Error, cli, error_page}; -use cot_core::handler::BoxedHandler; -use cot_core::router::{Route, Router, RouterService}; +use crate::{cli, error_page, Body, Error}; /// A building block for a Cot project. /// @@ -2140,14 +2142,14 @@ mod tests { use cot_core::error::handler::{RequestError, RequestOuterError}; use cot_core::request::extractors::FromRequestHead; use tower::util::MapResultLayer; - use tower::{ServiceExt, service_fn}; + use tower::{service_fn, ServiceExt}; use super::*; - use crate::StatusCode; use crate::auth::UserId; use crate::config::SecretKey; use crate::html::Html; use crate::test::serial_guard; + use crate::StatusCode; struct TestApp; diff --git a/cot/src/request.rs b/cot/src/request.rs index a3e753ae..f63a3bb7 100644 --- a/cot/src/request.rs +++ b/cot/src/request.rs @@ -1 +1,371 @@ +use std::sync::Arc; + +use cot::db::Database; +use cot::request::extractors::InvalidContentType; +use cot_core::request::extractors::FromRequestHead; +use cot_core::request::{PathParams, Request, RequestHead, RouteName}; +use cot_core::router::Router; +use http::Extensions; + pub mod extractors; + +mod private { + pub trait Sealed {} +} + +/// Extension trait for [`http::Request`] that provides helper methods for +/// working with HTTP requests. +/// +/// # Sealed +/// +/// This trait is sealed since it doesn't make sense to be implemented for types +/// outside the context of Cot. +pub trait RequestExt: private::Sealed { + /// Runs an extractor implementing [`FromRequestHead`] on the request. + /// + /// # Examples + /// + /// ``` + /// use cot::response::Response; + /// use cot_core::request::extractors::Path; + /// use cot_core::request::{Request, RequestExt}; + /// + /// 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 + where + E: FromRequestHead + 'static; + + /// Get the application context. + /// + /// # Examples + /// + /// ``` + /// use cot::response::Response; + /// use cot_core::request::{Request, RequestExt}; + /// + /// 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) -> &cot::project::ProjectContext; + + /// Get the project configuration. + /// + /// # Examples + /// + /// ``` + /// use cot::response::Response; + /// use cot_core::request::{Request, RequestExt}; + /// + /// 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) -> &cot::config::ProjectConfig; + + /// Get the router. + /// + /// # Examples + /// + /// ``` + /// use cot::response::Response; + /// use cot_core::request::{Request, RequestExt}; + /// + /// async fn my_handler(mut request: Request) -> cot_core::Result { + /// let router = request.router(); + /// // ... do something with the router + /// # unimplemented!() + /// } + /// ``` + #[must_use] + fn router(&self) -> &Arc; + + /// Get the app name the current route belongs to, or [`None`] if the + /// request is not routed. + /// + /// This is mainly useful for providing context to reverse redirects, where + /// you want to redirect to a route in the same app. + /// + /// # Examples + /// + /// ``` + /// use cot::response::Response; + /// use cot_core::request::{Request, RequestExt}; + /// + /// async fn my_handler(mut request: Request) -> cot_core::Result { + /// let app_name = request.app_name(); + /// // ... do something with the app name + /// # unimplemented!() + /// } + /// ``` + fn app_name(&self) -> Option<&str>; + + /// Get the route name, or [`None`] if the request is not routed or doesn't + /// have a route name. + /// + /// This is mainly useful for use in templates, where you want to know which + /// route is being rendered, for instance to mark the active tab. + /// + /// # Examples + /// + /// ``` + /// use cot::response::Response; + /// use cot_core::request::{Request, RequestExt}; + /// + /// async fn my_handler(mut request: Request) -> cot_core::Result { + /// let route_name = request.route_name(); + /// // ... do something with the route name + /// # unimplemented!() + /// } + /// ``` + #[must_use] + fn route_name(&self) -> Option<&str>; + + /// Get the path parameters. + /// + /// # Examples + /// + /// ``` + /// use cot::response::Response; + /// use cot_core::request::{Request, RequestExt}; + /// + /// async fn my_handler(mut request: Request) -> cot_core::Result { + /// let path_params = request.path_params(); + /// // ... do something with the path params + /// # unimplemented!() + /// } + /// ``` + #[must_use] + fn path_params(&self) -> &PathParams; + + /// Get the path parameters mutably. + /// + /// # Examples + /// + /// ``` + /// use cot::response::Response; + /// use cot_core::request::{Request, RequestExt}; + /// + /// async fn my_handler(mut request: Request) -> cot_core::Result { + /// let path_params = request.path_params_mut(); + /// // ... do something with the path params + /// # unimplemented!() + /// } + /// ``` + #[must_use] + fn path_params_mut(&mut self) -> &mut PathParams; + + /// Get the database. + /// + /// # Examples + /// + /// ``` + /// use cot::response::Response; + /// use cot_core::request::{Request, RequestExt}; + /// + /// async fn my_handler(mut request: Request) -> cot_core::Result { + /// let db = request.db(); + /// // ... do something with the database + /// # unimplemented!() + /// } + /// ``` + #[cfg(feature = "db")] + #[must_use] + fn db(&self) -> &Arc; + + /// Get the content type of the request. + /// + /// # Examples + /// + /// ``` + /// use cot::response::Response; + /// use cot_core::request::{Request, RequestExt}; + /// + /// async fn my_handler(mut request: Request) -> cot_core::Result { + /// let content_type = request.content_type(); + /// // ... do something with the content type + /// # unimplemented!() + /// } + /// ``` + #[must_use] + fn content_type(&self) -> Option<&http::HeaderValue>; + + /// Expect the content type of the request to be the given value. + /// + /// # Errors + /// + /// Throws an error if the content type is not the expected value. + /// + /// # Examples + /// + /// ``` + /// use cot::response::Response; + /// use cot_core::request::{Request, RequestExt}; + /// + /// 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) -> cot_core::Result<()> { + let content_type = self + .content_type() + .map_or("".into(), |value| String::from_utf8_lossy(value.as_bytes())); + if content_type == expected { + Ok(()) + } else { + Err(InvalidContentType { + expected, + actual: content_type.into_owned(), + } + .into()) + } + } + + #[doc(hidden)] + fn extensions(&self) -> &Extensions; +} + +impl private::Sealed for Request {} + +impl RequestExt for Request { + async fn extract_from_head(&mut self) -> cot_core::Result + where + E: FromRequestHead + 'static, + { + let request = std::mem::take(self); + + let (head, body) = request.into_parts(); + let result = E::from_request_head(&head).await; + + *self = Request::from_parts(head, body); + result + } + + #[track_caller] + fn context(&self) -> &cot::project::ProjectContext { + self.extensions() + .get::>() + .expect("AppContext extension missing") + } + + fn project_config(&self) -> &cot::config::ProjectConfig { + self.context().config() + } + + fn router(&self) -> &Arc { + self.context().router() + } + + fn app_name(&self) -> Option<&str> { + self.extensions() + .get::() + .map(|AppName(name)| name.as_str()) + } + + fn route_name(&self) -> Option<&str> { + self.extensions() + .get::() + .map(|RouteName(name)| name.as_str()) + } + + #[track_caller] + fn path_params(&self) -> &PathParams { + self.extensions() + .get::() + .expect("PathParams extension missing") + } + + fn path_params_mut(&mut self) -> &mut PathParams { + self.extensions_mut().get_or_insert_default::() + } + + #[cfg(feature = "db")] + fn db(&self) -> &Arc { + self.context().database() + } + + fn content_type(&self) -> Option<&http::HeaderValue> { + self.headers().get(http::header::CONTENT_TYPE) + } + + fn extensions(&self) -> &Extensions { + self.extensions() + } +} + +impl private::Sealed for RequestHead {} + +impl RequestExt for RequestHead { + async fn extract_from_head(&mut self) -> cot_core::Result + where + E: FromRequestHead + 'static, + { + E::from_request_head(self).await + } + + fn context(&self) -> &cot::project::ProjectContext { + self.extensions + .get::>() + .expect("AppContext extension missing") + } + + fn project_config(&self) -> &cot::config::ProjectConfig { + self.context().config() + } + + fn router(&self) -> &Arc { + self.context().router() + } + + fn app_name(&self) -> Option<&str> { + self.extensions + .get::() + .map(|AppName(name)| name.as_str()) + } + + fn route_name(&self) -> Option<&str> { + self.extensions + .get::() + .map(|RouteName(name)| name.as_str()) + } + + fn path_params(&self) -> &PathParams { + self.extensions + .get::() + .expect("PathParams extension missing") + } + + fn path_params_mut(&mut self) -> &mut PathParams { + self.extensions.get_or_insert_default::() + } + + #[cfg(feature = "db")] + fn db(&self) -> &Arc { + self.context().database() + } + + fn content_type(&self) -> Option<&http::HeaderValue> { + self.headers.get(http::header::CONTENT_TYPE) + } + + fn extensions(&self) -> &Extensions { + &self.extensions + } +} + +#[repr(transparent)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct AppName(pub String); From f38a2e725016d04f134ad54bfb7682499f149c31 Mon Sep 17 00:00:00 2001 From: Marek 'seqre' Grzelak Date: Mon, 29 Dec 2025 12:38:39 +0100 Subject: [PATCH 10/18] move i have no idea what anymore --- Cargo.lock | 2 + Cargo.toml | 1 + cot-core/Cargo.toml | 8 + {cot => cot-core}/src/html.rs | 28 +-- cot-core/src/lib.rs | 1 + cot-core/src/request.rs | 172 +----------------- cot-core/src/request/extractors.rs | 79 +------- cot-core/src/response/into_response.rs | 32 +--- cot-core/src/router.rs | 8 +- cot-core/src/router/method.rs | 2 +- cot/src/admin.rs | 4 +- cot/src/auth/db.rs | 10 +- cot/src/form/fields.rs | 2 +- cot/src/form/fields/chrono.rs | 2 +- cot/src/form/fields/files.rs | 2 +- cot/src/form/fields/select.rs | 2 +- cot/src/lib.rs | 3 +- cot/src/middleware.rs | 2 +- cot/src/openapi.rs | 35 ++-- .../openapi.rs => cot/src/openapi/method.rs | 44 +++-- cot/src/openapi/swagger_ui.rs | 4 +- cot/src/project.rs | 20 +- cot/src/request.rs | 169 ++++++++++++++++- cot/src/request/extractors.rs | 85 ++++++++- cot/src/response/into_response.rs | 29 ++- cot/src/session.rs | 12 +- cot/src/test.rs | 21 +-- cot/tests/openapi.rs | 4 +- cot/tests/project.rs | 2 +- cot/tests/router.rs | 2 +- examples/admin/src/main.rs | 4 +- examples/custom-error-pages/src/main.rs | 2 +- examples/file-upload/src/main.rs | 2 +- examples/forms/src/main.rs | 2 +- examples/json/src/main.rs | 2 +- examples/sessions/src/main.rs | 6 +- examples/todo-list/src/main.rs | 2 +- 37 files changed, 405 insertions(+), 402 deletions(-) rename {cot => cot-core}/src/html.rs (96%) rename cot-core/src/router/method/openapi.rs => cot/src/openapi/method.rs (97%) diff --git a/Cargo.lock b/Cargo.lock index e11911f0..358a2601 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -769,6 +769,7 @@ dependencies = [ name = "cot_core" version = "0.4.0" dependencies = [ + "aide", "askama", "async-stream", "axum", @@ -784,6 +785,7 @@ dependencies = [ "http-body", "http-body-util", "indexmap", + "schemars", "serde", "serde_html_form", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index f42fb70a..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", diff --git a/cot-core/Cargo.toml b/cot-core/Cargo.toml index 702914a7..1299c951 100644 --- a/cot-core/Cargo.toml +++ b/cot-core/Cargo.toml @@ -16,6 +16,7 @@ authors.workspace = true workspace = true [dependencies] +aide = { workspace = true, optional = true } askama = { workspace = true, features = ["derive", "std"] } axum = { workspace = true, features = ["http1", "tokio"] } backtrace.workspace = true @@ -29,6 +30,7 @@ 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 } @@ -43,3 +45,9 @@ tracing = "0.1.41" async-stream.workspace = true 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/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 index 8350de6a..4a1db090 100644 --- a/cot-core/src/lib.rs +++ b/cot-core/src/lib.rs @@ -12,6 +12,7 @@ pub mod request; pub mod response; #[macro_use] pub mod handler; +pub mod html; pub mod middleware; pub mod router; diff --git a/cot-core/src/request.rs b/cot-core/src/request.rs index 0524ab66..41fc52d5 100644 --- a/cot-core/src/request.rs +++ b/cot-core/src/request.rs @@ -14,13 +14,10 @@ use std::future::Future; -#[cfg(feature = "db")] -use cot::db::Database; -use cot::router::Router; use indexmap::IndexMap; +use crate::impl_into_cot_error; use crate::request::extractors::FromRequestHead; -use crate::{impl_into_cot_error, Result}; pub mod extractors; mod path_params_deserializer; @@ -266,7 +263,7 @@ impl PathParams { /// ``` pub fn parse<'de, T: serde::Deserialize<'de>>( &'de self, - ) -> std::result::Result { + ) -> Result { let deserializer = path_params_deserializer::PathParamsDeserializer::new(self); serde_path_to_error::deserialize(deserializer).map_err(PathParamsDeserializerError) } @@ -282,14 +279,13 @@ pub struct PathParamsDeserializerError( ); 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 cot::response::Response; - use cot::router::{Route, Router}; - use cot::test::TestRequestBuilder; - use super::*; - use crate::request::extractors::Path; #[test] fn path_params() { @@ -322,162 +318,6 @@ mod tests { ); } - #[test] - fn request_ext_app_name() { - let mut request = TestRequestBuilder::get("/").build(); - assert_eq!(request.app_name(), None); - - request - .extensions_mut() - .insert(AppName("test_app".to_string())); - assert_eq!(request.app_name(), Some("test_app")); - } - - #[test] - fn request_ext_route_name() { - let mut request = TestRequestBuilder::get("/").build(); - assert_eq!(request.route_name(), None); - - request - .extensions_mut() - .insert(RouteName("test_route".to_string())); - assert_eq!(request.route_name(), Some("test_route")); - } - - #[test] - fn request_ext_parts_route_name() { - let request = TestRequestBuilder::get("/").build(); - let (mut head, _body) = request.into_parts(); - assert_eq!(head.route_name(), None); - - head.extensions.insert(RouteName("test_route".to_string())); - assert_eq!(head.route_name(), Some("test_route")); - } - - #[test] - fn request_ext_path_params() { - let mut request = TestRequestBuilder::get("/").build(); - - let mut params = PathParams::new(); - params.insert("id".to_string(), "42".to_string()); - request.extensions_mut().insert(params); - - assert_eq!(request.path_params().get("id"), Some("42")); - } - - #[test] - fn request_ext_path_params_mut() { - let mut request = TestRequestBuilder::get("/").build(); - - request - .path_params_mut() - .insert("id".to_string(), "42".to_string()); - - assert_eq!(request.path_params().get("id"), Some("42")); - } - - #[test] - fn request_ext_content_type() { - let mut request = TestRequestBuilder::get("/").build(); - assert_eq!(request.content_type(), None); - - request.headers_mut().insert( - http::header::CONTENT_TYPE, - http::HeaderValue::from_static("text/plain"), - ); - - assert_eq!( - request.content_type(), - Some(&http::HeaderValue::from_static("text/plain")) - ); - } - - #[test] - fn request_ext_expect_content_type() { - let mut request = TestRequestBuilder::get("/").build(); - - // Should fail with no content type - assert!(request.expect_content_type("text/plain").is_err()); - - request.headers_mut().insert( - http::header::CONTENT_TYPE, - http::HeaderValue::from_static("text/plain"), - ); - - // Should succeed with matching content type - assert!(request.expect_content_type("text/plain").is_ok()); - - // Should fail with non-matching content type - assert!(request.expect_content_type("application/json").is_err()); - } - - #[cot_macros::test] - async fn request_ext_extract_from_head() { - async fn handler(mut request: Request) -> Result { - let Path(id): Path = request.extract_from_head().await?; - assert_eq!(id, "42"); - - Ok(Response::new(Body::empty())) - } - - let router = Router::with_urls([Route::with_handler("/{id}/", handler)]); - - let request = TestRequestBuilder::get("/42/") - .router(router.clone()) - .build(); - - router.handle(request).await.unwrap(); - } - - #[test] - fn parts_ext_path_params() { - 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); - - assert_eq!(head.path_params().get("id"), Some("42")); - } - - #[test] - fn parts_ext_mutating_path_params() { - let (mut head, _) = Request::new(crate::Body::empty()).into_parts(); - head.path_params_mut() - .insert("page".to_string(), "1".to_string()); - - assert_eq!(head.path_params().get("page"), Some("1")); - } - - #[test] - fn parts_ext_app_name() { - 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")); - } - - #[test] - fn parts_ext_route_name() { - 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")); - } - - #[test] - fn parts_ext_content_type() { - let (mut head, _) = Request::new(crate::Body::empty()).into_parts(); - head.headers.insert( - http::header::CONTENT_TYPE, - http::HeaderValue::from_static("text/plain"), - ); - - assert_eq!( - head.content_type(), - Some(&http::HeaderValue::from_static("text/plain")) - ); - } - #[cot_macros::test] async fn path_extract_from_head() { let (mut head, _) = Request::new(Body::empty()).into_parts(); diff --git a/cot-core/src/request/extractors.rs b/cot-core/src/request/extractors.rs index 0c1c3098..26342822 100644 --- a/cot-core/src/request/extractors.rs +++ b/cot-core/src/request/extractors.rs @@ -50,14 +50,10 @@ use std::future::Future; -use cot::auth::Auth; -use cot::form::{Form, FormResult}; -use cot::router::Urls; -use cot::session::Session; use serde::de::DeserializeOwned; use tower_sessions::Session; -pub use crate::request::{PathParams, Request, RequestExt, RequestHead}; +pub use crate::request::{PathParams, Request, RequestHead}; use crate::{Body, Method}; /// Trait for extractors that consume the request body. @@ -105,12 +101,6 @@ pub trait FromRequestHead: Sized { fn from_request_head(head: &RequestHead) -> impl Future> + Send; } -impl FromRequestHead for Urls { - 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 @@ -231,61 +221,6 @@ where struct QueryParametersParseError(serde_path_to_error::Error); impl_into_cot_error!(QueryParametersParseError, BAD_REQUEST); -/// An extractor that gets the request body as form data and deserializes it -/// into a type `F` implementing `cot::form::Form`. -/// -/// The content type of the request must be `application/x-www-form-urlencoded`. -/// -/// # Errors -/// -/// Throws an error if the content type is not -/// `application/x-www-form-urlencoded`. 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 form data is invalid or because the -/// deserialization to the target structure failed. -/// -/// # Example -/// -/// ``` -/// use cot::form::{Form, FormResult}; -/// use cot::html::Html; -/// use cot::test::TestRequestBuilder; -/// use cot_core::request::extractors::RequestForm; -/// -/// #[derive(Form)] -/// struct MyForm { -/// hello: String, -/// } -/// -/// async fn my_handler(RequestForm(form): RequestForm) -> Html { -/// let form = match form { -/// FormResult::Ok(form) => form, -/// FormResult::ValidationError(error) => { -/// panic!("Form validation error!") -/// } -/// }; -/// -/// Html::new(format!("Hello {}!", form.hello)) -/// } -/// -/// # #[tokio::main] -/// # async fn main() -> cot::Result<()> { -/// # use cot::RequestHandler; -/// # let request = TestRequestBuilder::post("/").form_data(&[("hello", "world")]).build(); -/// # my_handler.handle(request).await?; -/// # Ok(()) -/// # } -/// ``` -#[derive(Debug)] -pub struct RequestForm(pub FormResult); - -impl FromRequest for RequestForm { - 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?)) - } -} - // extractor impls for existing types impl FromRequestHead for RequestHead { async fn from_request_head(head: &RequestHead) -> crate::Result { @@ -305,18 +240,6 @@ impl FromRequestHead for Session { } } -impl FromRequestHead for Auth { - async fn from_request_head(head: &RequestHead) -> crate::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. /// diff --git a/cot-core/src/response/into_response.rs b/cot-core/src/response/into_response.rs index 1f281df9..77681fa2 100644 --- a/cot-core/src/response/into_response.rs +++ b/cot-core/src/response/into_response.rs @@ -1,8 +1,7 @@ use bytes::{Bytes, BytesMut}; -use cot::html::Html; use http; -use crate::headers::{HTML_CONTENT_TYPE, OCTET_STREAM_CONTENT_TYPE, PLAIN_TEXT_CONTENT_TYPE}; +use crate::headers::{OCTET_STREAM_CONTENT_TYPE, PLAIN_TEXT_CONTENT_TYPE}; use crate::response::Response; use crate::{Body, Error, StatusCode}; @@ -308,34 +307,6 @@ impl IntoResponse for crate::response::ResponseHead { } } -// 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_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() - } -} - -// Shortcuts for common uses - impl IntoResponse for Body { fn into_response(self) -> crate::Result { Ok(Response::new(self)) @@ -345,7 +316,6 @@ impl IntoResponse for Body { #[cfg(test)] mod tests { use bytes::{Bytes, BytesMut}; - use cot::html::Html; use http::{self, HeaderMap, HeaderValue}; use super::*; diff --git a/cot-core/src/router.rs b/cot-core/src/router.rs index 65ef738e..4df621fe 100644 --- a/cot-core/src/router.rs +++ b/cot-core/src/router.rs @@ -27,13 +27,12 @@ use std::pin::Pin; use std::sync::Arc; use std::task::{Context, Poll}; -use cot::project; use derive_more::with_trait::Debug; use tracing::debug; use crate::error::NotFound; use crate::handler::{into_box_request_handler, BoxRequestHandler, RequestHandler}; -use crate::request::{AppName, PathParams, Request, RequestExt, RequestHead, RouteName}; +use crate::request::{AppName, PathParams, Request, RequestHead, RouteName}; use crate::response::Response; use crate::router::path::{CaptureResult, PathMatcher, ReverseParamMap}; use crate::{impl_into_cot_error, Error, Result}; @@ -619,7 +618,7 @@ impl Route { /// ``` /// use cot_core::request::Request; /// use cot_core::response::Response; - /// use cot_core::router::method::openapi::api_get; + /// use cot_core::router::method::method::api_get; /// use cot_core::router::{Route, Router}; /// /// async fn home(request: Request) -> cot::Result { @@ -1029,9 +1028,6 @@ macro_rules! reverse_redirect { #[cfg(test)] mod tests { - use cot::html::Html; - use cot::test::TestRequestBuilder; - use super::*; use crate::request::Request; use crate::response::{IntoResponse, Response}; diff --git a/cot-core/src/router/method.rs b/cot-core/src/router/method.rs index b598aa8d..0e98bd54 100644 --- a/cot-core/src/router/method.rs +++ b/cot-core/src/router/method.rs @@ -420,10 +420,10 @@ async fn default_fallback(method: Method) -> crate::Error { #[cfg(test)] mod tests { - use cot::html::Html; use cot::test::TestRequestBuilder; use super::*; + use crate::html::Html; use crate::StatusCode; async fn test_handler(method: Method) -> Html { diff --git a/cot/src/admin.rs b/cot/src/admin.rs index fc8e6505..2db0a565 100644 --- a/cot/src/admin.rs +++ b/cot/src/admin.rs @@ -10,9 +10,11 @@ use askama::Template; use async_trait::async_trait; use bytes::Bytes; use cot_core::error::NotFound; +use cot_core::html::Html; use cot_core::request::extractors::{FromRequestHead, Path, StaticFiles, UrlQuery}; use cot_core::request::{Request, RequestExt, RequestHead}; use cot_core::response::{IntoResponse, Response}; +use cot_core::router::{Router, Urls}; /// Implements the [`AdminModel`] trait for a struct. /// /// This is a simple method for adding a database model to the admin panel. @@ -29,10 +31,8 @@ use crate::common_types::Password; use crate::form::{ Form, FormContext, FormErrorTarget, FormField, FormFieldValidationError, FormResult, }; -use crate::html::Html; use crate::static_files::StaticFile; use crate::{App, Error, Method, RequestHandler, reverse_redirect}; -use cot_core::router::{Router, Urls}; struct AdminAuthenticated(H, PhantomData T>); 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/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/lib.rs b/cot/src/lib.rs index 78da9232..51ee07c4 100644 --- a/cot/src/lib.rs +++ b/cot/src/lib.rs @@ -66,7 +66,6 @@ pub mod cli; pub mod common_types; pub mod config; mod error_page; -pub mod html; #[cfg(feature = "json")] pub mod json; pub mod middleware; @@ -89,6 +88,7 @@ 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}; pub use cot_core::{Body, Method}; /// An attribute macro that defines an end-to-end test function for a /// Cot-powered app. @@ -160,4 +160,3 @@ pub use {bytes, cot_core as core, http}; pub use crate::project::{ App, AppBuilder, Bootstrapper, Project, ProjectContext, run, run_at, run_cli, }; -pub use cot_core::handler::{BoxedHandler, RequestHandler}; diff --git a/cot/src/middleware.rs b/cot/src/middleware.rs index bf867f6a..0202799e 100644 --- a/cot/src/middleware.rs +++ b/cot/src/middleware.rs @@ -22,6 +22,7 @@ use tower_sessions::{SessionManagerLayer, SessionStore}; use crate::config::CacheType; use crate::config::{Expiry, SameSite, SessionStoreTypeConfig}; use crate::project::MiddlewareContext; +use crate::session::store::SessionStoreWrapper; #[cfg(all(feature = "db", feature = "json"))] use crate::session::store::db::DbStore; #[cfg(feature = "json")] @@ -29,7 +30,6 @@ use crate::session::store::file::FileStore; use crate::session::store::memory::MemoryStore; #[cfg(feature = "redis")] use crate::session::store::redis::RedisStore; -use crate::session::store::SessionStoreWrapper; use crate::{Body, Error}; #[cfg(feature = "live-reload")] diff --git a/cot/src/openapi.rs b/cot/src/openapi.rs index 2052494b..5f698cdd 100644 --- a/cot/src/openapi.rs +++ b/cot/src/openapi.rs @@ -9,13 +9,13 @@ //! //! 1. Add [`#[derive(schemars::JsonSchema)]`](schemars::JsonSchema) to the //! types used in the extractors and response types. -//! 2. Use [`ApiMethodRouter`](cot_core::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`](cot_core::router::method::openapi::api_get) or -//! [`api_post`](cot_core::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`](cot_core::router::method::openapi::ApiMethodRouter)s +//! [`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). @@ -34,10 +34,10 @@ //! use cot::openapi::swagger_ui::SwaggerUi; //! use cot::project::{MiddlewareContext, RegisterAppsContext, RootHandler, RootHandlerBuilder}; //! use cot::response::{Response, ResponseExt}; -//! use cot_core::router::method::openapi::api_post; -//! use cot_core::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)] @@ -103,6 +103,7 @@ //! # } //! ``` +pub mod method; #[cfg(feature = "swagger-ui")] pub mod swagger_ui; @@ -113,6 +114,8 @@ use aide::openapi::{ MediaType, Operation, Parameter, ParameterData, ParameterSchemaOrContent, PathItem, PathStyle, QueryStyle, ReferenceOr, RequestBody, StatusCode, }; +use cot_core::handler::BoxRequestHandler; +use cot_core::router::Urls; /// Derive macro for the [`ApiOperationResponse`] trait. /// /// This macro can be applied to enums to automatically implement the @@ -213,8 +216,6 @@ use crate::request::{Request, RequestHead}; use crate::response::{Response, WithExtension}; use crate::session::Session; use crate::{Body, Method, RequestHandler}; -use cot_core::handler::BoxRequestHandler; -use cot_core::router::Urls; /// Context for API route generation. /// @@ -259,7 +260,7 @@ impl Default for RouteContext<'_> { /// 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`](cot_core::router::method::openapi::ApiMethodRouter). +/// to use [`ApiMethodRouter`](cot_core::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. /// @@ -290,7 +291,7 @@ impl Default for RouteContext<'_> { /// } /// /// # assert_eq!( -/// # RouteWrapper(cot_core::router::method::openapi::ApiMethodRouter::new()) +/// # RouteWrapper(cot_core::router::method::method::ApiMethodRouter::new()) /// # .as_api_route(&RouteContext::new(), &mut SchemaGenerator::default()) /// # .summary, /// # Some("This route was wrapped with RouteWrapper".to_owned()) @@ -322,7 +323,7 @@ pub trait AsApiRoute { /// } /// /// # assert_eq!( - /// # RouteWrapper(cot_core::router::method::openapi::ApiMethodRouter::new()) + /// # RouteWrapper(cot_core::router::method::method::ApiMethodRouter::new()) /// # .as_api_route(&RouteContext::new(), &mut SchemaGenerator::default()) /// # .summary, /// # Some("This route was wrapped with RouteWrapper".to_owned()) @@ -513,7 +514,7 @@ where /// use cot::request::extractors::FromRequestHead; /// use cot::response::Response; /// use cot_core::router::Route; -/// use cot_core::router::method::openapi::api_get; +/// use cot_core::router::method::method::api_get; /// /// struct MyExtractor; /// impl FromRequestHead for MyExtractor { @@ -529,15 +530,17 @@ where /// # unimplemented!() /// } /// -/// let router = -/// cot_core::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_core::router::Route; -/// use cot_core::router::method::openapi::api_get; +/// use cot_core::router::method::method::api_get; /// /// async fn handler_with_openapi() -> cot::Result { /// // ... @@ -1140,11 +1143,11 @@ where #[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}; diff --git a/cot-core/src/router/method/openapi.rs b/cot/src/openapi/method.rs similarity index 97% rename from cot-core/src/router/method/openapi.rs rename to cot/src/openapi/method.rs index ffa43996..0e9446fb 100644 --- a/cot-core/src/router/method/openapi.rs +++ b/cot/src/openapi/method.rs @@ -6,18 +6,16 @@ use std::fmt::{Debug, Formatter}; -use crate::response::Response; use aide::openapi::Operation; -use cot::openapi::RouteContext; +use cot::openapi::{ + AsApiOperation, AsApiRoute, BoxApiRequestHandler, RouteContext, into_box_api_request_handler, +}; use cot::request::Request; -use cot_core::router::method::InnerHandler; +use cot_core::router::method::{InnerHandler, InnerMethodRouter}; use schemars::SchemaGenerator; use crate::handler::RequestHandler; -use cot::openapi::{ - into_box_api_request_handler, AsApiOperation, AsApiRoute, BoxApiRequestHandler, -}; -use cot_core::router::method::InnerMethodRouter; +use crate::response::Response; /// A version of [`MethodRouter`](crate::router::method::MethodRouter) that /// supports OpenAPI. @@ -47,9 +45,9 @@ use cot_core::router::method::InnerMethodRouter; /// /// ``` /// use cot::json::Json; -/// use cot_core::router::method::openapi::api_post; -/// use cot_core::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_core::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_core::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::test::TestRequestBuilder; /// use cot_core::router::method::MethodRouter; - /// use cot_core::router::method::openapi::ApiMethodRouter; + /// use cot_core::router::method::method::ApiMethodRouter; /// use cot_core::router::{Route, Router}; - /// use cot::test::TestRequestBuilder; /// /// async fn test_handler() -> Json<()> { /// Json(()) @@ -232,7 +230,7 @@ impl ApiMethodRouter { /// /// ``` /// use cot::json::Json; - /// use cot_core::router::method::openapi::ApiMethodRouter; + /// use cot_core::router::method::method::ApiMethodRouter; /// /// async fn test_handler() -> Json<()> { /// Json(()) @@ -276,10 +274,10 @@ impl ApiMethodRouter { /// /// ``` /// use cot::html::Html; - /// use cot_core::router::method::openapi::ApiMethodRouter; - /// use cot_core::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") @@ -477,8 +475,8 @@ define_method_router!(api_trace, trace => TRACE); /// /// ``` /// use cot::html::Html; -/// use cot_core::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") @@ -517,15 +515,15 @@ where #[cfg(test)] mod tests { - use crate::error::MethodNotAllowed; - use crate::response::{IntoResponse, Response}; - - use super::*; - use crate::request::extractors::Path; use cot::html::Html; use cot::json::Json; use cot::test::TestRequestBuilder; + use super::*; + use crate::error::MethodNotAllowed; + use crate::request::extractors::Path; + use crate::response::{IntoResponse, Response}; + async fn test_handler(method: Method) -> crate::Result { Html::new(method.as_str()).into_response() } diff --git a/cot/src/openapi/swagger_ui.rs b/cot/src/openapi/swagger_ui.rs index a0190c34..7caa0eb1 100644 --- a/cot/src/openapi/swagger_ui.rs +++ b/cot/src/openapi/swagger_ui.rs @@ -6,15 +6,15 @@ 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::static_files::StaticFile; -use cot_core::router::{Route, Router}; /// A wrapper around the Swagger UI functionality. /// diff --git a/cot/src/project.rs b/cot/src/project.rs index 3bc24ed9..c8e8cab6 100644 --- a/cot/src/project.rs +++ b/cot/src/project.rs @@ -28,10 +28,11 @@ use std::sync::Arc; use askama::Template; use async_trait::async_trait; use axum::handler::HandlerWithoutStateExt; +use cot_core::error::UncaughtPanic; use cot_core::error::error_impl::impl_into_cot_error; use cot_core::error::handler::{DynErrorPageHandler, RequestOuterError}; -use cot_core::error::UncaughtPanic; use cot_core::handler::BoxedHandler; +use cot_core::html::Html; use cot_core::middleware::{ IntoCotError, IntoCotErrorLayer, IntoCotResponse, IntoCotResponseLayer, }; @@ -54,14 +55,13 @@ use crate::cli::Cli; use crate::config::DatabaseConfig; use crate::config::{AuthBackendConfig, ProjectConfig}; #[cfg(feature = "db")] -use crate::db::migrations::{MigrationEngine, SyncDynMigration}; -#[cfg(feature = "db")] use crate::db::Database; +#[cfg(feature = "db")] +use crate::db::migrations::{MigrationEngine, SyncDynMigration}; use crate::error_page::Diagnostics; -use crate::html::Html; use crate::static_files::StaticFile; use crate::utils::accept_header_parser::AcceptHeaderParser; -use crate::{cli, error_page, Body, Error}; +use crate::{Body, Error, cli, error_page}; /// A building block for a Cot project. /// @@ -117,7 +117,7 @@ pub trait App: Send + Sync { /// /// ``` /// use cot::App; - /// use cot::html::Html; + /// use cot_core::html::Html; /// use cot_core::router::{Route, Router}; /// /// async fn index() -> Html { @@ -409,8 +409,8 @@ pub trait Project { /// /// ``` /// use cot::Project; - /// use cot::html::Html; /// use cot_core::error::handler::{DynErrorPageHandler, RequestError}; + /// use cot_core::html::Html; /// use cot_core::response::IntoResponse; /// /// struct MyProject; @@ -2140,16 +2140,16 @@ 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::{service_fn, ServiceExt}; + use tower::{ServiceExt, service_fn}; use super::*; + use crate::StatusCode; use crate::auth::UserId; use crate::config::SecretKey; - use crate::html::Html; use crate::test::serial_guard; - use crate::StatusCode; struct TestApp; diff --git a/cot/src/request.rs b/cot/src/request.rs index f63a3bb7..b2409d67 100644 --- a/cot/src/request.rs +++ b/cot/src/request.rs @@ -366,6 +366,169 @@ impl RequestExt for RequestHead { } } -#[repr(transparent)] -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct AppName(pub String); +#[cfg(test)] +mod tests { + use cot::test::TestRequestBuilder; + use cot_core::Body; + use cot_core::request::extractors::Path; + use cot_core::response::Response; + use cot_core::router::Route; + + use super::*; + + #[test] + fn request_ext_app_name() { + let mut request = TestRequestBuilder::get("/").build(); + assert_eq!(request.app_name(), None); + + request + .extensions_mut() + .insert(AppName("test_app".to_string())); + assert_eq!(request.app_name(), Some("test_app")); + } + + #[test] + fn request_ext_route_name() { + let mut request = TestRequestBuilder::get("/").build(); + assert_eq!(request.route_name(), None); + + request + .extensions_mut() + .insert(RouteName("test_route".to_string())); + assert_eq!(request.route_name(), Some("test_route")); + } + + #[test] + fn request_ext_parts_route_name() { + let request = TestRequestBuilder::get("/").build(); + let (mut head, _body) = request.into_parts(); + assert_eq!(head.route_name(), None); + + head.extensions.insert(RouteName("test_route".to_string())); + assert_eq!(head.route_name(), Some("test_route")); + } + + #[test] + fn request_ext_path_params() { + let mut request = TestRequestBuilder::get("/").build(); + + let mut params = PathParams::new(); + params.insert("id".to_string(), "42".to_string()); + request.extensions_mut().insert(params); + + assert_eq!(request.path_params().get("id"), Some("42")); + } + + #[test] + fn request_ext_path_params_mut() { + let mut request = TestRequestBuilder::get("/").build(); + + request + .path_params_mut() + .insert("id".to_string(), "42".to_string()); + + assert_eq!(request.path_params().get("id"), Some("42")); + } + + #[test] + fn request_ext_content_type() { + let mut request = TestRequestBuilder::get("/").build(); + assert_eq!(request.content_type(), None); + + request.headers_mut().insert( + http::header::CONTENT_TYPE, + http::HeaderValue::from_static("text/plain"), + ); + + assert_eq!( + request.content_type(), + Some(&http::HeaderValue::from_static("text/plain")) + ); + } + + #[test] + fn request_ext_expect_content_type() { + let mut request = TestRequestBuilder::get("/").build(); + + // Should fail with no content type + assert!(request.expect_content_type("text/plain").is_err()); + + request.headers_mut().insert( + http::header::CONTENT_TYPE, + http::HeaderValue::from_static("text/plain"), + ); + + // Should succeed with matching content type + assert!(request.expect_content_type("text/plain").is_ok()); + + // Should fail with non-matching content type + assert!(request.expect_content_type("application/json").is_err()); + } + + #[cot_macros::test] + async fn request_ext_extract_from_head() { + async fn handler(mut request: Request) -> cot_core::Result { + let Path(id): Path = request.extract_from_head().await?; + assert_eq!(id, "42"); + + Ok(Response::new(Body::empty())) + } + + let router = Router::with_urls([Route::with_handler("/{id}/", handler)]); + + let request = TestRequestBuilder::get("/42/") + .router(router.clone()) + .build(); + + router.handle(request).await.unwrap(); + } + + #[test] + fn parts_ext_path_params() { + 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); + + assert_eq!(head.path_params().get("id"), Some("42")); + } + + #[test] + fn parts_ext_mutating_path_params() { + let (mut head, _) = Request::new(crate::Body::empty()).into_parts(); + head.path_params_mut() + .insert("page".to_string(), "1".to_string()); + + assert_eq!(head.path_params().get("page"), Some("1")); + } + + #[test] + fn parts_ext_app_name() { + 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")); + } + + #[test] + fn parts_ext_route_name() { + 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")); + } + + #[test] + fn parts_ext_content_type() { + let (mut head, _) = Request::new(crate::Body::empty()).into_parts(); + head.headers.insert( + http::header::CONTENT_TYPE, + http::HeaderValue::from_static("text/plain"), + ); + + assert_eq!( + head.content_type(), + Some(&http::HeaderValue::from_static("text/plain")) + ); + } +} diff --git a/cot/src/request/extractors.rs b/cot/src/request/extractors.rs index 851ec049..172ccf7e 100644 --- a/cot/src/request/extractors.rs +++ b/cot/src/request/extractors.rs @@ -2,11 +2,13 @@ use std::sync::Arc; #[cfg(feature = "json")] use cot::json::Json; -use cot_core::request::RequestExt; +use cot_core::request::Request; use cot_core::request::extractors::{FromRequest, FromRequestHead, RequestHead}; use cot_core::{Body, impl_into_cot_error}; use serde::de::DeserializeOwned; +use crate::request::RequestExt; + #[derive(Debug, thiserror::Error)] #[error("invalid content type; expected `{expected}`, found `{actual}`")] pub struct InvalidContentType { @@ -15,6 +17,79 @@ pub struct InvalidContentType { } impl_into_cot_error!(InvalidContentType, BAD_REQUEST); +impl FromRequestHead for Urls { + async fn from_request_head(head: &RequestHead) -> crate::Result { + Ok(Self::from_parts(head)) + } +} + +impl FromRequestHead for Auth { + async fn from_request_head(head: &RequestHead) -> crate::Result { + let auth = head + .extensions + .get::() + .expect("AuthMiddleware not enabled for the route/project") + .clone(); + + Ok(auth) + } +} + +/// An extractor that gets the request body as form data and deserializes it +/// into a type `F` implementing `cot::form::Form`. +/// +/// The content type of the request must be `application/x-www-form-urlencoded`. +/// +/// # Errors +/// +/// Throws an error if the content type is not +/// `application/x-www-form-urlencoded`. 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 form data is invalid or because the +/// deserialization to the target structure failed. +/// +/// # Example +/// +/// ``` +/// use cot::form::{Form, FormResult}; +/// use cot::test::TestRequestBuilder; +/// use cot_core::html::Html; +/// use cot_core::request::extractors::RequestForm; +/// +/// #[derive(Form)] +/// struct MyForm { +/// hello: String, +/// } +/// +/// async fn my_handler(RequestForm(form): RequestForm) -> Html { +/// let form = match form { +/// FormResult::Ok(form) => form, +/// FormResult::ValidationError(error) => { +/// panic!("Form validation error!") +/// } +/// }; +/// +/// Html::new(format!("Hello {}!", form.hello)) +/// } +/// +/// # #[tokio::main] +/// # async fn main() -> cot::Result<()> { +/// # use cot::RequestHandler; +/// # let request = TestRequestBuilder::post("/").form_data(&[("hello", "world")]).build(); +/// # my_handler.handle(request).await?; +/// # Ok(()) +/// # } +/// ``` +#[derive(Debug)] +pub struct RequestForm(pub FormResult); + +impl FromRequest for RequestForm { + 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 { @@ -52,8 +127,8 @@ impl_into_cot_error!(JsonDeserializeError, BAD_REQUEST); /// # 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 @@ -89,9 +164,9 @@ impl FromRequestHead for RequestDb { /// # Examples /// /// ``` -/// use cot::html::Html; /// 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 { @@ -132,9 +207,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")?; @@ -197,7 +272,7 @@ impl FromRequestHead for StaticFiles { mod tests { use cot::request::extractors::Json; use cot::test::TestRequestBuilder; - use cot_core::request::extractors::{FromRequest, Path, UrlQuery}; + use cot_core::request::extractors::FromRequest; use serde::Deserialize; use super::*; diff --git a/cot/src/response/into_response.rs b/cot/src/response/into_response.rs index 2b173424..a2cbf46f 100644 --- a/cot/src/response/into_response.rs +++ b/cot/src/response/into_response.rs @@ -1,7 +1,32 @@ -#[cfg(feature = "json")] -use cot::core::headers::JSON_CONTENT_TYPE; use cot::core::impl_into_cot_error; use cot::core::response::{IntoResponse, Response}; +#[cfg(feature = "json")] +use cot_core::headers::HTML_CONTENT_TYPE; +use cot_core::html::Html; + +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(feature = "json")] impl IntoResponse for cot::json::Json { diff --git a/cot/src/session.rs b/cot/src/session.rs index 863fd8f8..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_core::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_core::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_core::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/test.rs b/cot/src/test.rs index 9ef6c985..0a1182d8 100644 --- a/cot/src/test.rs +++ b/cot/src/test.rs @@ -7,8 +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 derive_more::Debug; +use cot_core::router::Router; use tokio::net::TcpListener; use tokio::sync::oneshot; use tower::Service; @@ -29,8 +30,6 @@ use crate::request::Request; use crate::session::Session; use crate::static_files::{StaticFile, StaticFiles}; use crate::{Body, Bootstrapper, Project, ProjectContext, Result}; -use cot_core::handler::BoxedHandler; -use cot_core::router::Router; /// A test client for making requests to a Cot project. /// @@ -185,9 +184,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 +279,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 +313,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 +347,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 +402,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 +446,9 @@ impl TestRequestBuilder { /// /// ``` /// use cot::request::Request; - /// use cot_core::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 +540,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 +703,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/openapi.rs b/cot/tests/openapi.rs index cafc8d4e..4a25e683 100644 --- a/cot/tests/openapi.rs +++ b/cot/tests/openapi.rs @@ -1,12 +1,12 @@ use aide::openapi::{Parameter, PathItem, ReferenceOr}; -use cot::html::Html; use cot::json::Json; use cot::openapi::{AsApiRoute, NoApi, RouteContext}; use cot::test::TestRequestBuilder; use cot::{RequestHandler, StatusCode}; +use cot_core::html::Html; use cot_core::request::extractors::{Path, UrlQuery}; use cot_core::response::{IntoResponse, Response}; -use cot_core::router::method::openapi::{api_get, api_post, ApiMethodRouter}; +use cot_core::router::method::method::{ApiMethodRouter, api_get, api_post}; use cot_core::router::{Route, Router}; use schemars::SchemaGenerator; use serde::{Deserialize, Serialize}; diff --git a/cot/tests/project.rs b/cot/tests/project.rs index 64151a92..4963a94e 100644 --- a/cot/tests/project.rs +++ b/cot/tests/project.rs @@ -1,9 +1,9 @@ use bytes::Bytes; use cot::config::ProjectConfig; -use cot::html::Html; use cot::project::RegisterAppsContext; 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}; diff --git a/cot/tests/router.rs b/cot/tests/router.rs index 44acc559..cea0b242 100644 --- a/cot/tests/router.rs +++ b/cot/tests/router.rs @@ -1,9 +1,9 @@ use bytes::Bytes; use cot::config::ProjectConfig; -use cot::html::Html; use cot::project::RegisterAppsContext; use cot::test::Client; use cot::{App, AppBuilder, Project, StatusCode}; +use cot_core::html::Html; use cot_core::request::{Request, RequestExt}; use cot_core::router::{Route, Router}; diff --git a/examples/admin/src/main.rs b/examples/admin/src/main.rs index 92c9bcc1..777e1c04 100644 --- a/examples/admin/src/main.rs +++ b/examples/admin/src/main.rs @@ -13,14 +13,14 @@ use cot::config::{ StaticFilesConfig, StaticFilesPathRewriteMode, }; use cot::db::migrations::SyncDynMigration; -use cot::db::{model, Auto, Model}; +use cot::db::{Auto, Model, model}; use cot::form::Form; -use cot::html::Html; use cot::middleware::{AuthMiddleware, LiveReloadMiddleware, SessionMiddleware}; use cot::project::{MiddlewareContext, RegisterAppsContext, RootHandler}; use cot::request::extractors::RequestDb; use cot::static_files::StaticFilesMiddleware; use cot::{App, AppBuilder, Project, ProjectContext}; +use cot_core::html::Html; use cot_core::router::{Route, Router, Urls}; #[derive(Debug, Clone, Form, AdminModel)] diff --git a/examples/custom-error-pages/src/main.rs b/examples/custom-error-pages/src/main.rs index 59ecf62c..87343a5c 100644 --- a/examples/custom-error-pages/src/main.rs +++ b/examples/custom-error-pages/src/main.rs @@ -1,10 +1,10 @@ use askama::Template; use cot::cli::CliMetadata; use cot::config::ProjectConfig; -use cot::html::Html; use cot::project::RegisterAppsContext; use cot::{App, AppBuilder, Project}; use cot_core::error::handler::{DynErrorPageHandler, RequestError}; +use cot_core::html::Html; use cot_core::response::{IntoResponse, Response}; use cot_core::router::{Route, Router}; diff --git a/examples/file-upload/src/main.rs b/examples/file-upload/src/main.rs index 5160a7ad..145f682e 100644 --- a/examples/file-upload/src/main.rs +++ b/examples/file-upload/src/main.rs @@ -5,10 +5,10 @@ use cot::config::ProjectConfig; use cot::core::request::extractors::RequestForm; use cot::form::fields::InMemoryUploadedFile; use cot::form::{Form, FormContext}; -use cot::html::Html; use cot::project::{MiddlewareContext, RegisterAppsContext, RootHandler}; use cot::static_files::StaticFilesMiddleware; use cot::{App, AppBuilder, Project}; +use cot_core::html::Html; use cot_core::router::{Route, Router, Urls}; #[derive(Debug, Template)] diff --git a/examples/forms/src/main.rs b/examples/forms/src/main.rs index 75c247bc..217a8555 100644 --- a/examples/forms/src/main.rs +++ b/examples/forms/src/main.rs @@ -11,12 +11,12 @@ use cot::db::migrations::SyncDynMigration; use cot::db::{Auto, Model, model}; use cot::form::Form; use cot::form::fields::Step; -use cot::html::Html; use cot::middleware::{AuthMiddleware, LiveReloadMiddleware, SessionMiddleware}; use cot::project::{MiddlewareContext, RegisterAppsContext, RootHandler, RootHandlerBuilder}; use cot::request::extractors::{RequestDb, StaticFiles}; use cot::static_files::{StaticFile, StaticFilesMiddleware}; use cot::{App, AppBuilder, Project, reverse_redirect, static_files}; +use cot_core::html::Html; use cot_core::response::Response; use cot_core::router::{Route, Router, Urls}; diff --git a/examples/json/src/main.rs b/examples/json/src/main.rs index c3828bdc..ec210a84 100644 --- a/examples/json/src/main.rs +++ b/examples/json/src/main.rs @@ -7,7 +7,7 @@ use cot::static_files::StaticFilesMiddleware; use cot::{App, AppBuilder, Project}; use cot_core::error::handler::{DynErrorPageHandler, RequestError}; use cot_core::response::IntoResponse; -use cot_core::router::method::openapi::api_post; +use cot_core::router::method::method::api_post; use cot_core::router::{Route, Router}; use serde::{Deserialize, Serialize}; diff --git a/examples/sessions/src/main.rs b/examples/sessions/src/main.rs index 280a48c5..096d83b1 100644 --- a/examples/sessions/src/main.rs +++ b/examples/sessions/src/main.rs @@ -7,12 +7,12 @@ use cot::config::{ use cot::core::request::Request; use cot::core::response::{IntoResponse, Response}; use cot::form::Form; -use cot::html::Html; use cot::middleware::SessionMiddleware; use cot::project::{MiddlewareContext, RegisterAppsContext, RootHandler}; -use cot::session::db::SessionApp; use cot::session::Session; -use cot::{reverse_redirect, App, AppBuilder, Project}; +use cot::session::db::SessionApp; +use cot::{App, AppBuilder, Project, reverse_redirect}; +use cot_core::html::Html; use cot_core::router::{Route, Router, Urls}; #[derive(Debug, Template)] diff --git a/examples/todo-list/src/main.rs b/examples/todo-list/src/main.rs index d3b7381e..7ff5cbec 100644 --- a/examples/todo-list/src/main.rs +++ b/examples/todo-list/src/main.rs @@ -9,11 +9,11 @@ use cot::core::response::Response; use cot::db::migrations::SyncDynMigration; use cot::db::{Auto, Model, model, query}; use cot::form::Form; -use cot::html::Html; use cot::project::{MiddlewareContext, RegisterAppsContext, RootHandler}; use cot::request::extractors::RequestDb; use cot::static_files::StaticFilesMiddleware; use cot::{App, AppBuilder, Project, reverse_redirect}; +use cot_core::html::Html; use cot_core::router::{Route, Router, Urls}; #[derive(Debug, Clone)] From 5be4b4f6659333469359cffd369fb4e1bc6d2eb5 Mon Sep 17 00:00:00 2001 From: Marek 'seqre' Grzelak Date: Mon, 29 Dec 2025 18:52:31 +0100 Subject: [PATCH 11/18] move i have no idea what anymore once again --- Cargo.lock | 1 + cot-core/Cargo.toml | 1 + cot-core/src/middleware.rs | 1 + cot-core/src/request/extractors.rs | 7 -- cot-core/src/response/into_response.rs | 2 +- cot-core/src/router.rs | 121 ----------------------- cot-core/src/router/method.rs | 3 - cot/Cargo.toml | 6 +- cot/src/lib.rs | 7 +- cot/src/request/extractors.rs | 14 ++- cot/src/router.rs | 127 +++++++++++++++++++++++++ 11 files changed, 150 insertions(+), 140 deletions(-) create mode 100644 cot/src/router.rs diff --git a/Cargo.lock b/Cargo.lock index 358a2601..5ab47de5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -775,6 +775,7 @@ dependencies = [ "axum", "backtrace", "bytes", + "cot", "cot_macros", "derive_more", "form_urlencoded", diff --git a/cot-core/Cargo.toml b/cot-core/Cargo.toml index 1299c951..9d3c312a 100644 --- a/cot-core/Cargo.toml +++ b/cot-core/Cargo.toml @@ -43,6 +43,7 @@ tracing = "0.1.41" [dev-dependencies] async-stream.workspace = true +cot.workspace = true futures.workspace = true tokio = { workspace = true, features = ["macros"] } diff --git a/cot-core/src/middleware.rs b/cot-core/src/middleware.rs index 758cace2..807b8dd3 100644 --- a/cot-core/src/middleware.rs +++ b/cot-core/src/middleware.rs @@ -1,6 +1,7 @@ use std::task::{Context, Poll}; use bytes::Bytes; +use futures_util::TryFutureExt; use http_body_util::combinators::BoxBody; use http_body_util::BodyExt; use tower::Service; diff --git a/cot-core/src/request/extractors.rs b/cot-core/src/request/extractors.rs index 26342822..caa72428 100644 --- a/cot-core/src/request/extractors.rs +++ b/cot-core/src/request/extractors.rs @@ -51,7 +51,6 @@ use std::future::Future; use serde::de::DeserializeOwned; -use tower_sessions::Session; pub use crate::request::{PathParams, Request, RequestHead}; use crate::{Body, Method}; @@ -234,12 +233,6 @@ impl FromRequestHead for Method { } } -impl FromRequestHead for Session { - async fn from_request_head(head: &RequestHead) -> crate::Result { - Ok(Session::from_extensions(&head.extensions).clone()) - } -} - /// A derive macro that automatically implements the [`FromRequestHead`] trait /// for structs. /// diff --git a/cot-core/src/response/into_response.rs b/cot-core/src/response/into_response.rs index 77681fa2..96a7c526 100644 --- a/cot-core/src/response/into_response.rs +++ b/cot-core/src/response/into_response.rs @@ -192,7 +192,7 @@ where macro_rules! impl_into_response_for_type_and_mime { ($ty:ty, $mime:expr) => { impl IntoResponse for $ty { - fn into_response(self) -> cot_core::Result { + fn into_response(self) -> crate::Result { Body::from(self) .with_header(http::header::CONTENT_TYPE, $mime) .into_response() diff --git a/cot-core/src/router.rs b/cot-core/src/router.rs index 4df621fe..57093238 100644 --- a/cot-core/src/router.rs +++ b/cot-core/src/router.rs @@ -855,127 +855,6 @@ macro_rules! reverse { }}; } -/// A helper structure to allow reversing URLs from a request handler. -/// -/// This is mainly useful as an extractor to allow reversing URLs without -/// access to a full [`Request`] object. -/// -/// # Examples -/// -/// ``` -/// use cot::html::Html; -/// 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")?; -/// Ok(Html::new(format!("{url}"))) -/// } -/// -/// # #[tokio::main] -/// # async fn main() -> cot::Result<()> { -/// let router = Router::with_urls([Route::with_handler_and_name("/", my_handler, "home")]); -/// let request = TestRequestBuilder::get("/").router(router).build(); -/// -/// assert_eq!( -/// my_handler -/// .handle(request) -/// .await? -/// .into_body() -/// .into_bytes() -/// .await?, -/// "/" -/// ); -/// # Ok(()) -/// # } -/// ``` -#[derive(Debug, Clone)] -pub struct Urls { - app_name: Option, - router: Arc, -} - -impl Urls { - /// Create a new `Urls` object from a [`Request`] object. - /// - /// # Examples - /// - /// ``` - /// use cot::html::Html; - /// 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); - /// let url = reverse!(urls, "home")?; - /// Ok(Html::new(format!( - /// "Hello! The URL for this view is: {}", - /// url - /// ))) - /// } - /// ``` - pub fn from_request(request: &Request) -> Self { - Self { - app_name: request.app_name().map(ToOwned::to_owned), - router: Arc::clone(request.router()), - } - } - - 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()), - } - } - - /// Get the app name the current route belongs to, or [`None`] if the - /// request is not routed. - /// - /// This is mainly useful for providing context to reverse redirects, where - /// you want to redirect to a route in the same app. - /// - /// # Examples - /// - /// ``` - /// 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(); - /// // ... do something with the app name - /// # unimplemented!() - /// } - /// ``` - #[must_use] - pub fn app_name(&self) -> Option<&str> { - self.app_name.as_deref() - } - - /// Get the router. - /// - /// # Examples - /// - /// ``` - /// 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(); - /// // ... do something with the router - /// # unimplemented!() - /// } - /// ``` - #[must_use] - pub fn router(&self) -> &Router { - &self.router - } -} - impl Debug for RouteInner { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match &self { diff --git a/cot-core/src/router/method.rs b/cot-core/src/router/method.rs index 0e98bd54..f4c7e932 100644 --- a/cot-core/src/router/method.rs +++ b/cot-core/src/router/method.rs @@ -1,8 +1,5 @@ //! Route to handlers based on HTTP methods. -#[cfg(feature = "openapi")] -pub mod openapi; - use std::fmt::{Debug, Formatter}; use crate::error::MethodNotAllowed; diff --git a/cot/Cargo.toml b/cot/Cargo.toml index 8f3bc167..5620bd73 100644 --- a/cot/Cargo.toml +++ b/cot/Cargo.toml @@ -106,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/src/lib.rs b/cot/src/lib.rs index 51ee07c4..5d957d6a 100644 --- a/cot/src/lib.rs +++ b/cot/src/lib.rs @@ -74,6 +74,7 @@ pub mod openapi; pub mod project; pub mod request; mod response; +mod router; mod serializers; pub mod session; pub mod static_files; @@ -83,12 +84,12 @@ pub(crate) mod utils; #[cfg(feature = "openapi")] pub use aide; +pub use cot_core::error::error_impl::Error; +pub use cot_core::handler::{BoxedHandler, RequestHandler}; /// 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}; pub use cot_core::{Body, Method}; /// An attribute macro that defines an end-to-end test function for a /// Cot-powered app. @@ -158,5 +159,5 @@ pub use schemars; pub use {bytes, cot_core as core, http}; pub use crate::project::{ - App, AppBuilder, Bootstrapper, Project, ProjectContext, run, run_at, run_cli, + run, run_at, run_cli, App, AppBuilder, Bootstrapper, Project, ProjectContext, }; diff --git a/cot/src/request/extractors.rs b/cot/src/request/extractors.rs index 172ccf7e..8c010d50 100644 --- a/cot/src/request/extractors.rs +++ b/cot/src/request/extractors.rs @@ -1,10 +1,14 @@ use std::sync::Arc; +use cot::auth::Auth; +use cot::form::{Form, FormResult}; #[cfg(feature = "json")] use cot::json::Json; -use cot_core::request::Request; +use cot::router::Urls; +use cot::session::Session; use cot_core::request::extractors::{FromRequest, FromRequestHead, RequestHead}; -use cot_core::{Body, impl_into_cot_error}; +use cot_core::request::Request; +use cot_core::{impl_into_cot_error, Body}; use serde::de::DeserializeOwned; use crate::request::RequestExt; @@ -17,6 +21,12 @@ pub struct InvalidContentType { } impl_into_cot_error!(InvalidContentType, BAD_REQUEST); +impl FromRequestHead for Session { + async fn from_request_head(head: &RequestHead) -> crate::Result { + Ok(Session::from_extensions(&head.extensions).clone()) + } +} + impl FromRequestHead for Urls { async fn from_request_head(head: &RequestHead) -> crate::Result { Ok(Self::from_parts(head)) diff --git a/cot/src/router.rs b/cot/src/router.rs new file mode 100644 index 00000000..c3a45132 --- /dev/null +++ b/cot/src/router.rs @@ -0,0 +1,127 @@ +use std::sync::Arc; + +use cot_core::request::{Request, RequestHead}; +use cot_core::router::Router; + +use crate::request::RequestExt; + +/// A helper structure to allow reversing URLs from a request handler. +/// +/// This is mainly useful as an extractor to allow reversing URLs without +/// access to a full [`Request`] object. +/// +/// # Examples +/// +/// ``` +/// use cot::html::Html; +/// 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")?; +/// Ok(Html::new(format!("{url}"))) +/// } +/// +/// # #[tokio::main] +/// # async fn main() -> cot::Result<()> { +/// let router = Router::with_urls([Route::with_handler_and_name("/", my_handler, "home")]); +/// let request = TestRequestBuilder::get("/").router(router).build(); +/// +/// assert_eq!( +/// my_handler +/// .handle(request) +/// .await? +/// .into_body() +/// .into_bytes() +/// .await?, +/// "/" +/// ); +/// # Ok(()) +/// # } +/// ``` +#[derive(Debug, Clone)] +pub struct Urls { + app_name: Option, + router: Arc, +} + +impl Urls { + /// Create a new `Urls` object from a [`Request`] object. + /// + /// # Examples + /// + /// ``` + /// use cot::html::Html; + /// 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); + /// let url = reverse!(urls, "home")?; + /// Ok(Html::new(format!( + /// "Hello! The URL for this view is: {}", + /// url + /// ))) + /// } + /// ``` + pub fn from_request(request: &Request) -> Self { + Self { + app_name: request.app_name().map(ToOwned::to_owned), + router: Arc::clone(request.router()), + } + } + + 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()), + } + } + + /// Get the app name the current route belongs to, or [`None`] if the + /// request is not routed. + /// + /// This is mainly useful for providing context to reverse redirects, where + /// you want to redirect to a route in the same app. + /// + /// # Examples + /// + /// ``` + /// 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(); + /// // ... do something with the app name + /// # unimplemented!() + /// } + /// ``` + #[must_use] + pub fn app_name(&self) -> Option<&str> { + self.app_name.as_deref() + } + + /// Get the router. + /// + /// # Examples + /// + /// ``` + /// 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(); + /// // ... do something with the router + /// # unimplemented!() + /// } + /// ``` + #[must_use] + pub fn router(&self) -> &Router { + &self.router + } +} From 2a5638b25999ac250006b186146578d854c3294d Mon Sep 17 00:00:00 2001 From: Marek 'seqre' Grzelak Date: Mon, 29 Dec 2025 21:00:21 +0100 Subject: [PATCH 12/18] schlorp openapi --- cot-core/src/lib.rs | 1 + cot-core/src/openapi.rs | 292 +++++++++++++++++++++++++++++++++++++++ cot-core/src/router.rs | 12 +- cot/src/openapi.rs | 297 +--------------------------------------- 4 files changed, 306 insertions(+), 296 deletions(-) create mode 100644 cot-core/src/openapi.rs diff --git a/cot-core/src/lib.rs b/cot-core/src/lib.rs index 4a1db090..4c9082a6 100644 --- a/cot-core/src/lib.rs +++ b/cot-core/src/lib.rs @@ -14,6 +14,7 @@ pub mod response; 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`]. diff --git a/cot-core/src/openapi.rs b/cot-core/src/openapi.rs new file mode 100644 index 00000000..3086efb0 --- /dev/null +++ b/cot-core/src/openapi.rs @@ -0,0 +1,292 @@ +//! 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. + +use std::future::Future; +use std::marker::PhantomData; +use std::pin::Pin; + +use aide::openapi::{Operation, PathItem}; +use schemars::SchemaGenerator; + +use crate::handler::{BoxRequestHandler, RequestHandler}; +use crate::request::Request; +use crate::response::Response; +use crate::Method; + +/// 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; +} + +/// 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; +} + +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) +} diff --git a/cot-core/src/router.rs b/cot-core/src/router.rs index 57093238..069fc2d5 100644 --- a/cot-core/src/router.rs +++ b/cot-core/src/router.rs @@ -420,7 +420,7 @@ impl Router { let url = format!("{url}{}", route.url); - let mut route_context = cot::openapi::RouteContext::new(); + let mut route_context = crate::openapi::RouteContext::new(); route_context.param_names = ¶ms; paths.paths.insert( @@ -600,12 +600,12 @@ impl Route { pub fn with_api_handler(url: &str, handler: H) -> Self where HandlerParams: 'static, - H: RequestHandler + cot::openapi::AsApiRoute + Send + Sync + 'static, + H: RequestHandler + crate::openapi::AsApiRoute + Send + Sync + 'static, { Self { url: Arc::new(PathMatcher::new(url)), view: RouteInner::ApiHandler(Arc::new( - cot::openapi::into_box_api_endpoint_request_handler(handler), + crate::openapi::into_box_api_endpoint_request_handler(handler), )), name: None, } @@ -669,12 +669,12 @@ impl Route { where N: Into, HandlerParams: 'static, - H: RequestHandler + cot::openapi::AsApiRoute + Send + Sync + 'static, + H: RequestHandler + crate::openapi::AsApiRoute + Send + Sync + 'static, { Self { url: Arc::new(PathMatcher::new(url)), view: RouteInner::ApiHandler(Arc::new( - cot::openapi::into_box_api_endpoint_request_handler(handler), + crate::openapi::into_box_api_endpoint_request_handler(handler), )), name: Some(RouteName(name.into())), } @@ -780,7 +780,7 @@ enum RouteInner { Handler(Arc), Router(Router), #[cfg(feature = "openapi")] - ApiHandler(Arc), + ApiHandler(Arc), } /// Get a URL for a view by its registered name and given params. diff --git a/cot/src/openapi.rs b/cot/src/openapi.rs index 5f698cdd..53dd837e 100644 --- a/cot/src/openapi.rs +++ b/cot/src/openapi.rs @@ -107,15 +107,16 @@ pub mod method; #[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::router::Urls; +use cot_core::openapi::{AsApiOperation, RouteContext}; +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 @@ -211,294 +212,11 @@ use serde_json::Value; use crate::auth::Auth; use crate::form::Form; 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::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`](cot_core::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 used passed to -/// [`Route::with_api_handler`](cot_core::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_core::router::method::method::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_core::router::method::method::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. /// @@ -1150,7 +868,6 @@ mod tests { use super::*; use crate::json::Json; use crate::openapi::AsApiOperation; - use crate::request::extractors::{Path, UrlQuery}; #[derive(Deserialize, Serialize, schemars::JsonSchema)] struct TestRequest { From 88debc5d2a99b5b7273fe67bdfe8e66635d9117b Mon Sep 17 00:00:00 2001 From: Marek 'seqre' Grzelak Date: Tue, 30 Dec 2025 14:56:45 +0100 Subject: [PATCH 13/18] do some more things --- cot-core/src/error/handler.rs | 2 +- cot-core/src/handler.rs | 6 ++--- cot-core/src/middleware.rs | 4 ++-- cot-core/src/openapi.rs | 2 +- cot-core/src/request.rs | 4 +--- cot-core/src/request/extractors.rs | 3 +-- cot-core/src/response/into_response.rs | 27 +++++++++++++++++++++- cot-core/src/router.rs | 6 ++--- cot-core/src/router/method.rs | 6 ++--- cot/src/lib.rs | 11 +++++---- cot/src/middleware.rs | 4 ++++ cot/src/openapi.rs | 2 ++ cot/src/request.rs | 10 +++++--- cot/src/request/extractors.rs | 7 +++--- cot/src/response.rs | 5 ++++ cot/src/response/into_response.rs | 32 ++++---------------------- cot/src/router.rs | 3 ++- 17 files changed, 75 insertions(+), 59 deletions(-) diff --git a/cot-core/src/error/handler.rs b/cot-core/src/error/handler.rs index 94fa879d..b8cae428 100644 --- a/cot-core/src/error/handler.rs +++ b/cot-core/src/error/handler.rs @@ -12,7 +12,7 @@ use derive_more::with_trait::Debug; use crate::request::extractors::FromRequestHead; use crate::request::{Request, RequestHead}; use crate::response::Response; -use crate::{handle_all_parameters, Error}; +use crate::{Error, handle_all_parameters}; /// A trait for handling error pages in Cot applications. /// diff --git a/cot-core/src/handler.rs b/cot-core/src/handler.rs index 1733037c..4f2eb1a6 100644 --- a/cot-core/src/handler.rs +++ b/cot-core/src/handler.rs @@ -2,11 +2,11 @@ use std::future::Future; use std::marker::PhantomData; use std::pin::Pin; -use crate::request::extractors::{FromRequest, FromRequestHead}; -use crate::request::Request; -use crate::response::{IntoResponse, Response}; use crate::Error; use crate::Result; +use crate::request::Request; +use crate::request::extractors::{FromRequest, FromRequestHead}; +use crate::response::{IntoResponse, Response}; use tower::util::BoxCloneSyncService; /// A function that takes a request and returns a response. diff --git a/cot-core/src/middleware.rs b/cot-core/src/middleware.rs index 807b8dd3..cf54c667 100644 --- a/cot-core/src/middleware.rs +++ b/cot-core/src/middleware.rs @@ -2,14 +2,14 @@ use std::task::{Context, Poll}; use bytes::Bytes; use futures_util::TryFutureExt; -use http_body_util::combinators::BoxBody; 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; -use crate::Body; /// Middleware that converts a any [`http::Response`] generic type to a /// [`crate::response::Response`]. diff --git a/cot-core/src/openapi.rs b/cot-core/src/openapi.rs index 3086efb0..0a5eefb7 100644 --- a/cot-core/src/openapi.rs +++ b/cot-core/src/openapi.rs @@ -11,10 +11,10 @@ use std::pin::Pin; use aide::openapi::{Operation, PathItem}; use schemars::SchemaGenerator; +use crate::Method; use crate::handler::{BoxRequestHandler, RequestHandler}; use crate::request::Request; use crate::response::Response; -use crate::Method; /// Context for API route generation. /// diff --git a/cot-core/src/request.rs b/cot-core/src/request.rs index 41fc52d5..42823c42 100644 --- a/cot-core/src/request.rs +++ b/cot-core/src/request.rs @@ -17,8 +17,6 @@ use std::future::Future; use indexmap::IndexMap; use crate::impl_into_cot_error; -use crate::request::extractors::FromRequestHead; - pub mod extractors; mod path_params_deserializer; @@ -35,7 +33,7 @@ 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 using the [`PathParams::parse`] +/// 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. diff --git a/cot-core/src/request/extractors.rs b/cot-core/src/request/extractors.rs index caa72428..8faa79a2 100644 --- a/cot-core/src/request/extractors.rs +++ b/cot-core/src/request/extractors.rs @@ -52,7 +52,7 @@ use std::future::Future; use serde::de::DeserializeOwned; -pub use crate::request::{PathParams, Request, RequestHead}; +use crate::request::{PathParams, Request, RequestHead}; use crate::{Body, Method}; /// Trait for extractors that consume the request body. @@ -268,7 +268,6 @@ impl FromRequestHead for Method { /// ``` pub use cot_macros::FromRequestHead; - use crate::impl_into_cot_error; #[cfg(test)] diff --git a/cot-core/src/response/into_response.rs b/cot-core/src/response/into_response.rs index 96a7c526..9382ddcc 100644 --- a/cot-core/src/response/into_response.rs +++ b/cot-core/src/response/into_response.rs @@ -1,7 +1,8 @@ use bytes::{Bytes, BytesMut}; use http; -use crate::headers::{OCTET_STREAM_CONTENT_TYPE, PLAIN_TEXT_CONTENT_TYPE}; +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}; @@ -313,6 +314,30 @@ impl IntoResponse for Body { } } +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}; diff --git a/cot-core/src/router.rs b/cot-core/src/router.rs index 069fc2d5..eab23066 100644 --- a/cot-core/src/router.rs +++ b/cot-core/src/router.rs @@ -31,11 +31,11 @@ use derive_more::with_trait::Debug; use tracing::debug; use crate::error::NotFound; -use crate::handler::{into_box_request_handler, BoxRequestHandler, RequestHandler}; +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::{impl_into_cot_error, Error, Result}; +use crate::{Error, Result, impl_into_cot_error}; pub mod method; pub mod path; @@ -908,9 +908,9 @@ macro_rules! reverse_redirect { #[cfg(test)] mod tests { use super::*; + use crate::StatusCode; use crate::request::Request; use crate::response::{IntoResponse, Response}; - use crate::StatusCode; struct MockHandler; diff --git a/cot-core/src/router/method.rs b/cot-core/src/router/method.rs index f4c7e932..b80b90e4 100644 --- a/cot-core/src/router/method.rs +++ b/cot-core/src/router/method.rs @@ -2,11 +2,11 @@ use std::fmt::{Debug, Formatter}; +use crate::Method; use crate::error::MethodNotAllowed; -use crate::handler::{into_box_request_handler, BoxRequestHandler, RequestHandler}; +use crate::handler::{BoxRequestHandler, RequestHandler, into_box_request_handler}; use crate::request::Request; use crate::response::Response; -use crate::Method; /// A router that routes requests based on the HTTP method. /// @@ -420,8 +420,8 @@ mod tests { use cot::test::TestRequestBuilder; use super::*; - use crate::html::Html; use crate::StatusCode; + use crate::html::Html; async fn test_handler(method: Method) -> Html { Html::new(method.as_str()) diff --git a/cot/src/lib.rs b/cot/src/lib.rs index 5d957d6a..1cbf68e5 100644 --- a/cot/src/lib.rs +++ b/cot/src/lib.rs @@ -84,13 +84,14 @@ pub(crate) mod utils; #[cfg(feature = "openapi")] pub use aide; -pub use cot_core::error::error_impl::Error; -pub use cot_core::handler::{BoxedHandler, RequestHandler}; /// 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::{Body, Method}; +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. /// @@ -156,8 +157,8 @@ pub use cot_macros::main; pub use cot_macros::test; #[cfg(feature = "openapi")] pub use schemars; -pub use {bytes, cot_core as core, http}; +pub use {bytes, http}; pub use crate::project::{ - run, run_at, run_cli, App, AppBuilder, Bootstrapper, Project, ProjectContext, + App, AppBuilder, Bootstrapper, Project, ProjectContext, run, run_at, run_cli, }; diff --git a/cot/src/middleware.rs b/cot/src/middleware.rs index 0202799e..c3435a68 100644 --- a/cot/src/middleware.rs +++ b/cot/src/middleware.rs @@ -9,6 +9,10 @@ use std::fmt::Debug; use std::sync::Arc; use std::task::{Context, Poll}; +#[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; diff --git a/cot/src/openapi.rs b/cot/src/openapi.rs index 53dd837e..09a9ff6a 100644 --- a/cot/src/openapi.rs +++ b/cot/src/openapi.rs @@ -114,6 +114,8 @@ use aide::openapi::{ use cot::router::Urls; use cot_core::handle_all_parameters; use cot_core::handler::BoxRequestHandler; +#[doc(inline)] +pub use cot_core::openapi::*; use cot_core::openapi::{AsApiOperation, RouteContext}; use cot_core::request::extractors::{Path, UrlQuery}; use cot_core::response::{Response, WithExtension}; diff --git a/cot/src/request.rs b/cot/src/request.rs index b2409d67..107cfba1 100644 --- a/cot/src/request.rs +++ b/cot/src/request.rs @@ -2,13 +2,17 @@ use std::sync::Arc; use cot::db::Database; use cot::request::extractors::InvalidContentType; -use cot_core::request::extractors::FromRequestHead; -use cot_core::request::{PathParams, Request, RequestHead, RouteName}; use cot_core::router::Router; use http::Extensions; - pub mod extractors; +#[doc(inline)] +pub use cot_core::request::{ + AppName, PathParams, PathParamsDeserializerError, Request, RequestHead, RouteName, +}; + +use crate::request::extractors::FromRequestHead; + mod private { pub trait Sealed {} } diff --git a/cot/src/request/extractors.rs b/cot/src/request/extractors.rs index 8c010d50..55e09d69 100644 --- a/cot/src/request/extractors.rs +++ b/cot/src/request/extractors.rs @@ -6,9 +6,10 @@ use cot::form::{Form, FormResult}; use cot::json::Json; use cot::router::Urls; use cot::session::Session; -use cot_core::request::extractors::{FromRequest, FromRequestHead, RequestHead}; -use cot_core::request::Request; -use cot_core::{impl_into_cot_error, Body}; +#[doc(inline)] +pub use cot_core::request::extractors::{FromRequest, FromRequestHead}; +use cot_core::request::{Request, RequestHead}; +use cot_core::{Body, impl_into_cot_error}; use serde::de::DeserializeOwned; use crate::request::RequestExt; diff --git a/cot/src/response.rs b/cot/src/response.rs index 63fb6c45..dbfc6043 100644 --- a/cot/src/response.rs +++ b/cot/src/response.rs @@ -1 +1,6 @@ mod into_response; + +#[doc(inline)] +pub use cot_core::response::{ + IntoResponse, Response, WithBody, WithContentType, WithExtension, WithHeader, WithStatus, +}; diff --git a/cot/src/response/into_response.rs b/cot/src/response/into_response.rs index a2cbf46f..a3344258 100644 --- a/cot/src/response/into_response.rs +++ b/cot/src/response/into_response.rs @@ -1,32 +1,6 @@ use cot::core::impl_into_cot_error; -use cot::core::response::{IntoResponse, Response}; -#[cfg(feature = "json")] -use cot_core::headers::HTML_CONTENT_TYPE; -use cot_core::html::Html; - -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() - } -} +use cot_core::headers::JSON_CONTENT_TYPE; +use cot_core::response::{IntoResponse, Response}; #[cfg(feature = "json")] impl IntoResponse for cot::json::Json { @@ -72,6 +46,8 @@ impl_into_cot_error!(JsonSerializeError, INTERNAL_SERVER_ERROR); mod tests { use cot_core::StatusCode; + use super::*; + #[cfg(feature = "json")] #[cot_macros::test] async fn test_json_struct_into_response() { diff --git a/cot/src/router.rs b/cot/src/router.rs index c3a45132..e3ec3fc5 100644 --- a/cot/src/router.rs +++ b/cot/src/router.rs @@ -1,7 +1,8 @@ use std::sync::Arc; use cot_core::request::{Request, RequestHead}; -use cot_core::router::Router; +#[doc(inline)] +pub use cot_core::router::{Route, Router, method::MethodRouter}; use crate::request::RequestExt; From 2bfd883df8c67f8bae86d75094b96804bbf39a8f Mon Sep 17 00:00:00 2001 From: Marek 'seqre' Grzelak Date: Tue, 30 Dec 2025 15:33:06 +0100 Subject: [PATCH 14/18] more re-exports --- cot-core/src/openapi.rs | 1 - cot-core/src/request.rs | 2 -- cot/src/openapi.rs | 3 +-- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/cot-core/src/openapi.rs b/cot-core/src/openapi.rs index 0a5eefb7..55b9c361 100644 --- a/cot-core/src/openapi.rs +++ b/cot-core/src/openapi.rs @@ -15,7 +15,6 @@ use crate::Method; use crate::handler::{BoxRequestHandler, RequestHandler}; use crate::request::Request; use crate::response::Response; - /// Context for API route generation. /// /// `RouteContext` is used to generate OpenAPI paths from routes. It provides diff --git a/cot-core/src/request.rs b/cot-core/src/request.rs index 42823c42..aaa0ba60 100644 --- a/cot-core/src/request.rs +++ b/cot-core/src/request.rs @@ -12,8 +12,6 @@ //! use cot_core::request::RequestExt; //! ``` -use std::future::Future; - use indexmap::IndexMap; use crate::impl_into_cot_error; diff --git a/cot/src/openapi.rs b/cot/src/openapi.rs index 09a9ff6a..313fdcfb 100644 --- a/cot/src/openapi.rs +++ b/cot/src/openapi.rs @@ -115,8 +115,7 @@ use cot::router::Urls; use cot_core::handle_all_parameters; use cot_core::handler::BoxRequestHandler; #[doc(inline)] -pub use cot_core::openapi::*; -use cot_core::openapi::{AsApiOperation, RouteContext}; +pub use cot_core::openapi::{RouteContext, AsApiRoute, BoxApiEndpointRequestHandler, AsApiOperation, into_box_api_endpoint_request_handler}; use cot_core::request::extractors::{Path, UrlQuery}; use cot_core::response::{Response, WithExtension}; /// Derive macro for the [`ApiOperationResponse`] trait. From efb0f64ffdd71185fdcb635a71e1ee8b81c0557e Mon Sep 17 00:00:00 2001 From: Marek 'seqre' Grzelak Date: Tue, 30 Dec 2025 16:05:09 +0100 Subject: [PATCH 15/18] am i fixing or what --- cot-core/src/lib.rs | 2 +- cot-core/src/openapi.rs | 52 ++++++++++++ cot-core/src/router.rs | 123 ----------------------------- cot/src/admin.rs | 15 ++-- cot/src/auth.rs | 4 +- cot/src/form.rs | 6 +- cot/src/openapi.rs | 52 +----------- cot/src/openapi/method.rs | 3 +- cot/src/project.rs | 18 ++--- cot/src/request/extractors.rs | 2 +- cot/src/response.rs | 2 +- cot/src/response/into_response.rs | 6 +- cot/src/router.rs | 126 +++++++++++++++++++++++++++++- cot/src/test.rs | 1 + 14 files changed, 208 insertions(+), 204 deletions(-) diff --git a/cot-core/src/lib.rs b/cot-core/src/lib.rs index 4c9082a6..27d95373 100644 --- a/cot-core/src/lib.rs +++ b/cot-core/src/lib.rs @@ -1,4 +1,4 @@ -use crate::error::error_impl::Error; +pub use crate::error::error_impl::Error; pub mod body; /// Error handling types and utilities for Cot applications. diff --git a/cot-core/src/openapi.rs b/cot-core/src/openapi.rs index 55b9c361..505bb22f 100644 --- a/cot-core/src/openapi.rs +++ b/cot-core/src/openapi.rs @@ -243,6 +243,58 @@ pub trait AsApiOperation { ) -> 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( diff --git a/cot-core/src/router.rs b/cot-core/src/router.rs index eab23066..170b4a58 100644 --- a/cot-core/src/router.rs +++ b/cot-core/src/router.rs @@ -508,20 +508,6 @@ impl tower::Service for RouterService { } } -// 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 @@ -783,78 +769,6 @@ enum RouteInner { ApiHandler(Arc), } -/// Get a URL for a view by its registered name and given params. -/// -/// If the view name has two parts separated by a colon, the first part is -/// considered the app name. If the app name is not provided, the app name of -/// the request is used. This means that if you don't specify the `app_name`, -/// this macro will only return URLs for views in the same app as the current -/// request handler. -/// -/// # 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 URL. -/// -/// # Examples -/// -/// ``` -/// use cot::html::Html; -/// use cot::project::RegisterAppsContext; -/// 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: -/// let url = reverse!(request, "home")?; -/// let url = reverse!(request, "my_custom_app:home")?; -/// -/// Ok(Html::new(format!( -/// "Hello! The URL for this view is: {}", -/// url -/// ))) -/// } -/// -/// let router = Router::with_urls([Route::with_handler_and_name("/", home, "home")]); -/// -/// struct MyApp; -/// -/// impl App for MyApp { -/// fn name(&self) -> &'static str { -/// "my_custom_app" -/// } -/// -/// fn router(&self) -> Router { -/// Router::with_urls([Route::with_handler_and_name("/", home, "home")]) -/// } -/// } -/// -/// struct MyProject; -/// -/// impl Project for MyProject { -/// fn register_apps(&self, apps: &mut AppBuilder, context: &RegisterAppsContext) { -/// apps.register_with_views(MyApp, ""); -/// } -/// } -/// ``` -#[macro_export] -macro_rules! reverse { - ($request:expr, $view_name:literal $(, $($key:ident = $value:expr),*)?) => {{ - #[allow( - clippy::allow_attributes, - unused_imports, - reason = "allow using either `Request` or `Urls` objects" - )] - use $crate::request::RequestExt; - let (app_name, view_name) = $crate::router::split_view_name($view_name); - let app_name = app_name.or_else(|| $request.app_name()); - $request - .router() - .reverse(app_name, view_name, &$crate::reverse_param_map!($( $($key = $value),* )?)) - }}; -} - impl Debug for RouteInner { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match &self { @@ -868,43 +782,6 @@ impl Debug for RouteInner { } } -/// 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)) - }; -} - #[cfg(test)] mod tests { use super::*; diff --git a/cot/src/admin.rs b/cot/src/admin.rs index 2db0a565..9eada537 100644 --- a/cot/src/admin.rs +++ b/cot/src/admin.rs @@ -9,12 +9,13 @@ use std::marker::PhantomData; use askama::Template; use async_trait::async_trait; use bytes::Bytes; -use cot_core::error::NotFound; -use cot_core::html::Html; -use cot_core::request::extractors::{FromRequestHead, Path, StaticFiles, UrlQuery}; -use cot_core::request::{Request, RequestExt, RequestHead}; -use cot_core::response::{IntoResponse, Response}; -use cot_core::router::{Router, Urls}; +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}; /// Implements the [`AdminModel`] trait for a struct. /// /// This is a simple method for adding a database model to the admin panel. @@ -32,7 +33,7 @@ use crate::form::{ Form, FormContext, FormErrorTarget, FormField, FormFieldValidationError, FormResult, }; use crate::static_files::StaticFile; -use crate::{App, Error, Method, RequestHandler, reverse_redirect}; +use crate::{App, Error, Method, RequestHandler}; struct AdminAuthenticated(H, PhantomData T>); diff --git a/cot/src/auth.rs b/cot/src/auth.rs index a71626ea..0635f3bf 100644 --- a/cot/src/auth.rs +++ b/cot/src/auth.rs @@ -16,8 +16,8 @@ use std::sync::{Arc, Mutex, MutexGuard}; /// backwards compatible shim for form Password type. use async_trait::async_trait; use chrono::{DateTime, FixedOffset}; -use cot_core::error::error_impl::impl_into_cot_error; -use cot_core::request::{Request, RequestExt}; +use crate::error::error_impl::impl_into_cot_error; +use crate::request::{Request, RequestExt}; use derive_more::with_trait::Debug; #[cfg(test)] use mockall::automock; diff --git a/cot/src/form.rs b/cot/src/form.rs index 6cb3340d..8d320cb2 100644 --- a/cot/src/form.rs +++ b/cot/src/form.rs @@ -31,9 +31,9 @@ use async_trait::async_trait; use bytes::Bytes; use chrono::NaiveDateTime; use chrono_tz::Tz; -use cot_core::error::error_impl::impl_into_cot_error; -use cot_core::headers::{MULTIPART_FORM_CONTENT_TYPE, URLENCODED_FORM_CONTENT_TYPE}; -use cot_core::request::{Request, RequestExt}; +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}; /// Derive the [`Form`] trait for a struct and create a [`FormContext`] for it. /// /// This macro will generate an implementation of the [`Form`] trait for the diff --git a/cot/src/openapi.rs b/cot/src/openapi.rs index 313fdcfb..ec717603 100644 --- a/cot/src/openapi.rs +++ b/cot/src/openapi.rs @@ -114,6 +114,7 @@ use aide::openapi::{ use cot::router::Urls; use cot_core::handle_all_parameters; use cot_core::handler::BoxRequestHandler; +use cot_core::impl_as_openapi_operation; #[doc(inline)] pub use cot_core::openapi::{RouteContext, AsApiRoute, BoxApiEndpointRequestHandler, AsApiOperation, into_box_api_endpoint_request_handler}; use cot_core::request::extractors::{Path, UrlQuery}; @@ -312,57 +313,6 @@ 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 diff --git a/cot/src/openapi/method.rs b/cot/src/openapi/method.rs index 0e9446fb..b19fa73d 100644 --- a/cot/src/openapi/method.rs +++ b/cot/src/openapi/method.rs @@ -8,10 +8,9 @@ use std::fmt::{Debug, Formatter}; use aide::openapi::Operation; use cot::openapi::{ - AsApiOperation, AsApiRoute, BoxApiRequestHandler, RouteContext, into_box_api_request_handler, + AsApiOperation, AsApiRoute, RouteContext }; use cot::request::Request; -use cot_core::router::method::{InnerHandler, InnerMethodRouter}; use schemars::SchemaGenerator; use crate::handler::RequestHandler; diff --git a/cot/src/project.rs b/cot/src/project.rs index c8e8cab6..6d27b597 100644 --- a/cot/src/project.rs +++ b/cot/src/project.rs @@ -28,17 +28,17 @@ use std::sync::Arc; use askama::Template; use async_trait::async_trait; use axum::handler::HandlerWithoutStateExt; -use cot_core::error::UncaughtPanic; -use cot_core::error::error_impl::impl_into_cot_error; -use cot_core::error::handler::{DynErrorPageHandler, RequestOuterError}; -use cot_core::handler::BoxedHandler; -use cot_core::html::Html; -use cot_core::middleware::{ +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 cot_core::request::{AppName, Request, RequestExt, RequestHead}; -use cot_core::response::{IntoResponse, Response}; -use cot_core::router::{Route, Router, RouterService}; +use crate::request::{AppName, Request, RequestExt, RequestHead}; +use crate::response::{IntoResponse, Response}; +use crate::router::{Route, Router, RouterService}; use derive_more::with_trait::Debug; use futures_util::FutureExt; use thiserror::Error; diff --git a/cot/src/request/extractors.rs b/cot/src/request/extractors.rs index 55e09d69..939cf7ad 100644 --- a/cot/src/request/extractors.rs +++ b/cot/src/request/extractors.rs @@ -7,7 +7,7 @@ use cot::json::Json; use cot::router::Urls; use cot::session::Session; #[doc(inline)] -pub use cot_core::request::extractors::{FromRequest, FromRequestHead}; +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; diff --git a/cot/src/response.rs b/cot/src/response.rs index dbfc6043..28492a64 100644 --- a/cot/src/response.rs +++ b/cot/src/response.rs @@ -2,5 +2,5 @@ mod into_response; #[doc(inline)] pub use cot_core::response::{ - IntoResponse, Response, WithBody, WithContentType, WithExtension, WithHeader, WithStatus, + IntoResponse, Response, WithBody, WithContentType, WithExtension, WithHeader, WithStatus, ResponseExt }; diff --git a/cot/src/response/into_response.rs b/cot/src/response/into_response.rs index a3344258..4d72271c 100644 --- a/cot/src/response/into_response.rs +++ b/cot/src/response/into_response.rs @@ -1,6 +1,6 @@ -use cot::core::impl_into_cot_error; -use cot_core::headers::JSON_CONTENT_TYPE; -use cot_core::response::{IntoResponse, Response}; +use cot_core::impl_into_cot_error; +use crate::headers::JSON_CONTENT_TYPE; +use crate::response::{IntoResponse, Response}; #[cfg(feature = "json")] impl IntoResponse for cot::json::Json { diff --git a/cot/src/router.rs b/cot/src/router.rs index e3ec3fc5..3bae8a45 100644 --- a/cot/src/router.rs +++ b/cot/src/router.rs @@ -1,11 +1,135 @@ use std::sync::Arc; use cot_core::request::{Request, RequestHead}; +pub use cot_core::reverse_param_map; #[doc(inline)] -pub use cot_core::router::{Route, Router, method::MethodRouter}; +pub use cot_core::router::{Route, Router, method::MethodRouter, RouterService}; use crate::request::RequestExt; +/// Get a URL for a view by its registered name and given params. +/// +/// If the view name has two parts separated by a colon, the first part is +/// considered the app name. If the app name is not provided, the app name of +/// the request is used. This means that if you don't specify the `app_name`, +/// this macro will only return URLs for views in the same app as the current +/// request handler. +/// +/// # 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 URL. +/// +/// # Examples +/// +/// ``` +/// use cot::html::Html; +/// use cot::project::RegisterAppsContext; +/// 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: +/// let url = reverse!(request, "home")?; +/// let url = reverse!(request, "my_custom_app:home")?; +/// +/// Ok(Html::new(format!( +/// "Hello! The URL for this view is: {}", +/// url +/// ))) +/// } +/// +/// let router = Router::with_urls([Route::with_handler_and_name("/", home, "home")]); +/// +/// struct MyApp; +/// +/// impl App for MyApp { +/// fn name(&self) -> &'static str { +/// "my_custom_app" +/// } +/// +/// fn router(&self) -> Router { +/// Router::with_urls([Route::with_handler_and_name("/", home, "home")]) +/// } +/// } +/// +/// struct MyProject; +/// +/// impl Project for MyProject { +/// fn register_apps(&self, apps: &mut AppBuilder, context: &RegisterAppsContext) { +/// apps.register_with_views(MyApp, ""); +/// } +/// } +/// ``` +#[macro_export] +macro_rules! reverse { + ($request:expr, $view_name:literal $(, $($key:ident = $value:expr),*)?) => {{ + #[allow( + clippy::allow_attributes, + unused_imports, + reason = "allow using either `Request` or `Urls` objects" + )] + use $crate::request::RequestExt; + let (app_name, view_name) = $crate::router::split_view_name($view_name); + let app_name = app_name.or_else(|| $request.app_name()); + $request + .router() + .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 diff --git a/cot/src/test.rs b/cot/src/test.rs index 0a1182d8..8b394b5f 100644 --- a/cot/src/test.rs +++ b/cot/src/test.rs @@ -14,6 +14,7 @@ use tokio::net::TcpListener; use tokio::sync::oneshot; use tower::Service; use tower_sessions::MemoryStore; +use derive_more::Debug; #[cfg(feature = "db")] use crate::auth::db::DatabaseUserBackend; From 273cc4d7f73dbda841c3ba8e0cb9005ed31e700f Mon Sep 17 00:00:00 2001 From: Marek 'seqre' Grzelak Date: Tue, 30 Dec 2025 16:28:41 +0100 Subject: [PATCH 16/18] am i fixingggggggg --- cot-core/src/openapi.rs | 419 +++++++++++++++++++++++- {cot => cot-core}/src/openapi/method.rs | 9 +- cot-core/src/router/method.rs | 6 +- cot/src/openapi.rs | 403 +---------------------- 4 files changed, 426 insertions(+), 411 deletions(-) rename {cot => cot-core}/src/openapi/method.rs (98%) diff --git a/cot-core/src/openapi.rs b/cot-core/src/openapi.rs index 505bb22f..3768083f 100644 --- a/cot-core/src/openapi.rs +++ b/cot-core/src/openapi.rs @@ -4,17 +4,26 @@ //! 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::{Operation, PathItem}; -use schemars::SchemaGenerator; +use aide::openapi::{Operation, PathItem, StatusCode}; +use aide::openapi::{ + MediaType, Parameter, ParameterData, ParameterSchemaOrContent, PathStyle, + QueryStyle, ReferenceOr, RequestBody, +}; +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::response::Response; +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 @@ -118,6 +127,203 @@ pub trait AsApiRoute { ) -> 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. /// @@ -341,3 +547,210 @@ where 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(), + } +} \ No newline at end of file diff --git a/cot/src/openapi/method.rs b/cot-core/src/openapi/method.rs similarity index 98% rename from cot/src/openapi/method.rs rename to cot-core/src/openapi/method.rs index b19fa73d..3792cfe5 100644 --- a/cot/src/openapi/method.rs +++ b/cot-core/src/openapi/method.rs @@ -7,10 +7,11 @@ use std::fmt::{Debug, Formatter}; use aide::openapi::Operation; -use cot::openapi::{ - AsApiOperation, AsApiRoute, RouteContext +use crate::openapi::{ + AsApiOperation, AsApiRoute, BoxApiRequestHandler, RouteContext, into_box_api_request_handler }; -use cot::request::Request; +use crate::request::Request; +use crate::router::method::{InnerHandler, InnerMethodRouter}; use schemars::SchemaGenerator; use crate::handler::RequestHandler; @@ -327,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); } diff --git a/cot-core/src/router/method.rs b/cot-core/src/router/method.rs index b80b90e4..6c249226 100644 --- a/cot-core/src/router/method.rs +++ b/cot-core/src/router/method.rs @@ -249,7 +249,7 @@ impl RequestHandler for MethodRouter { #[derive(Debug)] #[must_use] -struct InnerMethodRouter { +pub(crate) struct InnerMethodRouter { pub get: Option, pub head: Option, pub delete: Option, @@ -315,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, diff --git a/cot/src/openapi.rs b/cot/src/openapi.rs index ec717603..36e9eaf7 100644 --- a/cot/src/openapi.rs +++ b/cot/src/openapi.rs @@ -103,7 +103,6 @@ //! # } //! ``` -pub mod method; #[cfg(feature = "swagger-ui")] pub mod swagger_ui; @@ -115,8 +114,9 @@ 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::{RouteContext, AsApiRoute, BoxApiEndpointRequestHandler, AsApiOperation, into_box_api_endpoint_request_handler}; +pub use cot_core::openapi::{RouteContext, AsApiRoute, BoxApiEndpointRequestHandler, AsApiOperation, into_box_api_endpoint_request_handler, method, ApiOperationPart, ApiOperationResponse}; use cot_core::request::extractors::{Path, UrlQuery}; use cot_core::response::{Response, WithExtension}; /// Derive macro for the [`ApiOperationResponse`] trait. @@ -313,207 +313,8 @@ impl AsApiOperation for NoApi { } } -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")] @@ -543,92 +344,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( @@ -667,69 +382,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( @@ -758,57 +410,6 @@ 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}; From b6768f95d139be6dbfe966186b79cacd836c61cb Mon Sep 17 00:00:00 2001 From: Marek 'seqre' Grzelak Date: Tue, 30 Dec 2025 18:10:53 +0100 Subject: [PATCH 17/18] am i fixingggggggg again :3 --- cot-core/src/openapi/method.rs | 1 + cot/src/error_page.rs | 9 +++++---- cot/src/request/extractors.rs | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/cot-core/src/openapi/method.rs b/cot-core/src/openapi/method.rs index 3792cfe5..b11d4096 100644 --- a/cot-core/src/openapi/method.rs +++ b/cot-core/src/openapi/method.rs @@ -520,6 +520,7 @@ mod tests { use cot::test::TestRequestBuilder; use super::*; + use crate::Method; use crate::error::MethodNotAllowed; use crate::request::extractors::Path; use crate::response::{IntoResponse, Response}; diff --git a/cot/src/error_page.rs b/cot/src/error_page.rs index 6bcb2701..5ab73205 100644 --- a/cot/src/error_page.rs +++ b/cot/src/error_page.rs @@ -73,14 +73,15 @@ impl ErrorPageTemplateBuilder { if let Some(not_found) = error.inner().downcast_ref::() { 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()); } + _ => {} } } @@ -164,7 +165,7 @@ impl ErrorPageTemplateBuilder { } } - fn build_error_data(vec: &mut Vec, error: &(dyn std::error::Error + 'static)) { +fn build_error_data(vec: &mut Vec, error: &(dyn std::error::Error + 'static)) { let data = ErrorData { description: error.to_string(), debug_str: format!("{error:#?}"), diff --git a/cot/src/request/extractors.rs b/cot/src/request/extractors.rs index 939cf7ad..5449a85f 100644 --- a/cot/src/request/extractors.rs +++ b/cot/src/request/extractors.rs @@ -17,8 +17,8 @@ use crate::request::RequestExt; #[derive(Debug, thiserror::Error)] #[error("invalid content type; expected `{expected}`, found `{actual}`")] pub struct InvalidContentType { - expected: &'static str, - actual: String, + pub(crate) expected: &'static str, + pub(crate) actual: String, } impl_into_cot_error!(InvalidContentType, BAD_REQUEST); From d4d6b60e40c539bbde6f6f9e3994c856e6453cf9 Mon Sep 17 00:00:00 2001 From: Marek 'seqre' Grzelak Date: Tue, 6 Jan 2026 17:51:54 +0100 Subject: [PATCH 18/18] is it worky?? --- Cargo.lock | 2 -- cot-core/Cargo.toml | 2 +- cot-core/src/openapi.rs | 10 ++++---- cot-core/src/openapi/method.rs | 12 +++++----- cot-core/src/request.rs | 2 ++ cot-core/src/request/extractors.rs | 4 ++++ cot-core/src/response.rs | 31 +------------------------ cot-core/src/router.rs | 14 +++++++---- cot/src/admin.rs | 6 ++--- cot/src/auth.rs | 4 ++-- cot/src/cli.rs | 8 +++---- cot/src/error_page.rs | 2 +- cot/src/form.rs | 6 ++--- cot/src/lib.rs | 4 ++-- cot/src/openapi.rs | 8 +++---- cot/src/project.rs | 10 ++++---- cot/src/response.rs | 3 ++- cot/src/response/into_response.rs | 29 +++++++++++++++++++++++ cot/src/router.rs | 2 +- cot/src/test.rs | 2 +- cot/tests/auth.rs | 4 ++-- cot/tests/openapi.rs | 10 ++++---- cot/tests/router.rs | 6 ++--- examples/admin/src/main.rs | 4 ++-- examples/custom-error-pages/Cargo.toml | 1 - examples/custom-error-pages/src/main.rs | 8 +++---- examples/file-upload/src/main.rs | 6 ++--- examples/forms/src/main.rs | 11 ++++----- examples/hello-world/src/main.rs | 2 +- examples/json/Cargo.toml | 1 - examples/json/src/main.rs | 8 +++---- examples/sessions/src/main.rs | 8 +++---- examples/todo-list/src/main.rs | 9 ++++--- 33 files changed, 119 insertions(+), 120 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5ab47de5..45d50776 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1259,7 +1259,6 @@ version = "0.1.0" dependencies = [ "askama", "cot", - "cot_core", ] [[package]] @@ -1292,7 +1291,6 @@ name = "example-json" version = "0.1.0" dependencies = [ "cot", - "cot_core", "schemars", "serde", ] diff --git a/cot-core/Cargo.toml b/cot-core/Cargo.toml index 9d3c312a..c3dad3c4 100644 --- a/cot-core/Cargo.toml +++ b/cot-core/Cargo.toml @@ -43,7 +43,7 @@ tracing = "0.1.41" [dev-dependencies] async-stream.workspace = true -cot.workspace = true +cot = { workspace = true, features = ["test"] } futures.workspace = true tokio = { workspace = true, features = ["macros"] } diff --git a/cot-core/src/openapi.rs b/cot-core/src/openapi.rs index 3768083f..67bfc1cc 100644 --- a/cot-core/src/openapi.rs +++ b/cot-core/src/openapi.rs @@ -10,11 +10,11 @@ use std::future::Future; use std::marker::PhantomData; use std::pin::Pin; -use aide::openapi::{Operation, PathItem, StatusCode}; use aide::openapi::{ - MediaType, Parameter, ParameterData, ParameterSchemaOrContent, PathStyle, - QueryStyle, ReferenceOr, RequestBody, + 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; @@ -127,7 +127,6 @@ pub trait AsApiRoute { ) -> PathItem; } - /// A trait that can be implemented for types that should be taken into /// account when generating OpenAPI paths. /// @@ -736,7 +735,6 @@ fn extract_is_required(object_item: &mut Schema) -> bool { } } - fn param_with_name(param_name: String, schema: Schema, required: bool) -> ParameterData { ParameterData { name: param_name, @@ -753,4 +751,4 @@ fn param_with_name(param_name: String, schema: Schema, required: bool) -> Parame explode: None, extensions: IndexMap::default(), } -} \ No newline at end of file +} diff --git a/cot-core/src/openapi/method.rs b/cot-core/src/openapi/method.rs index b11d4096..3c4daca4 100644 --- a/cot-core/src/openapi/method.rs +++ b/cot-core/src/openapi/method.rs @@ -7,15 +7,15 @@ use std::fmt::{Debug, Formatter}; use aide::openapi::Operation; -use crate::openapi::{ - AsApiOperation, AsApiRoute, BoxApiRequestHandler, RouteContext, into_box_api_request_handler -}; -use crate::request::Request; -use crate::router::method::{InnerHandler, InnerMethodRouter}; use schemars::SchemaGenerator; use crate::handler::RequestHandler; +use crate::openapi::{ + into_box_api_request_handler, AsApiOperation, AsApiRoute, BoxApiRequestHandler, RouteContext, +}; +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. @@ -520,10 +520,10 @@ mod tests { use cot::test::TestRequestBuilder; use super::*; - use crate::Method; use crate::error::MethodNotAllowed; use crate::request::extractors::Path; use crate::response::{IntoResponse, Response}; + use crate::{Method, StatusCode}; async fn test_handler(method: Method) -> crate::Result { Html::new(method.as_str()).into_response() diff --git a/cot-core/src/request.rs b/cot-core/src/request.rs index aaa0ba60..4006af63 100644 --- a/cot-core/src/request.rs +++ b/cot-core/src/request.rs @@ -282,6 +282,8 @@ pub struct AppName(pub String); #[cfg(test)] mod tests { use super::*; + use crate::request::extractors::Path; + use crate::Body; #[test] fn path_params() { diff --git a/cot-core/src/request/extractors.rs b/cot-core/src/request/extractors.rs index 8faa79a2..361ceffa 100644 --- a/cot-core/src/request/extractors.rs +++ b/cot-core/src/request/extractors.rs @@ -268,11 +268,15 @@ impl FromRequestHead for Method { /// ``` pub use cot_macros::FromRequestHead; + use crate::impl_into_cot_error; #[cfg(test)] mod tests { + use cot::form::Form; use cot::html::Html; + use cot::request::extractors::RequestForm; + use cot::reverse; use cot::router::{Route, Router, Urls}; use cot::test::TestRequestBuilder; use serde::Deserialize; diff --git a/cot-core/src/response.rs b/cot-core/src/response.rs index 35cb1888..ee41ff3e 100644 --- a/cot-core/src/response.rs +++ b/cot-core/src/response.rs @@ -67,6 +67,7 @@ pub use into_response::{ IntoResponse, WithBody, WithContentType, WithExtension, WithHeader, WithStatus, }; + use crate::{Body, StatusCode}; const RESPONSE_BUILD_FAILURE: &str = "Failed to build response"; @@ -146,39 +147,9 @@ impl ResponseExt for Response { #[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"; diff --git a/cot-core/src/router.rs b/cot-core/src/router.rs index 170b4a58..10724233 100644 --- a/cot-core/src/router.rs +++ b/cot-core/src/router.rs @@ -31,11 +31,11 @@ use derive_more::with_trait::Debug; use tracing::debug; use crate::error::NotFound; -use crate::handler::{BoxRequestHandler, RequestHandler, into_box_request_handler}; +use crate::handler::{into_box_request_handler, BoxRequestHandler, RequestHandler}; 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}; +use crate::{impl_into_cot_error, Error, Result}; pub mod method; pub mod path; @@ -784,10 +784,14 @@ impl Debug for RouteInner { #[cfg(test)] mod tests { + use cot::reverse; + use cot::test::TestRequestBuilder; + use super::*; - use crate::StatusCode; + use crate::html::Html; use crate::request::Request; use crate::response::{IntoResponse, Response}; + use crate::StatusCode; struct MockHandler; @@ -798,10 +802,10 @@ mod tests { } #[cfg(feature = "openapi")] - impl cot::openapi::AsApiRoute for MockHandler { + impl crate::openapi::AsApiRoute for MockHandler { fn as_api_route( &self, - _route_context: &cot::openapi::RouteContext<'_>, + _route_context: &crate::openapi::RouteContext<'_>, _schema_generator: &mut schemars::SchemaGenerator, ) -> aide::openapi::PathItem { aide::openapi::PathItem::default() diff --git a/cot/src/admin.rs b/cot/src/admin.rs index 9eada537..056e3976 100644 --- a/cot/src/admin.rs +++ b/cot/src/admin.rs @@ -6,9 +6,6 @@ use std::any::Any; use std::marker::PhantomData; -use askama::Template; -use async_trait::async_trait; -use bytes::Bytes; use crate::error::NotFound; use crate::html::Html; use crate::request::extractors::{FromRequestHead, Path, StaticFiles, UrlQuery}; @@ -16,6 +13,9 @@ 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; /// Implements the [`AdminModel`] trait for a struct. /// /// This is a simple method for adding a database model to the admin panel. diff --git a/cot/src/auth.rs b/cot/src/auth.rs index 0635f3bf..f3082597 100644 --- a/cot/src/auth.rs +++ b/cot/src/auth.rs @@ -13,11 +13,11 @@ 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}; -use crate::error::error_impl::impl_into_cot_error; -use crate::request::{Request, RequestExt}; use derive_more::with_trait::Debug; #[cfg(test)] use mockall::automock; diff --git a/cot/src/cli.rs b/cot/src/cli.rs index a77a747c..cef0f51c 100644 --- a/cot/src/cli.rs +++ b/cot/src/cli.rs @@ -6,8 +6,7 @@ use std::str::FromStr; use async_trait::async_trait; pub use clap; -use clap::{Arg, ArgMatches, Command, value_parser}; -use derive_more::Debug; +use clap::{value_parser, Arg, ArgMatches, Command}; use crate::{Bootstrapper, Error, Result}; @@ -62,7 +61,7 @@ const COLLECT_STATIC_DIR_PARAM: &str = "dir"; /// } /// } /// ``` -#[derive(Debug)] +#[derive(derive_more::Debug)] pub struct Cli { command: Command, #[debug("..")] @@ -407,7 +406,6 @@ mod tests { use clap::Command; use cot::test::serial_guard; use tempfile::tempdir; - use thiserror::__private::AsDisplay; use super::*; use crate::config::ProjectConfig; @@ -437,7 +435,7 @@ mod tests { assert_eq!(cli.command.get_version().unwrap(), "1.0"); assert_eq!(cli.command.get_author().unwrap(), "Author"); assert_eq!( - cli.command.get_about().unwrap().as_display().to_string(), + cli.command.get_about().unwrap().to_string(), "Test application" ); } diff --git a/cot/src/error_page.rs b/cot/src/error_page.rs index 5ab73205..b0ed1cb6 100644 --- a/cot/src/error_page.rs +++ b/cot/src/error_page.rs @@ -165,7 +165,7 @@ impl ErrorPageTemplateBuilder { } } -fn build_error_data(vec: &mut Vec, error: &(dyn std::error::Error + 'static)) { + fn build_error_data(vec: &mut Vec, error: &(dyn std::error::Error + 'static)) { let data = ErrorData { description: error.to_string(), debug_str: format!("{error:#?}"), diff --git a/cot/src/form.rs b/cot/src/form.rs index 8d320cb2..52d26599 100644 --- a/cot/src/form.rs +++ b/cot/src/form.rs @@ -27,13 +27,13 @@ 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; use chrono_tz::Tz; -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}; /// Derive the [`Form`] trait for a struct and create a [`FormContext`] for it. /// /// This macro will generate an implementation of the [`Form`] trait for the diff --git a/cot/src/lib.rs b/cot/src/lib.rs index 1cbf68e5..b4de9155 100644 --- a/cot/src/lib.rs +++ b/cot/src/lib.rs @@ -73,8 +73,8 @@ pub mod middleware; pub mod openapi; pub mod project; pub mod request; -mod response; -mod router; +pub mod response; +pub mod router; mod serializers; pub mod session; pub mod static_files; diff --git a/cot/src/openapi.rs b/cot/src/openapi.rs index 36e9eaf7..a1dc6100 100644 --- a/cot/src/openapi.rs +++ b/cot/src/openapi.rs @@ -116,7 +116,10 @@ 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::{RouteContext, AsApiRoute, BoxApiEndpointRequestHandler, AsApiOperation, into_box_api_endpoint_request_handler, method, ApiOperationPart, ApiOperationResponse}; +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. @@ -313,7 +316,6 @@ impl AsApiOperation for NoApi { } } - impl ApiOperationPart for Urls {} impl ApiOperationPart for Session {} impl ApiOperationPart for Auth {} @@ -344,7 +346,6 @@ impl ApiOperationPart for Json { } } - impl ApiOperationPart for RequestForm { fn modify_api_operation( operation: &mut Operation, @@ -382,7 +383,6 @@ impl ApiOperationPart for RequestForm { } } - impl ApiOperationResponse for Json { fn api_operation_responses( _operation: &mut Operation, diff --git a/cot/src/project.rs b/cot/src/project.rs index 6d27b597..5e84206b 100644 --- a/cot/src/project.rs +++ b/cot/src/project.rs @@ -25,20 +25,18 @@ use std::panic::AssertUnwindSafe; use std::path::PathBuf; use std::sync::Arc; -use askama::Template; -use async_trait::async_trait; -use axum::handler::HandlerWithoutStateExt; 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::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; use derive_more::with_trait::Debug; use futures_util::FutureExt; use thiserror::Error; diff --git a/cot/src/response.rs b/cot/src/response.rs index 28492a64..8bbbbff0 100644 --- a/cot/src/response.rs +++ b/cot/src/response.rs @@ -2,5 +2,6 @@ mod into_response; #[doc(inline)] pub use cot_core::response::{ - IntoResponse, Response, WithBody, WithContentType, WithExtension, WithHeader, WithStatus, ResponseExt + IntoResponse, Response, ResponseExt, WithBody, WithContentType, WithExtension, WithHeader, + WithStatus, }; diff --git a/cot/src/response/into_response.rs b/cot/src/response/into_response.rs index 4d72271c..6da0c4a9 100644 --- a/cot/src/response/into_response.rs +++ b/cot/src/response/into_response.rs @@ -1,4 +1,5 @@ use cot_core::impl_into_cot_error; + use crate::headers::JSON_CONTENT_TYPE; use crate::response::{IntoResponse, Response}; @@ -44,10 +45,38 @@ impl_into_cot_error!(JsonSerializeError, INTERNAL_SERVER_ERROR); #[cfg(test)] mod tests { + use cot_core::body::BodyInner; use cot_core::StatusCode; use super::*; + #[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"); + } + } + } + #[cfg(feature = "json")] #[cot_macros::test] async fn test_json_struct_into_response() { diff --git a/cot/src/router.rs b/cot/src/router.rs index 3bae8a45..2f900df9 100644 --- a/cot/src/router.rs +++ b/cot/src/router.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use cot_core::request::{Request, RequestHead}; pub use cot_core::reverse_param_map; #[doc(inline)] -pub use cot_core::router::{Route, Router, method::MethodRouter, RouterService}; +pub use cot_core::router::{Route, Router, RouterService, method}; use crate::request::RequestExt; diff --git a/cot/src/test.rs b/cot/src/test.rs index 8b394b5f..7ec490cd 100644 --- a/cot/src/test.rs +++ b/cot/src/test.rs @@ -10,11 +10,11 @@ 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; use tower::Service; use tower_sessions::MemoryStore; -use derive_more::Debug; #[cfg(feature = "db")] use crate::auth::db::DatabaseUserBackend; diff --git a/cot/tests/auth.rs b/cot/tests/auth.rs index 3e40b0f1..f37888fe 100644 --- a/cot/tests/auth.rs +++ b/cot/tests/auth.rs @@ -1,10 +1,10 @@ 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}; -use cot_core::request::RequestExt; #[cot_macros::dbtest] async fn database_user(test_db: &mut TestDatabase) { diff --git a/cot/tests/openapi.rs b/cot/tests/openapi.rs index 4a25e683..b0a5547e 100644 --- a/cot/tests/openapi.rs +++ b/cot/tests/openapi.rs @@ -1,13 +1,13 @@ 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::{Route, Router}; use cot::test::TestRequestBuilder; use cot::{RequestHandler, StatusCode}; -use cot_core::html::Html; -use cot_core::request::extractors::{Path, UrlQuery}; -use cot_core::response::{IntoResponse, Response}; -use cot_core::router::method::method::{ApiMethodRouter, api_get, api_post}; -use cot_core::router::{Route, Router}; use schemars::SchemaGenerator; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; diff --git a/cot/tests/router.rs b/cot/tests/router.rs index cea0b242..cd598cf4 100644 --- a/cot/tests/router.rs +++ b/cot/tests/router.rs @@ -1,11 +1,11 @@ use bytes::Bytes; use cot::config::ProjectConfig; +use cot::html::Html; use cot::project::RegisterAppsContext; +use cot::request::{Request, RequestExt}; +use cot::router::{Route, Router}; use cot::test::Client; use cot::{App, AppBuilder, Project, StatusCode}; -use cot_core::html::Html; -use cot_core::request::{Request, RequestExt}; -use cot_core::router::{Route, Router}; async fn index() -> Html { Html::new("Hello world!") diff --git a/examples/admin/src/main.rs b/examples/admin/src/main.rs index 777e1c04..82e5447f 100644 --- a/examples/admin/src/main.rs +++ b/examples/admin/src/main.rs @@ -15,13 +15,13 @@ use cot::config::{ use cot::db::migrations::SyncDynMigration; use cot::db::{Auto, Model, model}; use cot::form::Form; +use cot::html::Html; use cot::middleware::{AuthMiddleware, LiveReloadMiddleware, SessionMiddleware}; use cot::project::{MiddlewareContext, RegisterAppsContext, RootHandler}; use cot::request::extractors::RequestDb; +use cot::router::{Route, Router, Urls}; use cot::static_files::StaticFilesMiddleware; use cot::{App, AppBuilder, Project, ProjectContext}; -use cot_core::html::Html; -use cot_core::router::{Route, Router, Urls}; #[derive(Debug, Clone, Form, AdminModel)] #[model] diff --git a/examples/custom-error-pages/Cargo.toml b/examples/custom-error-pages/Cargo.toml index 1b7b8d00..47606c2e 100644 --- a/examples/custom-error-pages/Cargo.toml +++ b/examples/custom-error-pages/Cargo.toml @@ -9,4 +9,3 @@ edition = "2024" [dependencies] askama = "0.14" cot = { path = "../../cot" } -cot_core = { path = "../../cot-core" } diff --git a/examples/custom-error-pages/src/main.rs b/examples/custom-error-pages/src/main.rs index 87343a5c..8c4872f5 100644 --- a/examples/custom-error-pages/src/main.rs +++ b/examples/custom-error-pages/src/main.rs @@ -1,12 +1,12 @@ use askama::Template; use cot::cli::CliMetadata; use cot::config::ProjectConfig; +use cot::error::handler::{DynErrorPageHandler, RequestError}; +use cot::html::Html; use cot::project::RegisterAppsContext; +use cot::response::{IntoResponse, Response}; +use cot::router::{Route, Router}; use cot::{App, AppBuilder, Project}; -use cot_core::error::handler::{DynErrorPageHandler, RequestError}; -use cot_core::html::Html; -use cot_core::response::{IntoResponse, Response}; -use cot_core::router::{Route, Router}; async fn return_hello() -> cot::Result { panic!() diff --git a/examples/file-upload/src/main.rs b/examples/file-upload/src/main.rs index 145f682e..6c6f9164 100644 --- a/examples/file-upload/src/main.rs +++ b/examples/file-upload/src/main.rs @@ -2,14 +2,14 @@ use askama::Template; use base64::Engine; use cot::cli::CliMetadata; use cot::config::ProjectConfig; -use cot::core::request::extractors::RequestForm; use cot::form::fields::InMemoryUploadedFile; use cot::form::{Form, FormContext}; +use cot::html::Html; use cot::project::{MiddlewareContext, RegisterAppsContext, RootHandler}; +use cot::request::extractors::RequestForm; +use cot::router::{Route, Router, Urls}; use cot::static_files::StaticFilesMiddleware; use cot::{App, AppBuilder, Project}; -use cot_core::html::Html; -use cot_core::router::{Route, Router, Urls}; #[derive(Debug, Template)] #[template(path = "index.html")] diff --git a/examples/forms/src/main.rs b/examples/forms/src/main.rs index 217a8555..fa63c7a5 100644 --- a/examples/forms/src/main.rs +++ b/examples/forms/src/main.rs @@ -5,20 +5,19 @@ use chrono::{DateTime, Duration, FixedOffset, NaiveDate, NaiveDateTime, NaiveTim use chrono_tz::Tz; use cot::cli::CliMetadata; use cot::config::ProjectConfig; -use cot::core::request::Request; -use cot::core::request::extractors::RequestForm; use cot::db::migrations::SyncDynMigration; use cot::db::{Auto, Model, model}; use cot::form::Form; use cot::form::fields::Step; +use cot::html::Html; use cot::middleware::{AuthMiddleware, LiveReloadMiddleware, SessionMiddleware}; use cot::project::{MiddlewareContext, RegisterAppsContext, RootHandler, RootHandlerBuilder}; -use cot::request::extractors::{RequestDb, StaticFiles}; +use cot::request::Request; +use cot::request::extractors::{RequestDb, RequestForm, StaticFiles}; +use cot::response::Response; +use cot::router::{Route, Router, Urls}; use cot::static_files::{StaticFile, StaticFilesMiddleware}; use cot::{App, AppBuilder, Project, reverse_redirect, static_files}; -use cot_core::html::Html; -use cot_core::response::Response; -use cot_core::router::{Route, Router, Urls}; #[derive(Debug, Clone)] #[model] diff --git a/examples/hello-world/src/main.rs b/examples/hello-world/src/main.rs index 12578116..cfc3464d 100644 --- a/examples/hello-world/src/main.rs +++ b/examples/hello-world/src/main.rs @@ -1,8 +1,8 @@ use cot::cli::CliMetadata; use cot::config::ProjectConfig; use cot::project::RegisterAppsContext; +use cot::router::{Route, Router}; use cot::{App, AppBuilder, Project}; -use cot_core::router::{Route, Router}; async fn return_hello() -> &'static str { "Hello Cot!" diff --git a/examples/json/Cargo.toml b/examples/json/Cargo.toml index 154814a6..8f921cdd 100644 --- a/examples/json/Cargo.toml +++ b/examples/json/Cargo.toml @@ -8,6 +8,5 @@ edition = "2024" [dependencies] cot = { path = "../../cot", features = ["openapi", "swagger-ui"] } -cot_core = { path = "../../cot-core" } serde = "1" schemars = "0.9" diff --git a/examples/json/src/main.rs b/examples/json/src/main.rs index ec210a84..56de1a69 100644 --- a/examples/json/src/main.rs +++ b/examples/json/src/main.rs @@ -1,14 +1,14 @@ 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::{Route, Router}; use cot::static_files::StaticFilesMiddleware; use cot::{App, AppBuilder, Project}; -use cot_core::error::handler::{DynErrorPageHandler, RequestError}; -use cot_core::response::IntoResponse; -use cot_core::router::method::method::api_post; -use cot_core::router::{Route, Router}; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Deserialize, schemars::JsonSchema)] diff --git a/examples/sessions/src/main.rs b/examples/sessions/src/main.rs index 096d83b1..6e69cea3 100644 --- a/examples/sessions/src/main.rs +++ b/examples/sessions/src/main.rs @@ -4,16 +4,16 @@ use cot::config::{ DatabaseConfig, MiddlewareConfig, ProjectConfig, SessionMiddlewareConfig, SessionStoreConfig, SessionStoreTypeConfig, }; -use cot::core::request::Request; -use cot::core::response::{IntoResponse, Response}; use cot::form::Form; +use cot::html::Html; use cot::middleware::SessionMiddleware; use cot::project::{MiddlewareContext, RegisterAppsContext, RootHandler}; +use cot::request::Request; +use cot::response::{IntoResponse, Response}; +use cot::router::{Route, Router, Urls}; use cot::session::Session; use cot::session::db::SessionApp; use cot::{App, AppBuilder, Project, reverse_redirect}; -use cot_core::html::Html; -use cot_core::router::{Route, Router, Urls}; #[derive(Debug, Template)] #[template(path = "index.html")] diff --git a/examples/todo-list/src/main.rs b/examples/todo-list/src/main.rs index 7ff5cbec..28837978 100644 --- a/examples/todo-list/src/main.rs +++ b/examples/todo-list/src/main.rs @@ -4,17 +4,16 @@ use askama::Template; use cot::auth::db::DatabaseUserApp; use cot::cli::CliMetadata; use cot::config::{DatabaseConfig, ProjectConfig}; -use cot::core::request::extractors::{Path, RequestForm}; -use cot::core::response::Response; use cot::db::migrations::SyncDynMigration; use cot::db::{Auto, Model, model, query}; use cot::form::Form; +use cot::html::Html; use cot::project::{MiddlewareContext, RegisterAppsContext, RootHandler}; -use cot::request::extractors::RequestDb; +use cot::request::extractors::{Path, RequestDb, RequestForm}; +use cot::response::Response; +use cot::router::{Route, Router, Urls}; use cot::static_files::StaticFilesMiddleware; use cot::{App, AppBuilder, Project, reverse_redirect}; -use cot_core::html::Html; -use cot_core::router::{Route, Router, Urls}; #[derive(Debug, Clone)] #[model]