diff --git a/Cargo.lock b/Cargo.lock index 92a53d77..5e571054 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -862,15 +862,14 @@ dependencies = [ "ahash", "aide", "askama", - "async-stream", "async-trait", "axum", - "backtrace", "bytes", "chrono", "chrono-tz", "chumsky", "clap", + "cot_core", "cot_macros", "criterion", "deadpool-redis", @@ -881,14 +880,12 @@ dependencies = [ "fake", "fantoccini", "form_urlencoded", - "futures", "futures-core", "futures-util", "grass", "hex", "hmac", "http 1.4.0", - "http-body", "http-body-util", "humantime", "idna", @@ -907,15 +904,12 @@ dependencies = [ "sea-query", "sea-query-binder", "serde", - "serde_html_form", "serde_json", - "serde_path_to_error", "serde_urlencoded", "sha2", "sqlx", "subtle", "swagger-ui-redist", - "sync_wrapper", "tempfile", "thiserror 2.0.17", "time", @@ -976,6 +970,37 @@ dependencies = [ "tracing", ] +[[package]] +name = "cot_core" +version = "0.5.0" +dependencies = [ + "askama", + "async-stream", + "axum", + "backtrace", + "bytes", + "cot", + "cot_macros", + "derive_more", + "form_urlencoded", + "futures", + "futures-core", + "futures-util", + "http 1.4.0", + "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" version = "0.5.0" @@ -3672,9 +3697,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.148" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", diff --git a/Cargo.toml b/Cargo.toml index f2e988ce..1d4da47a 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", @@ -76,6 +77,7 @@ clap-verbosity-flag = { version = "3", default-features = false } clap_complete = "4" clap_mangen = "0.2.31" cot = { version = "0.5.0", path = "cot" } +cot_core = { version = "0.5.0", path = "cot-core" } cot_codegen = { version = "0.5.0", path = "cot-codegen" } cot_macros = { version = "0.5.0", path = "cot-macros" } criterion = "0.8" diff --git a/cot-core/Cargo.toml b/cot-core/Cargo.toml new file mode 100644 index 00000000..49417b4c --- /dev/null +++ b/cot-core/Cargo.toml @@ -0,0 +1,49 @@ +[package] +name = "cot_core" +version = "0.5.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] +http.workspace = true +derive_more = { workspace = true, features = ["debug", "deref", "display", "from"] } +thiserror.workspace = true +serde.workspace = true +serde_json.workspace = true +backtrace.workspace = true +bytes.workspace = true +futures-core.workspace = true +http-body.workspace = true +http-body-util.workspace = true +sync_wrapper.workspace = true +axum.workspace = true +cot_macros.workspace = true +askama = { workspace = true, features = ["alloc"] } +tower-sessions.workspace = true +serde_path_to_error.workspace = true +indexmap.workspace = true +serde_html_form.workspace = true +form_urlencoded.workspace = true +tower.workspace = true +futures-util.workspace = true + +[dev-dependencies] +async-stream.workspace = true +cot = { workspace = true, features = ["test"] } +futures.workspace = true +tokio.workspace = true + +[features] +default = [] +json = [] diff --git a/cot/src/body.rs b/cot-core/src/body.rs similarity index 97% rename from cot/src/body.rs rename to cot-core/src/body.rs index 874307c3..59ee68e3 100644 --- a/cot/src/body.rs +++ b/cot-core/src/body.rs @@ -1,3 +1,8 @@ +//! HTTP body type. +//! +//! This module provides the [`Body`] type for representing HTTP bodies, +//! supporting both fixed in-memory buffers and streaming data sources. + use std::error::Error as StdError; use std::fmt::{Debug, Formatter}; use std::pin::Pin; @@ -9,7 +14,7 @@ use http_body::{Frame, SizeHint}; use http_body_util::combinators::BoxBody; use sync_wrapper::SyncWrapper; -use crate::error::error_impl::impl_into_cot_error; +use crate::error::impl_into_cot_error; use crate::{Error, Result}; /// A type that represents an HTTP request or response body. @@ -166,7 +171,8 @@ impl Body { } #[must_use] - pub(crate) fn axum(inner: axum::body::Body) -> Self { + #[doc(hidden)] + pub fn axum(inner: axum::body::Body) -> Self { Self::new(BodyInner::Axum(SyncWrapper::new(inner))) } @@ -261,9 +267,6 @@ impl_into_cot_error!(ReadRequestBody, BAD_REQUEST); #[cfg(test)] mod tests { - use std::pin::Pin; - use std::task::{Context, Poll}; - use futures::stream; use http_body::Body as HttpBody; diff --git a/cot-core/src/error.rs b/cot-core/src/error.rs new file mode 100644 index 00000000..3179a379 --- /dev/null +++ b/cot-core/src/error.rs @@ -0,0 +1,14 @@ +//! 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 backtrace; +pub(crate) mod error_impl; +mod method_not_allowed; +mod uncaught_panic; + +pub use error_impl::{Error, impl_into_cot_error}; +pub use method_not_allowed::MethodNotAllowed; +pub use uncaught_panic::UncaughtPanic; 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..e06f1d81 100644 --- a/cot/src/error/backtrace.rs +++ b/cot-core/src/error/backtrace.rs @@ -1,7 +1,8 @@ // 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 { +#[must_use] +pub fn __cot_create_backtrace() -> Backtrace { let mut backtrace = Vec::new(); let mut start = false; backtrace::trace(|frame| { @@ -21,19 +22,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 +43,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 +51,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 91% rename from cot/src/error/error_impl.rs rename to cot-core/src/error/error_impl.rs index dbe2856b..c38bf28e 100644 --- a/cot/src/error/error_impl.rs +++ b/cot-core/src/error/error_impl.rs @@ -8,7 +8,6 @@ use crate::StatusCode; // Need to rename Backtrace to CotBacktrace, because otherwise it triggers special behavior // in the thiserror library use crate::error::backtrace::{__cot_create_backtrace, Backtrace as CotBacktrace}; -use crate::error::not_found::NotFound; /// An error that can occur while using Cot. pub struct Error { @@ -151,46 +150,6 @@ impl Error { Self::internal(error) } - /// Create a new "404 Not Found" error without a message. - /// - /// # Examples - /// - /// ``` - /// use cot::Error; - /// - /// let error = Error::not_found(); - /// ``` - #[must_use] - #[deprecated( - note = "Use `cot::Error::from(cot::error::NotFound::new())` instead", - since = "0.4.0" - )] - pub fn not_found() -> Self { - Self::from(NotFound::new()) - } - - /// Create a new "404 Not Found" error with a message. - /// - /// Note that the message is only displayed when Cot's debug mode is - /// enabled. It will not be exposed to the user in production. - /// - /// # Examples - /// - /// ``` - /// use cot::Error; - /// - /// let id = 123; - /// let error = Error::not_found_message(format!("User with id={id} not found")); - /// ``` - #[must_use] - #[deprecated( - note = "Use `cot::Error::from(cot::error::NotFound::with_message())` instead", - since = "0.4.0" - )] - pub fn not_found_message(message: String) -> Self { - Self::from(NotFound::with_message(message)) - } - /// Returns the HTTP status code associated with this error. /// /// This method returns the appropriate HTTP status code that should be @@ -216,7 +175,8 @@ impl Error { } #[must_use] - pub(crate) fn backtrace(&self) -> &CotBacktrace { + #[doc(hidden)] + pub fn backtrace(&self) -> &CotBacktrace { &self.repr.backtrace } @@ -319,6 +279,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 +296,7 @@ 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/method_not_allowed.rs b/cot-core/src/error/method_not_allowed.rs similarity index 100% rename from cot/src/error/method_not_allowed.rs rename to cot-core/src/error/method_not_allowed.rs diff --git a/cot/src/error/uncaught_panic.rs b/cot-core/src/error/uncaught_panic.rs similarity index 100% rename from cot/src/error/uncaught_panic.rs rename to cot-core/src/error/uncaught_panic.rs diff --git a/cot/src/handler.rs b/cot-core/src/handler.rs similarity index 88% rename from cot/src/handler.rs rename to cot-core/src/handler.rs index be8f6423..b4593326 100644 --- a/cot/src/handler.rs +++ b/cot-core/src/handler.rs @@ -1,3 +1,9 @@ +//! Request handler traits and utilities. +//! +//! This module provides the [`RequestHandler`] trait, which is the core +//! abstraction for handling HTTP requests in Cot. It is automatically +//! implemented for async functions taking extractors and returning responses. + use std::future::Future; use std::marker::PhantomData; use std::pin::Pin; @@ -48,14 +54,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 +148,7 @@ macro_rules! impl_request_handler_from_request { }; } +#[macro_export] macro_rules! handle_all_parameters { ($name:ident) => { $name!(); @@ -227,38 +234,9 @@ 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). -/// -/// It is returned by -/// [`Bootstrapper::into_bootstrapped_project`](cot::Bootstrapper::finish). -/// Typically, you don't need to interact with this type directly, except for -/// creating it in [`Project::middlewares`](cot::Project::middlewares) through -/// the [`RootHandlerBuilder::build`](cot::project::RootHandlerBuilder::build) -/// method. -/// -/// # Examples -/// -/// ``` -/// use cot::config::ProjectConfig; -/// use cot::{Bootstrapper, BoxedHandler, Project}; -/// -/// struct MyProject; -/// impl Project for MyProject {} -/// -/// # #[tokio::main] -/// # async fn main() -> cot::Result<()> { -/// let bootstrapper = Bootstrapper::new(MyProject) -/// .with_config(ProjectConfig::default()) -/// .boot() -/// .await?; -/// let handler: BoxedHandler = bootstrapper.finish().handler; -/// # Ok(()) -/// # } -/// ``` pub type BoxedHandler = BoxCloneSyncService; diff --git a/cot-core/src/headers.rs b/cot-core/src/headers.rs new file mode 100644 index 00000000..52458b05 --- /dev/null +++ b/cot-core/src/headers.rs @@ -0,0 +1,11 @@ +//! HTTP header constants. +//! +//! This module provides commonly used content type header values. + +pub const HTML_CONTENT_TYPE: &str = "text/html; charset=utf-8"; +pub const MULTIPART_FORM_CONTENT_TYPE: &str = "multipart/form-data"; +pub const URLENCODED_FORM_CONTENT_TYPE: &str = "application/x-www-form-urlencoded"; +#[cfg(feature = "json")] +pub const JSON_CONTENT_TYPE: &str = "application/json"; +pub const PLAIN_TEXT_CONTENT_TYPE: &str = "text/plain; charset=utf-8"; +pub const OCTET_STREAM_CONTENT_TYPE: &str = "application/octet-stream"; diff --git a/cot/src/html.rs b/cot-core/src/html.rs similarity index 100% rename from cot/src/html.rs rename to cot-core/src/html.rs diff --git a/cot/src/json.rs b/cot-core/src/json.rs similarity index 100% rename from cot/src/json.rs rename to cot-core/src/json.rs diff --git a/cot-core/src/lib.rs b/cot-core/src/lib.rs new file mode 100644 index 00000000..a46896b4 --- /dev/null +++ b/cot-core/src/lib.rs @@ -0,0 +1,34 @@ +//! Core types and functionality for the Cot web framework. +//! +//! This crate provides the foundational building blocks for +//! [Cot](https://docs.rs/cot/latest/cot/), including HTTP primitives, body handling, error +//! types, handlers, middleware, and request/response types. +//! +//! Most applications should use the main `cot` crate rather than depending on +//! `cot-core` directly. This crate is primarily intended for internal use by +//! the Cot framework and for building custom extensions. + +mod body; + +pub mod error; +#[macro_use] +pub mod handler; +pub mod headers; +pub mod html; +#[cfg(feature = "json")] +pub mod json; +pub mod middleware; +pub mod request; +pub mod response; + +pub use body::Body; +pub use error::Error; + +/// 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; + +/// A type alias for a result that can return a [`Error`]. +pub type Result = std::result::Result; diff --git a/cot-core/src/middleware.rs b/cot-core/src/middleware.rs new file mode 100644 index 00000000..c8a48edd --- /dev/null +++ b/cot-core/src/middleware.rs @@ -0,0 +1,185 @@ +//! Middlewares for modifying requests and responses. +//! +//! Middlewares are used to modify requests and responses in a pipeline. They +//! are used to add functionality to the request/response cycle, such as +//! session management, adding security headers, and more. + +use std::fmt::Debug; +use std::task::{Context, Poll}; + +use bytes::Bytes; +use futures_util::TryFutureExt; +use http_body_util::BodyExt; +use http_body_util::combinators::BoxBody; +use tower::Service; + +use crate::request::Request; +use crate::response::Response; +use crate::{Body, Error}; + +#[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 [`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)))) +} + +#[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 [`Error`]. +/// +/// Used by [`IntoCotErrorLayer`]. +/// +/// # Examples +/// +/// ``` +/// use std::any::TypeId; +/// +/// use cot::middleware::{IntoCotError, IntoCotErrorLayer}; +/// +/// assert_eq!( +/// TypeId::of::<>::Service>(), +/// TypeId::of::>() +/// ); +/// ``` +#[derive(Debug, Clone)] +pub struct IntoCotError { + inner: S, +} + +impl Service for IntoCotError +where + S: Service, + >::Error: std::error::Error + Send + Sync + 'static, +{ + type Response = S::Response; + type Error = Error; + type Future = futures_util::future::MapErr Error>; + + #[inline] + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx).map_err(map_err) + } + + #[inline] + fn call(&mut self, request: Request) -> Self::Future { + self.inner.call(request).map_err(map_err) + } +} + +fn map_err(error: E) -> Error +where + E: std::error::Error + Send + Sync + 'static, +{ + #[expect(trivial_casts)] + let boxed = Box::new(error) as Box; + boxed.downcast::().map_or_else(Error::wrap, |e| *e) +} diff --git a/cot-core/src/request.rs b/cot-core/src/request.rs new file mode 100644 index 00000000..4286ad41 --- /dev/null +++ b/cot-core/src/request.rs @@ -0,0 +1,312 @@ +use indexmap::IndexMap; + +use crate::Body; +use crate::error::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; + +#[derive(Debug, thiserror::Error)] +#[error("invalid content type; expected `{expected}`, found `{actual}`")] +pub struct InvalidContentType { + pub expected: &'static str, + pub actual: String, +} +impl_into_cot_error!(InvalidContentType, BAD_REQUEST); + +#[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 via 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, + ) -> 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::*; + + #[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(), + } + ); + } +} diff --git a/cot-core/src/request/extractors.rs b/cot-core/src/request/extractors.rs new file mode 100644 index 00000000..61c9904d --- /dev/null +++ b/cot-core/src/request/extractors.rs @@ -0,0 +1,487 @@ +//! 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 serde::de::DeserializeOwned; + +#[cfg(feature = "json")] +use crate::json::Json; +use crate::request::{InvalidContentType, PathParams, Request, RequestHead}; +use crate::{Body, Method}; + +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)) + } +} + +/// extractors. +pub trait FromRequestHead: Sized { + /// Extracts data from the request head. + /// + /// # Errors + /// + /// Throws an error if the extractor fails to extract the data from the + /// request head. + fn from_request_head(head: &RequestHead) -> impl Future> + Send; +} + +/// An extractor that extracts data from the URL params. +/// +/// The extractor is generic over a type that implements +/// [`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) -> 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 +/// [`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) -> crate::Result { + let query = head.uri.query().unwrap_or_default(); + + let deserializer = + serde_html_form::Deserializer::new(form_urlencoded::parse(query.as_bytes())); + + let value = + serde_path_to_error::deserialize(deserializer).map_err(QueryParametersParseError)?; + + Ok(UrlQuery(value)) + } +} + +#[derive(Debug, thiserror::Error)] +#[error("could not parse query parameters: {0}")] +struct QueryParametersParseError(serde_path_to_error::Error); +impl_into_cot_error!(QueryParametersParseError, BAD_REQUEST); + +/// Extractor that gets the request body as JSON and deserializes it into a type +/// `T` implementing [`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) -> crate::Result { + let content_type = head + .headers + .get(http::header::CONTENT_TYPE) + .map_or("".into(), |value| String::from_utf8_lossy(value.as_bytes())); + if content_type != crate::headers::JSON_CONTENT_TYPE { + return Err(InvalidContentType { + expected: crate::headers::JSON_CONTENT_TYPE, + actual: content_type.into_owned(), + } + .into()); + } + + let bytes = body.into_bytes().await?; + + let deserializer = &mut serde_json::Deserializer::from_slice(&bytes); + let result = + serde_path_to_error::deserialize(deserializer).map_err(JsonDeserializeError)?; + + Ok(Self(result)) + } +} + +#[cfg(feature = "json")] +#[derive(Debug, thiserror::Error)] +#[error("JSON deserialization error: {0}")] +struct JsonDeserializeError(serde_path_to_error::Error); +#[cfg(feature = "json")] +impl_into_cot_error!(JsonDeserializeError, BAD_REQUEST); + +// extractor impls for existing types +impl FromRequestHead for RequestHead { + async fn from_request_head(head: &RequestHead) -> crate::Result { + Ok(head.clone()) + } +} + +impl FromRequestHead for Method { + async fn from_request_head(head: &RequestHead) -> crate::Result { + Ok(head.method.clone()) + } +} + +/// A derive macro that automatically implements the [`FromRequestHead`] trait +/// for structs. +/// +/// This macro generates code to extract each field of the struct from HTTP +/// request head, making it easy to create composite extractors that combine +/// multiple data sources from an incoming request. +/// +/// The macro works by calling [`FromRequestHead::from_request_head`] on each +/// field's type, allowing you to compose extractors seamlessly. All fields must +/// implement the [`FromRequestHead`] trait for the derivation to work. +/// +/// # Requirements +/// +/// - The target struct must have all fields implement [`FromRequestHead`] +/// - Works with named fields, unnamed fields (tuple structs), and unit structs +/// - The struct must be accessible where the macro is used +/// +/// # Examples +/// +/// ## Named Fields +/// +/// ```no_run +/// use cot::request::extractors::{Path, StaticFiles, UrlQuery}; +/// use cot::router::Urls; +/// use cot_macros::FromRequestHead; +/// use serde::Deserialize; +/// +/// #[derive(Debug, FromRequestHead)] +/// pub struct BaseContext { +/// urls: Urls, +/// static_files: StaticFiles, +/// } +/// ``` +pub use cot_macros::FromRequestHead; + +use crate::error::impl_into_cot_error; + +#[cfg(test)] +mod tests { + use serde::Deserialize; + + use super::*; + use crate::request::extractors::{FromRequest, Json, Path, UrlQuery}; + + #[cfg(feature = "json")] + #[cot::test] + async fn json() { + let request = http::Request::builder() + .method(http::Method::POST) + .header( + http::header::CONTENT_TYPE, + crate::headers::JSON_CONTENT_TYPE, + ) + .body(Body::fixed(r#"{"hello":"world"}"#)) + .unwrap(); + + let (head, body) = request.into_parts(); + let Json(data): Json = Json::from_request(&head, body).await.unwrap(); + assert_eq!(data, serde_json::json!({"hello": "world"})); + } + + #[cfg(feature = "json")] + #[cot::test] + async fn json_empty() { + #[derive(Debug, Deserialize, PartialEq, Eq)] + struct TestData {} + + let request = http::Request::builder() + .method(http::Method::POST) + .header( + http::header::CONTENT_TYPE, + crate::headers::JSON_CONTENT_TYPE, + ) + .body(Body::fixed("{}")) + .unwrap(); + + let (head, body) = request.into_parts(); + let Json(data): Json = Json::from_request(&head, body).await.unwrap(); + assert_eq!(data, TestData {}); + } + + #[cfg(feature = "json")] + #[cot::test] + async fn json_struct() { + #[derive(Debug, Deserialize, PartialEq, Eq)] + struct TestDataInner { + hello: String, + } + + #[derive(Debug, Deserialize, PartialEq, Eq)] + struct TestData { + inner: TestDataInner, + } + + let request = http::Request::builder() + .method(http::Method::POST) + .header( + http::header::CONTENT_TYPE, + crate::headers::JSON_CONTENT_TYPE, + ) + .body(Body::fixed(r#"{"inner":{"hello":"world"}}"#)) + .unwrap(); + + let (head, body) = request.into_parts(); + let Json(data): Json = Json::from_request(&head, body).await.unwrap(); + assert_eq!( + data, + TestData { + inner: TestDataInner { + hello: "world".to_string(), + } + } + ); + } + + #[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] + async fn json_invalid_content_type() { + let request = http::Request::builder() + .method(http::Method::POST) + .header(http::header::CONTENT_TYPE, "text/plain") + .body(Body::fixed(r#"{"hello":"world"}"#)) + .unwrap(); + + let (head, body) = request.into_parts(); + let result = Json::::from_request(&head, body).await; + assert!(result.is_err()); + } +} diff --git a/cot/src/request/path_params_deserializer.rs b/cot-core/src/request/path_params_deserializer.rs similarity index 100% rename from cot/src/request/path_params_deserializer.rs rename to cot-core/src/request/path_params_deserializer.rs diff --git a/cot/src/response.rs b/cot-core/src/response.rs similarity index 90% rename from cot/src/response.rs rename to cot-core/src/response.rs index d0ad7007..d19ef210 100644 --- a/cot/src/response.rs +++ b/cot-core/src/response.rs @@ -1,17 +1,3 @@ -//! 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; @@ -122,8 +108,9 @@ pub trait ResponseExt: Sized + private::Sealed { /// /// # See also /// - /// * [`crate::reverse_redirect!`] – a more ergonomic way to create - /// redirects to internal views + /// * [`cot::reverse_redirect!`](../../cot/macro.reverse_redirect!.html) – a + /// more ergonomic way to create redirects to internal views (available in + /// the `cot` crate) #[must_use] #[deprecated(since = "0.5.0", note = "Use Redirect::new() instead")] fn new_redirect>(location: T) -> Self; diff --git a/cot/src/response/into_response.rs b/cot-core/src/response/into_response.rs similarity index 91% rename from cot/src/response/into_response.rs rename to cot-core/src/response/into_response.rs index 256300f7..9252343d 100644 --- a/cot/src/response/into_response.rs +++ b/cot-core/src/response/into_response.rs @@ -1,14 +1,13 @@ 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_BUILD_FAILURE, Response}; -use cot::{Body, Error, StatusCode}; use http; +use crate::error::impl_into_cot_error; #[cfg(feature = "json")] use crate::headers::JSON_CONTENT_TYPE; +use crate::headers::{HTML_CONTENT_TYPE, OCTET_STREAM_CONTENT_TYPE, PLAIN_TEXT_CONTENT_TYPE}; use crate::html::Html; -use crate::response::Redirect; +use crate::response::{RESPONSE_BUILD_FAILURE, Redirect, Response}; +use crate::{Body, Error, StatusCode}; /// Trait for generating responses. /// Types that implement `IntoResponse` can be returned from handlers. @@ -21,11 +20,11 @@ use crate::response::Redirect; /// 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`. + /// Converts the implementing type into a `crate::Result`. /// /// # Errors /// Returns an error if the conversion fails. - fn into_response(self) -> cot::Result; + fn into_response(self) -> crate::Result; /// Modifies the response by appending the specified header. /// @@ -113,7 +112,7 @@ pub struct WithHeader { } impl IntoResponse for WithHeader { - fn into_response(self) -> cot::Result { + 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); @@ -131,7 +130,7 @@ pub struct WithContentType { } impl IntoResponse for WithContentType { - fn into_response(self) -> cot::Result { + fn into_response(self) -> crate::Result { self.inner.into_response().map(|mut resp| { if let Some(content_type) = self.content_type { resp.headers_mut() @@ -150,7 +149,7 @@ pub struct WithStatus { } impl IntoResponse for WithStatus { - fn into_response(self) -> cot::Result { + fn into_response(self) -> crate::Result { self.inner.into_response().map(|mut resp| { *resp.status_mut() = self.status; resp @@ -166,7 +165,7 @@ pub struct WithBody { } impl IntoResponse for WithBody { - fn into_response(self) -> cot::Result { + fn into_response(self) -> crate::Result { self.inner.into_response().map(|mut resp| { *resp.body_mut() = self.body; resp @@ -186,7 +185,7 @@ where T: IntoResponse, D: Clone + Send + Sync + 'static, { - fn into_response(self) -> cot::Result { + fn into_response(self) -> crate::Result { self.inner.into_response().map(|mut resp| { resp.extensions_mut().insert(self.extension); resp @@ -197,7 +196,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) -> crate::Result { Body::from(self) .with_header(http::header::CONTENT_TYPE, $mime) .into_response() @@ -209,7 +208,7 @@ macro_rules! impl_into_response_for_type_and_mime { // General implementations impl IntoResponse for () { - fn into_response(self) -> cot::Result { + fn into_response(self) -> crate::Result { Body::empty().into_response() } } @@ -219,7 +218,7 @@ where R: IntoResponse, E: Into, { - fn into_response(self) -> cot::Result { + fn into_response(self) -> crate::Result { match self { Ok(value) => value.into_response(), Err(err) => Err(err.into()), @@ -228,13 +227,13 @@ where } impl IntoResponse for Error { - fn into_response(self) -> cot::Result { + fn into_response(self) -> crate::Result { Err(self) } } impl IntoResponse for Response { - fn into_response(self) -> cot::Result { + fn into_response(self) -> crate::Result { Ok(self) } } @@ -245,7 +244,7 @@ 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 { + fn into_response(self) -> crate::Result { String::from(self).into_response() } } @@ -257,25 +256,25 @@ 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 { + fn into_response(self) -> crate::Result { self.as_slice().into_response() } } impl IntoResponse for [u8; N] { - fn into_response(self) -> cot::Result { + fn into_response(self) -> crate::Result { self.to_vec().into_response() } } impl IntoResponse for Box<[u8]> { - fn into_response(self) -> cot::Result { + fn into_response(self) -> crate::Result { Vec::from(self).into_response() } } impl IntoResponse for BytesMut { - fn into_response(self) -> cot::Result { + fn into_response(self) -> crate::Result { self.freeze().into_response() } } @@ -283,13 +282,13 @@ impl IntoResponse for BytesMut { // HTTP structures for common uses impl IntoResponse for StatusCode { - fn into_response(self) -> cot::Result { + fn into_response(self) -> crate::Result { ().into_response().with_status(self).into_response() } } impl IntoResponse for http::HeaderMap { - fn into_response(self) -> cot::Result { + fn into_response(self) -> crate::Result { ().into_response().map(|mut resp| { *resp.headers_mut() = self; resp @@ -298,7 +297,7 @@ impl IntoResponse for http::HeaderMap { } impl IntoResponse for http::Extensions { - fn into_response(self) -> cot::Result { + fn into_response(self) -> crate::Result { ().into_response().map(|mut resp| { *resp.extensions_mut() = self; resp @@ -307,7 +306,7 @@ impl IntoResponse for http::Extensions { } impl IntoResponse for crate::response::ResponseHead { - fn into_response(self) -> cot::Result { + fn into_response(self) -> crate::Result { Ok(Response::from_parts(self, Body::empty())) } } @@ -330,7 +329,7 @@ impl IntoResponse for Html { /// /// let response = html.into_response(); /// ``` - fn into_response(self) -> cot::Result { + fn into_response(self) -> crate::Result { self.0 .into_response() .with_content_type(HTML_CONTENT_TYPE) @@ -339,7 +338,7 @@ impl IntoResponse for Html { } #[cfg(feature = "json")] -impl IntoResponse for cot::json::Json { +impl IntoResponse for crate::json::Json { /// Create a new JSON response. /// /// This creates a new [`Response`] object with a content type of @@ -358,7 +357,7 @@ impl IntoResponse for cot::json::Json { /// /// 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; @@ -381,13 +380,13 @@ impl_into_cot_error!(JsonSerializeError, INTERNAL_SERVER_ERROR); // Shortcuts for common uses impl IntoResponse for Body { - fn into_response(self) -> cot::Result { + fn into_response(self) -> crate::Result { Ok(Response::new(self)) } } impl IntoResponse for Redirect { - fn into_response(self) -> cot::Result { + fn into_response(self) -> crate::Result { let response = http::Response::builder() .status(StatusCode::SEE_OTHER) .header(http::header::LOCATION, self.0) @@ -400,13 +399,13 @@ impl IntoResponse for Redirect { #[cfg(test)] mod tests { use bytes::{Bytes, BytesMut}; - use cot::response::Response; - use cot::{Body, StatusCode}; - use http::{self, HeaderMap, HeaderValue}; + use http::{self, HeaderMap, HeaderValue, Method}; use super::*; - use crate::error::NotFound; + use crate::error::MethodNotAllowed; use crate::html::Html; + use crate::response::Response; + use crate::{Body, StatusCode}; #[cot::test] async fn test_unit_into_response() { @@ -433,13 +432,13 @@ mod tests { #[cot::test] async fn test_result_err_into_response() { - let err = Error::from(NotFound::with_message("test")); + let err = Error::from(MethodNotAllowed::new(Method::POST)); 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")); + assert!(error_result.err().unwrap().to_string().contains("POST")); } #[cot::test] @@ -792,7 +791,7 @@ mod tests { name: "test".to_string(), value: 123, }; - let json = cot::json::Json(data); + let json = crate::json::Json(data); let response = json.into_response().unwrap(); assert_eq!(response.status(), StatusCode::OK); @@ -813,7 +812,7 @@ mod tests { use std::collections::HashMap; let data = HashMap::from([("key", "value")]); - let json = cot::json::Json(data); + let json = crate::json::Json(data); let response = json.into_response().unwrap(); assert_eq!(response.status(), StatusCode::OK); diff --git a/cot/Cargo.toml b/cot/Cargo.toml index 9f16a502..f12f86d3 100644 --- a/cot/Cargo.toml +++ b/cot/Cargo.toml @@ -20,12 +20,12 @@ aide = { workspace = true, optional = true } askama = { workspace = true, features = ["std"] } async-trait.workspace = true axum = { workspace = true, features = ["http1", "tokio"] } -backtrace.workspace = true bytes.workspace = true chrono = { workspace = true, features = ["alloc", "serde", "clock"] } chrono-tz.workspace = true chumsky = { workspace = true, optional = 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 @@ -39,7 +39,6 @@ futures-util.workspace = true hex.workspace = true hmac.workspace = true http-body-util.workspace = true -http-body.workspace = true http.workspace = true humantime.workspace = true idna = { workspace = true, optional = true } @@ -55,14 +54,11 @@ schemars = { workspace = true, optional = true } sea-query = { workspace = true, optional = true } sea-query-binder = { workspace = true, features = ["with-chrono", "runtime-tokio"], optional = true } serde = { workspace = true, features = ["derive"] } -serde_html_form = { workspace = true } serde_json = { workspace = true, optional = true } -serde_path_to_error = { workspace = true } sha2.workspace = true sqlx = { workspace = true, features = ["runtime-tokio", "chrono"], optional = true } subtle = { workspace = true, features = ["std"] } swagger-ui-redist = { workspace = true, optional = true } -sync_wrapper.workspace = true thiserror.workspace = true time.workspace = true tokio = { workspace = true, features = ["macros", "rt-multi-thread", "signal", "fs", "io-util"] } @@ -74,11 +70,9 @@ tracing.workspace = true url = { workspace = true, features = ["serde"] } [dev-dependencies] -async-stream.workspace = true criterion = { workspace = true, features = ["async_tokio"] } fake.workspace = true fantoccini.workspace = true -futures.workspace = true mockall.workspace = true reqwest = { workspace = true, features = ["json"] } rustversion.workspace = true @@ -118,7 +112,7 @@ sqlite = ["db", "sea-query/backend-sqlite", "sea-query-binder/sqlx-sqlite", "sql 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"] +json = ["dep:serde_json", "cot_core/json"] openapi = ["json", "dep:aide", "dep:schemars"] swagger-ui = ["openapi", "dep:swagger-ui-redist"] live-reload = ["dep:tower-livereload"] diff --git a/cot/src/auth.rs b/cot/src/auth.rs index 8dd86daf..f9841cfa 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::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/cache.rs b/cot/src/cache.rs index c1f282e7..ed3106c6 100644 --- a/cot/src/cache.rs +++ b/cot/src/cache.rs @@ -116,6 +116,7 @@ use std::future::Future; use std::sync::Arc; use cot::config::CacheStoreTypeConfig; +use cot_core::error::impl_into_cot_error; use derive_more::with_trait::Debug; use serde::Serialize; use serde::de::DeserializeOwned; @@ -126,7 +127,6 @@ use crate::cache::store::memory::Memory; use crate::cache::store::redis::Redis; use crate::cache::store::{BoxCacheStore, CacheStore}; use crate::config::{CacheConfig, Timeout}; -use crate::error::error_impl::impl_into_cot_error; /// An error that can occur when interacting with the cache. #[derive(Debug, Error)] diff --git a/cot/src/cache/store.rs b/cot/src/cache/store.rs index ef278cbb..78a84dac 100644 --- a/cot/src/cache/store.rs +++ b/cot/src/cache/store.rs @@ -12,11 +12,11 @@ pub mod redis; use std::fmt::Debug; use std::pin::Pin; +use cot_core::error::impl_into_cot_error; use serde_json::Value; use thiserror::Error; use crate::config::Timeout; -use crate::error::error_impl::impl_into_cot_error; const CACHE_STORE_ERROR_PREFIX: &str = "cache store error:"; diff --git a/cot/src/cache/store/redis.rs b/cot/src/cache/store/redis.rs index 1043da85..4d2a6a88 100644 --- a/cot/src/cache/store/redis.rs +++ b/cot/src/cache/store/redis.rs @@ -18,6 +18,7 @@ //! # } use cot::cache::store::CacheStoreResult; use cot::config::Timeout; +use cot_core::error::impl_into_cot_error; use deadpool_redis::{Config, Connection, Pool, Runtime}; use redis::{AsyncCommands, SetExpiry, SetOptions}; use serde_json::Value; @@ -25,7 +26,6 @@ use thiserror::Error; use crate::cache::store::{CacheStore, CacheStoreError}; use crate::config::CacheUrl; -use crate::error::error_impl::impl_into_cot_error; const ERROR_PREFIX: &str = "redis cache store error:"; diff --git a/cot/src/config.rs b/cot/src/config.rs index fbe86659..2cd25369 100644 --- a/cot/src/config.rs +++ b/cot/src/config.rs @@ -19,6 +19,7 @@ use std::path::PathBuf; use std::time::Duration; use chrono::{DateTime, FixedOffset, Utc}; +use cot_core::error::impl_into_cot_error; use derive_builder::Builder; use derive_more::with_trait::{Debug, From}; use serde::{Deserialize, Serialize}; @@ -27,7 +28,6 @@ use thiserror::Error; #[cfg(feature = "email")] use crate::email::transport::smtp::Mechanism; -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 96d7cde9..ba6f1ed2 100644 --- a/cot/src/db.rs +++ b/cot/src/db.rs @@ -21,6 +21,7 @@ use std::str::FromStr; use std::sync::Arc; use async_trait::async_trait; +use cot_core::error::impl_into_cot_error; pub use cot_macros::{model, query}; use derive_more::{Debug, Deref, Display}; #[cfg(test)] @@ -42,7 +43,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/email.rs b/cot/src/email.rs index 8bd6855a..ae764075 100644 --- a/cot/src/email.rs +++ b/cot/src/email.rs @@ -32,6 +32,7 @@ use std::sync::Arc; use cot::config::{EmailConfig, EmailTransportTypeConfig}; use cot::email::transport::smtp::Smtp; +use cot_core::error::impl_into_cot_error; use derive_builder::Builder; use derive_more::with_trait::Debug; use thiserror::Error; @@ -39,7 +40,6 @@ use transport::{BoxedTransport, Transport}; use crate::email::transport::TransportError; use crate::email::transport::console::Console; -use crate::error::error_impl::impl_into_cot_error; const ERROR_PREFIX: &str = "email message build error:"; /// Represents errors that can occur when sending an email. diff --git a/cot/src/email/transport.rs b/cot/src/email/transport.rs index f5d75c70..ab5ae574 100644 --- a/cot/src/email/transport.rs +++ b/cot/src/email/transport.rs @@ -8,10 +8,10 @@ use std::future::Future; use std::pin::Pin; use cot::email::EmailMessageError; +use cot_core::error::impl_into_cot_error; use thiserror::Error; use crate::email::EmailMessage; -use crate::error::error_impl::impl_into_cot_error; pub mod console; pub mod smtp; diff --git a/cot/src/error.rs b/cot/src/error.rs index 2e24fa37..4dc0739e 100644 --- a/cot/src/error.rs +++ b/cot/src/error.rs @@ -1,10 +1,12 @@ -pub(crate) mod backtrace; -pub(crate) mod error_impl; +//! 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 handler; -mod method_not_allowed; mod not_found; -mod uncaught_panic; -pub use method_not_allowed::MethodNotAllowed; +#[doc(inline)] +pub use cot_core::error::{MethodNotAllowed, UncaughtPanic}; pub use not_found::{Kind as NotFoundKind, NotFound}; -pub use uncaught_panic::UncaughtPanic; diff --git a/cot/src/error/handler.rs b/cot/src/error/handler.rs index 8dd03002..cbfd9eeb 100644 --- a/cot/src/error/handler.rs +++ b/cot/src/error/handler.rs @@ -7,10 +7,10 @@ use std::pin::Pin; use std::sync::Arc; use std::task::{Context, Poll}; +use cot_core::handler::handle_all_parameters; 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; @@ -20,7 +20,7 @@ use crate::response::Response; /// This trait is implemented by functions that can handle error pages. The /// trait is automatically implemented for async functions that take parameters /// implementing [`FromRequestHead`] and return a type that implements -/// [`IntoResponse`]. +/// [`IntoResponse`](crate::response::IntoResponse). /// /// # Examples /// diff --git a/cot/src/error/not_found.rs b/cot/src/error/not_found.rs index f71144ba..f62c7057 100644 --- a/cot/src/error/not_found.rs +++ b/cot/src/error/not_found.rs @@ -1,9 +1,8 @@ //! Error types and utilities for handling "404 Not Found" errors. +use cot_core::error::impl_into_cot_error; use thiserror::Error; -use crate::error::error_impl::impl_into_cot_error; - #[expect(clippy::doc_link_with_quotes, reason = "404 Not Found link")] /// A ["404 Not Found"] error that can be returned by Cot applications. /// diff --git a/cot/src/error_page.rs b/cot/src/error_page.rs index 8f43ee35..16e2eec4 100644 --- a/cot/src/error_page.rs +++ b/cot/src/error_page.rs @@ -2,11 +2,11 @@ use std::any::Any; use std::panic::PanicHookInfo; use std::sync::Arc; +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, Template}; @@ -72,7 +72,6 @@ impl ErrorPageTemplateBuilder { if let Some(not_found) = error.inner().downcast_ref::() { use crate::error::NotFoundKind as Kind; match ¬_found.kind { - Kind::FromRouter => {} Kind::Custom => { Self::build_error_data(&mut error_data, error); } @@ -80,6 +79,8 @@ impl ErrorPageTemplateBuilder { Self::build_error_data(&mut error_data, error); error_message = Some(message.clone()); } + // We don't need to build error data for Kind::FromRouter + _ => {} } } @@ -326,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 67d790f9..dfc01f4d 100644 --- a/cot/src/form.rs +++ b/cot/src/form.rs @@ -31,6 +31,8 @@ use async_trait::async_trait; use bytes::Bytes; use chrono::NaiveDateTime; use chrono_tz::Tz; +use cot_core::error::impl_into_cot_error; +use cot_core::headers::{MULTIPART_FORM_CONTENT_TYPE, URLENCODED_FORM_CONTENT_TYPE}; /// 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,8 +63,6 @@ pub use field_value::{FormFieldValue, FormFieldValueError}; use http_body_util::BodyExt; use thiserror::Error; -use crate::error::error_impl::impl_into_cot_error; -use crate::headers::{MULTIPART_FORM_CONTENT_TYPE, URLENCODED_FORM_CONTENT_TYPE}; use crate::request::{Request, RequestExt}; const ERROR_PREFIX: &str = "failed to process a form:"; @@ -657,10 +657,10 @@ pub trait AsFormField { #[cfg(test)] mod tests { use bytes::Bytes; + use cot_core::headers::{MULTIPART_FORM_CONTENT_TYPE, URLENCODED_FORM_CONTENT_TYPE}; use super::*; use crate::Body; - use crate::headers::{MULTIPART_FORM_CONTENT_TYPE, URLENCODED_FORM_CONTENT_TYPE}; #[cot::test] async fn urlencoded_form_data_extract_get_empty() { diff --git a/cot/src/form/field_value.rs b/cot/src/form/field_value.rs index 04c31c1b..50dd0dd9 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::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/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 3c363c45..cc11ba1a 100644 --- a/cot/src/lib.rs +++ b/cot/src/lib.rs @@ -55,38 +55,25 @@ pub mod cache; #[cfg(feature = "db")] pub mod db; -/// Error handling types and utilities for Cot applications. -/// -/// This module provides error types, error handlers, and utilities for -/// handling various types of errors that can occur in Cot applications, -/// including 404 Not Found errors, uncaught panics, and custom error pages. pub mod error; pub mod form; -mod headers; // Not public API. Referenced by macro-generated code. #[doc(hidden)] #[path = "private.rs"] pub mod __private; pub mod admin; pub mod auth; -mod body; pub mod cli; pub mod common_types; pub mod config; #[cfg(feature = "email")] pub mod email; mod error_page; -#[macro_use] -pub(crate) mod handler; -pub mod html; -#[cfg(feature = "json")] -pub mod json; pub mod middleware; #[cfg(feature = "openapi")] pub mod openapi; pub mod project; pub mod request; -pub mod response; pub mod router; mod serializers; pub mod session; @@ -97,7 +84,40 @@ pub(crate) mod utils; #[cfg(feature = "openapi")] pub use aide; -pub use body::Body; +/// A wrapper around a handler that's used in [`Bootstrapper`]. +/// +/// It is returned by [`Bootstrapper::finish`]. Typically, you don't need to +/// interact with this type directly, except for creating it in +/// [`Project::middlewares`] through the +/// [`RootHandlerBuilder::build`](crate::project::RootHandlerBuilder::build) +/// method. +/// +/// # Examples +/// +/// ``` +/// use cot::config::ProjectConfig; +/// use cot::{Bootstrapper, BoxedHandler, Project}; +/// +/// struct MyProject; +/// impl Project for MyProject {} +/// +/// # #[tokio::main] +/// # async fn main() -> cot::Result<()> { +/// let bootstrapper = Bootstrapper::new(MyProject) +/// .with_config(ProjectConfig::default()) +/// .boot() +/// .await?; +/// let handler: BoxedHandler = bootstrapper.finish().handler; +/// # Ok(()) +/// # } +/// ``` +pub use cot_core::handler::BoxedHandler; +pub use cot_core::handler::RequestHandler; +#[cfg(feature = "json")] +#[doc(inline)] +pub use cot_core::json; +#[doc(inline)] +pub use cot_core::{Body, Method, Result, StatusCode, error::Error, html, response}; /// An attribute macro that defines an end-to-end test function for a /// Cot-powered app. /// @@ -166,17 +186,6 @@ pub use schemars; pub use {bytes, http}; pub use crate::__private::askama::{Template, filter_fn}; -pub use crate::error::error_impl::Error; -pub use crate::handler::{BoxedHandler, RequestHandler}; pub use crate::project::{ App, AppBuilder, Bootstrapper, Project, ProjectContext, run, run_at, run_cli, }; - -/// A type alias for a result that can return a [`cot::Error`]. -pub type Result = std::result::Result; - -/// A type alias for an HTTP status code. -pub type StatusCode = http::StatusCode; - -/// A type alias for an HTTP method. -pub type Method = http::Method; diff --git a/cot/src/middleware.rs b/cot/src/middleware.rs index b9e6e27c..ca946e2b 100644 --- a/cot/src/middleware.rs +++ b/cot/src/middleware.rs @@ -9,15 +9,12 @@ use std::fmt::Debug; use std::sync::Arc; use std::task::{Context, Poll}; -use bytes::Bytes; 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}; +use crate::Error; #[cfg(feature = "cache")] use crate::config::CacheType; use crate::config::{Expiry, SameSite, SessionStoreTypeConfig}; @@ -32,22 +29,17 @@ use crate::session::store::file::FileStore; use crate::session::store::memory::MemoryStore; #[cfg(feature = "redis")] use crate::session::store::redis::RedisStore; -use crate::{Body, Error}; #[cfg(feature = "live-reload")] mod live_reload; -#[cfg(feature = "live-reload")] -pub use live_reload::LiveReloadMiddleware; - -/// Middleware that converts a any [`http::Response`] generic type to a -/// [`cot::response::Response`]. +/// Middleware that converts any error type to [`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. +/// [`RootHandlerBuilder::middleware`](crate::project::RootHandlerBuilder::middleware) and is not needed to be added +/// manually. /// /// # Examples /// @@ -64,102 +56,20 @@ pub use live_reload::LiveReloadMiddleware; /// context: &MiddlewareContext, /// ) -> RootHandler { /// handler -/// // IntoCotResponseLayer used internally in middleware() +/// // IntoCotErrorLayer used internally in middleware() /// .middleware(LiveReloadMiddleware::from_context(context)) /// .build() /// } /// } /// ``` -#[derive(Debug, Copy, Clone)] -pub struct IntoCotResponseLayer; - -impl IntoCotResponseLayer { - /// Create a new [`IntoCotResponseLayer`]. - /// - /// # Examples - /// - /// ``` - /// use cot::middleware::IntoCotResponseLayer; - /// - /// let middleware = IntoCotResponseLayer::new(); - /// ``` - #[must_use] - pub fn new() -> Self { - Self {} - } -} - -impl Default for IntoCotResponseLayer { - fn default() -> Self { - Self::new() - } -} - -impl tower::Layer for IntoCotResponseLayer { - type Service = IntoCotResponse; - - fn layer(&self, inner: S) -> Self::Service { - IntoCotResponse { inner } - } -} - -/// Service struct that converts any [`http::Response`] generic type to -/// [`cot::response::Response`]. -/// -/// Used by [`IntoCotResponseLayer`]. -/// -/// # Examples -/// -/// ``` -/// use std::any::TypeId; -/// -/// use cot::middleware::{IntoCotResponse, IntoCotResponseLayer}; -/// -/// assert_eq!( -/// TypeId::of::<>::Service>(), -/// TypeId::of::>() -/// ); -/// ``` -#[derive(Debug, Clone)] -pub struct IntoCotResponse { - inner: S, -} - -impl Service for IntoCotResponse -where - S: Service>, - ResBody: http_body::Body + Send + Sync + 'static, - E: std::error::Error + Send + Sync + 'static, -{ - type Response = Response; - type Error = S::Error; - type Future = futures_util::future::MapOk) -> Response>; - - #[inline] - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - self.inner.poll_ready(cx) - } - - #[inline] - fn call(&mut self, request: Request) -> Self::Future { - self.inner.call(request).map_ok(map_response) - } -} - -fn map_response(response: http::response::Response) -> Response -where - ResBody: http_body::Body + Send + Sync + 'static, - E: std::error::Error + Send + Sync + 'static, -{ - response.map(|body| Body::wrapper(BoxBody::new(body.map_err(map_err)))) -} - -/// Middleware that converts any error type to [`cot::Error`]. +pub use cot_core::middleware::IntoCotErrorLayer; +/// Middleware that converts any `http::Response` generic type +/// to a [`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()) +/// [`RootHandlerBuilder::middleware`](crate::project::RootHandlerBuilder::middleware) /// and is not needed to be added manually. /// /// # Examples @@ -177,94 +87,17 @@ where /// context: &MiddlewareContext, /// ) -> RootHandler { /// handler -/// // IntoCotErrorLayer used internally in middleware() +/// // IntoCotResponseLayer 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) -} +pub use cot_core::middleware::IntoCotResponseLayer; +#[doc(inline)] +pub use cot_core::middleware::{IntoCotError, IntoCotResponse}; +#[cfg(feature = "live-reload")] +pub use live_reload::LiveReloadMiddleware; type DynamicSessionStore = SessionManagerLayer; diff --git a/cot/src/openapi.rs b/cot/src/openapi.rs index 5be3cc05..af26bfa7 100644 --- a/cot/src/openapi.rs +++ b/cot/src/openapi.rs @@ -113,6 +113,7 @@ use aide::openapi::{ MediaType, Operation, Parameter, ParameterData, ParameterSchemaOrContent, PathItem, PathStyle, QueryStyle, ReferenceOr, RequestBody, StatusCode, }; +use cot_core::handler::{BoxRequestHandler, RequestHandler, handle_all_parameters}; /// Derive macro for the [`ApiOperationResponse`] trait. /// /// This macro can be applied to enums to automatically implement the @@ -207,14 +208,13 @@ use serde_json::Value; use crate::auth::Auth; use crate::form::Form; -use crate::handler::BoxRequestHandler; use crate::json::Json; use crate::request::extractors::{FromRequest, FromRequestHead, Path, RequestForm, UrlQuery}; use crate::request::{Request, RequestHead}; use crate::response::{Response, WithExtension}; use crate::router::Urls; use crate::session::Session; -use crate::{Body, Method, RequestHandler}; +use crate::{Body, Method}; /// Context for API route generation. /// @@ -847,7 +847,7 @@ impl ApiOperationPart for Json { ) { operation.request_body = Some(ReferenceOr::Item(RequestBody { content: IndexMap::from([( - crate::headers::JSON_CONTENT_TYPE.to_string(), + cot_core::headers::JSON_CONTENT_TYPE.to_string(), MediaType { schema: Some(aide::openapi::SchemaObject { json_schema: D::json_schema(schema_generator), @@ -970,7 +970,7 @@ impl ApiOperationPart for RequestForm { } else { operation.request_body = Some(ReferenceOr::Item(RequestBody { content: IndexMap::from([( - crate::headers::URLENCODED_FORM_CONTENT_TYPE.to_string(), + cot_core::headers::URLENCODED_FORM_CONTENT_TYPE.to_string(), MediaType { schema: Some(aide::openapi::SchemaObject { json_schema: F::json_schema(schema_generator), @@ -1062,7 +1062,7 @@ impl ApiOperationResponse for Json { aide::openapi::Response { description: "OK".to_string(), content: IndexMap::from([( - crate::headers::JSON_CONTENT_TYPE.to_string(), + cot_core::headers::JSON_CONTENT_TYPE.to_string(), MediaType { schema: Some(aide::openapi::SchemaObject { json_schema: S::json_schema(schema_generator), diff --git a/cot/src/project.rs b/cot/src/project.rs index b1f94c7f..8420855d 100644 --- a/cot/src/project.rs +++ b/cot/src/project.rs @@ -28,6 +28,9 @@ use std::sync::Arc; use async_trait::async_trait; use axum::handler::HandlerWithoutStateExt; use cot::Template; +use cot_core::error::impl_into_cot_error; +use cot_core::handler::BoxedHandler; +use cot_core::request::AppName; use derive_more::with_trait::Debug; use futures_util::FutureExt; use thiserror::Error; @@ -54,13 +57,11 @@ use crate::db::migrations::{MigrationEngine, SyncDynMigration}; #[cfg(feature = "email")] use crate::email::Email; use crate::error::UncaughtPanic; -use crate::error::error_impl::impl_into_cot_error; use crate::error::handler::{DynErrorPageHandler, RequestOuterError}; use crate::error_page::Diagnostics; -use crate::handler::BoxedHandler; use crate::html::Html; use crate::middleware::{IntoCotError, IntoCotErrorLayer, IntoCotResponse, IntoCotResponseLayer}; -use crate::request::{AppName, Request, RequestExt, RequestHead}; +use crate::request::{Request, RequestExt, RequestHead}; use crate::response::{IntoResponse, Response}; use crate::router::{Route, Router, RouterService}; use crate::static_files::StaticFile; diff --git a/cot/src/request.rs b/cot/src/request.rs index 033ea374..d6499934 100644 --- a/cot/src/request.rs +++ b/cot/src/request.rs @@ -15,23 +15,16 @@ use std::future::Future; use std::sync::Arc; +use cot_core::request::{AppName, InvalidContentType, RouteName}; +#[doc(inline)] +pub use cot_core::request::{PathParams, PathParamsDeserializerError, Request, RequestHead}; use http::Extensions; -use indexmap::IndexMap; -use crate::error::error_impl::impl_into_cot_error; +use crate::Result; 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 {} } @@ -393,308 +386,15 @@ impl RequestExt for RequestHead { } } -#[derive(Debug, thiserror::Error)] -#[error("invalid content type; expected `{expected}`, found `{actual}`")] -pub(crate) struct InvalidContentType { - expected: &'static str, - actual: String, -} -impl_into_cot_error!(InvalidContentType, BAD_REQUEST); - -#[repr(transparent)] -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub(crate) struct AppName(pub(crate) String); - -#[repr(transparent)] -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub(crate) struct RouteName(pub(crate) String); - -/// Path parameters extracted from the request URL, and available as a map of -/// strings. -/// -/// This struct is meant to be mainly used using the [`PathParams::parse`] -/// method, which will deserialize the path parameters into a type `T` -/// implementing `serde::DeserializeOwned`. If needed, you can also access the -/// path parameters directly using the [`PathParams::get`] method. -/// -/// # Examples -/// -/// ``` -/// use cot::request::{PathParams, Request, RequestExt}; -/// use cot::response::Response; -/// use cot::test::TestRequestBuilder; -/// -/// async fn my_handler(mut request: Request) -> cot::Result { -/// let path_params = request.path_params(); -/// let name = path_params.get("name").unwrap(); -/// -/// // using more ergonomic syntax: -/// let name: String = request.path_params().parse()?; -/// -/// let name = println!("Hello, {}!", name); -/// // ... -/// # unimplemented!() -/// } -/// ``` -#[derive(Debug, Clone)] -pub struct PathParams { - params: IndexMap, -} - -impl Default for PathParams { - fn default() -> Self { - Self::new() - } -} - -impl PathParams { - /// Creates a new [`PathParams`] instance. - /// - /// # Examples - /// - /// ``` - /// use cot::request::PathParams; - /// - /// let mut path_params = PathParams::new(); - /// path_params.insert("name".into(), "world".into()); - /// assert_eq!(path_params.get("name"), Some("world")); - /// ``` - #[must_use] - pub fn new() -> Self { - Self { - params: IndexMap::new(), - } - } - - /// Inserts a new path parameter. - /// - /// # Examples - /// - /// ``` - /// use cot::request::PathParams; - /// - /// let mut path_params = PathParams::new(); - /// path_params.insert("name".into(), "world".into()); - /// assert_eq!(path_params.get("name"), Some("world")); - /// ``` - pub fn insert(&mut self, name: String, value: String) { - self.params.insert(name, value); - } - - /// Iterates over the path parameters. - /// - /// # Examples - /// - /// ``` - /// use cot::request::PathParams; - /// - /// let mut path_params = PathParams::new(); - /// path_params.insert("name".into(), "world".into()); - /// for (name, value) in path_params.iter() { - /// println!("{}: {}", name, value); - /// } - /// ``` - pub fn iter(&self) -> impl Iterator { - self.params - .iter() - .map(|(name, value)| (name.as_str(), value.as_str())) - } - - /// Returns the number of path parameters. - /// - /// # Examples - /// - /// ``` - /// use cot::request::PathParams; - /// - /// let path_params = PathParams::new(); - /// assert_eq!(path_params.len(), 0); - /// ``` - #[must_use] - pub fn len(&self) -> usize { - self.params.len() - } - - /// Returns `true` if the path parameters are empty. - /// - /// # Examples - /// - /// ``` - /// use cot::request::PathParams; - /// - /// let path_params = PathParams::new(); - /// assert!(path_params.is_empty()); - /// ``` - #[must_use] - pub fn is_empty(&self) -> bool { - self.params.is_empty() - } - - /// Returns the value of a path parameter. - /// - /// # Examples - /// - /// ``` - /// use cot::request::PathParams; - /// - /// let mut path_params = PathParams::new(); - /// path_params.insert("name".into(), "world".into()); - /// assert_eq!(path_params.get("name"), Some("world")); - /// ``` - #[must_use] - pub fn get(&self, name: &str) -> Option<&str> { - self.params.get(name).map(String::as_str) - } - - /// Returns the value of a path parameter at the given index. - /// - /// # Examples - /// - /// ``` - /// use cot::request::PathParams; - /// - /// let mut path_params = PathParams::new(); - /// path_params.insert("name".into(), "world".into()); - /// assert_eq!(path_params.get_index(0), Some("world")); - /// ``` - #[must_use] - pub fn get_index(&self, index: usize) -> Option<&str> { - self.params - .get_index(index) - .map(|(_, value)| value.as_str()) - } - - /// Returns the key of a path parameter at the given index. - /// - /// # Examples - /// - /// ``` - /// use cot::request::PathParams; - /// - /// let mut path_params = PathParams::new(); - /// path_params.insert("name".into(), "world".into()); - /// assert_eq!(path_params.key_at_index(0), Some("name")); - /// ``` - #[must_use] - pub fn key_at_index(&self, index: usize) -> Option<&str> { - self.params.get_index(index).map(|(key, _)| key.as_str()) - } - - /// Deserializes the path parameters into a type `T` implementing - /// `serde::DeserializeOwned`. - /// - /// # Errors - /// - /// Throws an error if the path parameters could not be deserialized. - /// - /// # Examples - /// - /// ``` - /// use cot::request::PathParams; - /// - /// # fn main() -> Result<(), cot::Error> { - /// let mut path_params = PathParams::new(); - /// path_params.insert("hello".into(), "world".into()); - /// - /// let hello: String = path_params.parse()?; - /// assert_eq!(hello, "world"); - /// # Ok(()) - /// # } - /// ``` - /// - /// ``` - /// use cot::request::PathParams; - /// - /// # fn main() -> Result<(), cot::Error> { - /// let mut path_params = PathParams::new(); - /// path_params.insert("hello".into(), "world".into()); - /// path_params.insert("name".into(), "john".into()); - /// - /// let (hello, name): (String, String) = path_params.parse()?; - /// assert_eq!(hello, "world"); - /// assert_eq!(name, "john"); - /// # Ok(()) - /// # } - /// ``` - /// - /// ``` - /// use cot::request::PathParams; - /// use serde::Deserialize; - /// - /// # fn main() -> Result<(), cot::Error> { - /// let mut path_params = PathParams::new(); - /// path_params.insert("hello".into(), "world".into()); - /// path_params.insert("name".into(), "john".into()); - /// - /// #[derive(Deserialize)] - /// struct Params { - /// hello: String, - /// name: String, - /// } - /// - /// let params: Params = path_params.parse()?; - /// assert_eq!(params.hello, "world"); - /// assert_eq!(params.name, "john"); - /// # Ok(()) - /// # } - /// ``` - pub fn parse<'de, T: serde::Deserialize<'de>>( - &'de self, - ) -> std::result::Result { - let deserializer = path_params_deserializer::PathParamsDeserializer::new(self); - serde_path_to_error::deserialize(deserializer).map_err(PathParamsDeserializerError) - } -} - -/// An error that occurs when deserializing path parameters. -#[derive(Debug, Clone, thiserror::Error)] -#[error("could not parse path parameters: {0}")] -pub struct PathParamsDeserializerError( - // A wrapper over the original deserializer error. The exact error reason - // shouldn't be useful to the user, hence we're not exposing it. - #[source] serde_path_to_error::Error, -); -impl_into_cot_error!(PathParamsDeserializerError, BAD_REQUEST); - #[cfg(test)] mod tests { use super::*; + use crate::Body; 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(); diff --git a/cot/src/request/extractors.rs b/cot/src/request/extractors.rs index f7d9ebab..6d1ded82 100644 --- a/cot/src/request/extractors.rs +++ b/cot/src/request/extractors.rs @@ -48,20 +48,9 @@ //! # } //! ``` -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}; - +use cot_core::error::impl_into_cot_error; /// Trait for extractors that consume the request body. /// /// Extractors implementing this trait are used in route handlers that consume @@ -69,25 +58,7 @@ use crate::{Body, Method}; /// /// 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)) - } -} - +pub use cot_core::request::extractors::FromRequest; /// Trait for extractors that don't consume the request body. /// /// Extractors implementing this trait are used in route handlers that don't @@ -96,16 +67,16 @@ impl FromRequest for 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; -} +pub use cot_core::request::extractors::FromRequestHead; +#[doc(inline)] +pub use cot_core::request::extractors::{Path, UrlQuery}; + +use crate::Body; +use crate::auth::Auth; +use crate::form::{Form, FormResult}; +use crate::request::{Request, RequestExt, RequestHead}; +use crate::router::Urls; +use crate::session::Session; impl FromRequestHead for Urls { async fn from_request_head(head: &RequestHead) -> cot::Result { @@ -113,210 +84,8 @@ impl FromRequestHead for Urls { } } -/// 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)) - } -} - -#[derive(Debug, thiserror::Error)] -#[error("could not parse query parameters: {0}")] -struct QueryParametersParseError(serde_path_to_error::Error); -impl_into_cot_error!(QueryParametersParseError, BAD_REQUEST); - -/// Extractor that gets the request body as JSON and deserializes it into a type -/// `T` implementing `serde::de::DeserializeOwned`. -/// -/// The content type of the request must be `application/json`. -/// -/// # Errors -/// -/// Throws an error if the content type is not `application/json`. -/// Throws an error if the request body could not be read. -/// Throws an error if the request body could not be deserialized - either -/// because the JSON is invalid or because the deserialization to the target -/// structure failed. -/// -/// # Example -/// -/// ``` -/// use cot::RequestHandler; -/// use cot::json::Json; -/// use cot::test::TestRequestBuilder; -/// use serde::{Deserialize, Serialize}; -/// -/// #[derive(Serialize, Deserialize)] -/// struct MyData { -/// hello: String, -/// } -/// -/// async fn my_handler(Json(data): Json) -> Json { -/// Json(data) -/// } -/// -/// # #[tokio::main] -/// # async fn main() -> cot::Result<()> { -/// let request = TestRequestBuilder::get("/") -/// .json(&MyData { -/// hello: "world".to_string(), -/// }) -/// .build(); -/// -/// assert_eq!( -/// my_handler -/// .handle(request) -/// .await? -/// .into_body() -/// .into_bytes() -/// .await?, -/// "{\"hello\":\"world\"}" -/// ); -/// # Ok(()) -/// # } -/// ``` -#[cfg(feature = "json")] -impl FromRequest for Json { - async fn from_request(head: &RequestHead, body: Body) -> cot::Result { - let content_type = head - .headers - .get(http::header::CONTENT_TYPE) - .map_or("".into(), |value| String::from_utf8_lossy(value.as_bytes())); - if content_type != cot::headers::JSON_CONTENT_TYPE { - return Err(InvalidContentType { - expected: cot::headers::JSON_CONTENT_TYPE, - actual: content_type.into_owned(), - } - .into()); - } - - let bytes = body.into_bytes().await?; - - let deserializer = &mut serde_json::Deserializer::from_slice(&bytes); - let result = - serde_path_to_error::deserialize(deserializer).map_err(JsonDeserializeError)?; - - Ok(Self(result)) - } -} - -#[cfg(feature = "json")] -#[derive(Debug, thiserror::Error)] -#[error("JSON deserialization error: {0}")] -struct JsonDeserializeError(serde_path_to_error::Error); -#[cfg(feature = "json")] -impl_into_cot_error!(JsonDeserializeError, BAD_REQUEST); - /// An extractor that gets the request body as form data and deserializes it -/// into a type `F` implementing `cot::form::Form`. +/// into a type `F` implementing [`Form`]. /// /// The content type of the request must be `application/x-www-form-urlencoded`. /// @@ -501,19 +270,6 @@ impl FromRequestHead for StaticFiles { } } -// 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()) @@ -532,184 +288,44 @@ impl FromRequestHead for Auth { } } -/// A derive macro that automatically implements the [`FromRequestHead`] trait -/// for structs. -/// -/// This macro generates code to extract each field of the struct from HTTP -/// request head, making it easy to create composite extractors that combine -/// multiple data sources from an incoming request. -/// -/// The macro works by calling [`FromRequestHead::from_request_head`] on each -/// field's type, allowing you to compose extractors seamlessly. All fields must -/// implement the [`FromRequestHead`] trait for the derivation to work. -/// -/// # Requirements -/// -/// - The target struct must have all fields implement [`FromRequestHead`] -/// - Works with named fields, unnamed fields (tuple structs), and unit structs -/// - The struct must be accessible where the macro is used -/// -/// # Examples -/// -/// ## Named Fields -/// -/// ```no_run -/// use cot::request::extractors::{Path, StaticFiles, UrlQuery}; -/// use cot::router::Urls; -/// use cot_macros::FromRequestHead; -/// use serde::Deserialize; -/// -/// #[derive(Debug, FromRequestHead)] -/// pub struct BaseContext { -/// urls: Urls, -/// static_files: StaticFiles, -/// } -/// ``` -pub use cot_macros::FromRequestHead; - -use crate::error::error_impl::impl_into_cot_error; - #[cfg(test)] mod tests { - use serde::Deserialize; + use cot_core::Method; + use cot_core::html::Html; use super::*; - use crate::html::Html; - use crate::request::extractors::{FromRequest, Json, Path, UrlQuery}; - use crate::router::{Route, Router, Urls}; + use crate::request::extractors::FromRequest; + use crate::reverse; + use crate::router::{Route, Router}; use crate::test::TestRequestBuilder; - use crate::{Body, reverse}; - - #[cfg(feature = "json")] - #[cot::test] - async fn json() { - let request = http::Request::builder() - .method(http::Method::POST) - .header(http::header::CONTENT_TYPE, cot::headers::JSON_CONTENT_TYPE) - .body(Body::fixed(r#"{"hello":"world"}"#)) - .unwrap(); - - let (head, body) = request.into_parts(); - let Json(data): Json = Json::from_request(&head, body).await.unwrap(); - assert_eq!(data, serde_json::json!({"hello": "world"})); - } - - #[cfg(feature = "json")] - #[cot::test] - async fn json_empty() { - #[derive(Debug, Deserialize, PartialEq, Eq)] - struct TestData {} - - let request = http::Request::builder() - .method(http::Method::POST) - .header(http::header::CONTENT_TYPE, cot::headers::JSON_CONTENT_TYPE) - .body(Body::fixed("{}")) - .unwrap(); - - let (head, body) = request.into_parts(); - let Json(data): Json = Json::from_request(&head, body).await.unwrap(); - assert_eq!(data, TestData {}); - } - - #[cfg(feature = "json")] - #[cot::test] - async fn json_struct() { - #[derive(Debug, Deserialize, PartialEq, Eq)] - struct TestDataInner { - hello: String, - } - - #[derive(Debug, Deserialize, PartialEq, Eq)] - struct TestData { - inner: TestDataInner, - } - - let request = http::Request::builder() - .method(http::Method::POST) - .header(http::header::CONTENT_TYPE, cot::headers::JSON_CONTENT_TYPE) - .body(Body::fixed(r#"{"inner":{"hello":"world"}}"#)) - .unwrap(); - - let (head, body) = request.into_parts(); - let Json(data): Json = Json::from_request(&head, body).await.unwrap(); - assert_eq!( - data, - TestData { - inner: TestDataInner { - hello: "world".to_string(), - } - } - ); - } #[cot::test] - async fn path_extraction() { - #[derive(Deserialize, Debug, PartialEq)] - struct TestParams { - id: i32, - name: String, + async fn urls_extraction() { + async fn handler() -> Html { + Html::new("") } - 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 router = Router::with_urls([Route::with_handler_and_name( + "/test/", + handler, + "test_route", + )]); - let (mut head, _body) = Request::new(Body::empty()).into_parts(); - head.uri = "https://example.com/?page=2&filter=active".parse().unwrap(); + let mut request = TestRequestBuilder::get("/test/").router(router).build(); - let UrlQuery(query): UrlQuery = - UrlQuery::from_request_head(&head).await.unwrap(); + let urls: Urls = request.extract_from_head().await.unwrap(); - assert_eq!(query.page, 2); - assert_eq!(query.filter, "active"); + assert!(reverse!(urls, "test_route").is_ok()); } #[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(_))); - } + async fn method_extraction() { + let mut request = TestRequestBuilder::get("/test/").build(); - #[cfg(feature = "json")] - #[cot::test] - async fn json_invalid_content_type() { - let request = http::Request::builder() - .method(http::Method::POST) - .header(http::header::CONTENT_TYPE, "text/plain") - .body(Body::fixed(r#"{"hello":"world"}"#)) - .unwrap(); + let method: Method = request.extract_from_head().await.unwrap(); - let (head, body) = request.into_parts(); - let result = Json::::from_request(&head, body).await; - assert!(result.is_err()); + assert_eq!(method, Method::GET); } - #[cot::test] async fn request_form() { #[derive(Debug, PartialEq, Eq, Form)] @@ -735,34 +351,6 @@ mod tests { ); } - #[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] #[cfg_attr( diff --git a/cot/src/router.rs b/cot/src/router.rs index 3c568af5..9553fd24 100644 --- a/cot/src/router.rs +++ b/cot/src/router.rs @@ -27,13 +27,14 @@ use std::pin::Pin; use std::sync::Arc; use std::task::{Context, Poll}; +use cot_core::error::impl_into_cot_error; +use cot_core::handler::{BoxRequestHandler, RequestHandler, into_box_request_handler}; +use cot_core::request::{AppName, RouteName}; 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::request::{PathParams, Request, RequestExt, RequestHead}; use crate::response::Response; 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 c2c55ac9..7c03361c 100644 --- a/cot/src/router/method.rs +++ b/cot/src/router/method.rs @@ -5,8 +5,9 @@ pub mod openapi; use std::fmt::{Debug, Formatter}; +use cot_core::handler::{BoxRequestHandler, into_box_request_handler}; + use crate::error::MethodNotAllowed; -use crate::handler::{BoxRequestHandler, into_box_request_handler}; use crate::request::Request; use crate::response::Response; use crate::{Method, RequestHandler}; diff --git a/cot/src/router/path.rs b/cot/src/router/path.rs index 3b2e35d1..22639b39 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::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/session/store.rs b/cot/src/session/store.rs index f68ea7ab..57014b51 100644 --- a/cot/src/session/store.rs +++ b/cot/src/session/store.rs @@ -26,12 +26,13 @@ pub(crate) const MAX_COLLISION_RETRIES: u32 = 32; pub(crate) const ERROR_PREFIX: &str = "session store:"; /// A wrapper that provides a concrete type for -/// [`tower_session::SessionManagerLayer`] while delegating to a boxed -/// [`SessionStore`] trait object. +/// [`SessionManagerLayer`](tower_sessions::SessionManagerLayer) while +/// delegating to a boxed [`SessionStore`] trait +/// object. /// /// This enables runtime selection of session store implementations, as -/// [`tower_sessions::SessionManagerLayer`] requires a concrete type rather than -/// a boxed trait object. +/// [`SessionManagerLayer`](tower_sessions::SessionManagerLayer) requires a +/// concrete type rather than a boxed trait object. /// /// # Examples /// diff --git a/cot/src/static_files.rs b/cot/src/static_files.rs index 7e7a8d96..85bab5c5 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::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/cot/src/test.rs b/cot/src/test.rs index 20294b9a..f1c60133 100644 --- a/cot/src/test.rs +++ b/cot/src/test.rs @@ -10,8 +10,10 @@ use async_trait::async_trait; #[cfg(feature = "cache")] use cot::config::CacheUrl; #[cfg(feature = "redis")] +use cot_core::error::impl_into_cot_error; +use cot_core::handler::BoxedHandler; +#[cfg(feature = "redis")] use deadpool_redis::Connection; -use derive_more::Debug; #[cfg(feature = "redis")] use redis::AsyncCommands; use tokio::net::TcpListener; @@ -41,9 +43,6 @@ use crate::db::migrations::{ use crate::email::Email; #[cfg(feature = "email")] use crate::email::transport::console::Console; -#[cfg(feature = "redis")] -use crate::error::error_impl::impl_into_cot_error; -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; @@ -243,7 +242,7 @@ pub struct TestRequestBuilder { } /// A wrapper over an auth backend that is cloneable. -#[derive(Debug, Clone)] +#[derive(derive_more::Debug, Clone)] struct AuthBackendWrapper { #[debug("..")] inner: Arc,