Skip to content
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 34 additions & 9 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ members = [
"cot-cli",
"cot-codegen",
"cot-macros",
"cot-core",
# Examples
"examples/admin",
"examples/custom-error-pages",
Expand Down Expand Up @@ -76,6 +77,7 @@ clap-verbosity-flag = { version = "3", default-features = false }
clap_complete = "4"
clap_mangen = "0.2.31"
cot = { version = "0.4.0", path = "cot" }
cot_core = { version = "0.4.0", path = "cot-core" }
cot_codegen = { version = "0.4.0", path = "cot-codegen" }
cot_macros = { version = "0.4.0", path = "cot-macros" }
criterion = "0.8"
Expand Down
49 changes: 49 additions & 0 deletions cot-core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
[package]
name = "cot_core"
version = "0.4.0"
description = "The Rust web framework for lazy developers - framework core."
categories = ["web-programming", "web-programming::http-server", "network-programming"]
edition.workspace = true
rust-version.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
keywords.workspace = true
readme.workspace = true
authors.workspace = true

[lints]
workspace = true

[dependencies]
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"] }
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cot-core crate has a circular dev-dependency on the main cot crate. This creates a circular dependency structure where cot depends on cot_core, and cot_core has a dev-dependency on cot for tests. While this is technically allowed for dev-dependencies in Rust, it can complicate the build process and may cause issues with certain tools or workflows. Consider whether the tests in cot-core can be restructured to avoid depending on the main cot crate, or whether they can be moved to the main cot crate's test suite instead.

Suggested change
cot = { workspace = true, features = ["test"] }

Copilot uses AI. Check for mistakes.
futures.workspace = true
tokio.workspace = true

[features]
default = []
json = []
18 changes: 10 additions & 8 deletions cot/src/body.rs → cot-core/src/body.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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.
Expand All @@ -28,10 +33,10 @@ use crate::{Error, Result};
/// ```
#[derive(Debug)]
pub struct Body {
pub(crate) inner: BodyInner,
pub inner: BodyInner,
}

pub(crate) enum BodyInner {
pub enum BodyInner {
Fixed(Bytes),
Streaming(SyncWrapper<Pin<Box<dyn Stream<Item = Result<Bytes>> + Send>>>),
Axum(SyncWrapper<axum::body::Body>),
Expand Down Expand Up @@ -166,12 +171,12 @@ impl Body {
}

#[must_use]
pub(crate) fn axum(inner: axum::body::Body) -> Self {
pub fn axum(inner: axum::body::Body) -> Self {
Self::new(BodyInner::Axum(SyncWrapper::new(inner)))
}

#[must_use]
pub(crate) fn wrapper(inner: BoxBody<Bytes, Error>) -> Self {
pub fn wrapper(inner: BoxBody<Bytes, Error>) -> Self {
Self::new(BodyInner::Wrapper(inner))
}
}
Expand Down Expand Up @@ -261,9 +266,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;

Expand Down
16 changes: 16 additions & 0 deletions cot-core/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//! 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 not_found;
mod uncaught_panic;

pub use error_impl::{Error, impl_into_cot_error};
pub use method_not_allowed::MethodNotAllowed;
pub use not_found::{Kind as NotFoundKind, NotFound};
pub use uncaught_panic::UncaughtPanic;
13 changes: 7 additions & 6 deletions cot/src/error/backtrace.rs → cot-core/src/error/backtrace.rs
Original file line number Diff line number Diff line change
@@ -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| {
Expand All @@ -21,19 +22,19 @@ pub(crate) fn __cot_create_backtrace() -> Backtrace {
}

#[derive(Debug, Clone)]
pub(crate) struct Backtrace {
pub struct Backtrace {
frames: Vec<StackFrame>,
}

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<String>,
filename: Option<String>,
lineno: Option<u32>,
Expand All @@ -42,15 +43,15 @@ 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("<unknown>")
.to_string()
}

#[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();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ impl Error {
}

#[must_use]
pub(crate) fn backtrace(&self) -> &CotBacktrace {
pub fn backtrace(&self) -> &CotBacktrace {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an implementation detail and cannot be exposed to users. The CotBacktrace struct is (rightly) not even reexported in Cot.

Copy link
Member Author

@seqre seqre Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's problematic, it has to be public so that the error page can use it to show backtrace properly. The easiest solution would be to pull that to cot, but then we would have to pull Error as well and it's impossible as it's used in cot_core. Alternatively, we could pull ErrorPage to cot_core, but it's not really core and it's specific to cot. Thoughts?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the error struct should be kept in the core crate. I can see several approaches here:

  1. #[doc(hidden)] - the easiest way. Perhaps it's good enough (I think some popular crates such as tokio and serde do use it), but it doesn't really stop the users from calling the method.
  2. Defining an extension trait (similar to ResponseExt, for example), and implementing it for the Error type. The extension trait is then public in cot-core, but not reexported in cot. This way the method will be invisible to the normal users, but still accessible if you use cot-core directly.
  3. Sort of similar to the one above: defining an access token. The code explains it all:
mod private {
    pub struct Token;
}

pub struct MyStruct;

impl MyStruct {
    pub fn restricted_method(&self, _: private::Token) {
        println!("Restricted!");
    }
}

#[doc(hidden)]
pub fn get_token() -> private::Token {
    private::Token
}

I'll let you pick the favorite one for the job.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went with option 1. I want to keep this PR as change-light as possible and if it's used like that by other crates, I suppose it's good enough. If it's ever become a problem, we can change it then.

&self.repr.backtrace
}

Expand Down Expand Up @@ -319,6 +319,7 @@ impl From<Error> for askama::Error {
}
}

#[macro_export]
macro_rules! impl_into_cot_error {
($error_ty:ty) => {
impl From<$error_ty> for $crate::Error {
Expand All @@ -335,7 +336,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}")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ impl NotFound {
}

#[must_use]
pub(crate) fn router() -> Self {
pub fn router() -> Self {
Self::with_kind(Kind::FromRouter)
}

Expand Down
File renamed without changes.
25 changes: 17 additions & 8 deletions cot/src/handler.rs → cot-core/src/handler.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -48,14 +54,14 @@ pub trait RequestHandler<T = ()> {
fn handle(&self, request: Request) -> impl Future<Output = Result<Response>> + Send;
}

pub(crate) trait BoxRequestHandler {
pub trait BoxRequestHandler {
fn handle(
&self,
request: Request,
) -> Pin<Box<dyn Future<Output = Result<Response>> + Send + '_>>;
}

pub(crate) fn into_box_request_handler<T, H: RequestHandler<T> + Send + Sync>(
pub fn into_box_request_handler<T, H: RequestHandler<T> + Send + Sync>(
handler: H,
) -> impl BoxRequestHandler {
struct Inner<T, H>(H, PhantomData<fn() -> T>);
Expand Down Expand Up @@ -142,6 +148,7 @@ macro_rules! impl_request_handler_from_request {
};
}

#[macro_export]
macro_rules! handle_all_parameters {
($name:ident) => {
$name!();
Expand Down Expand Up @@ -227,19 +234,21 @@ 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);

#[rustfmt::skip] // `wrap_comments` breaks local links
/// A wrapper around a handler that's used in
/// [`Bootstrapper`](cot::Bootstrapper).
/// [`Bootstrapper`](../../cot/project/struct.Bootstrapper.html).
///
/// 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)
/// [`Bootstrapper::finish()`](../../cot/project/struct.Bootstrapper.html#method.finish).
/// Typically, you don't need to interact with this type directly, except
/// for creating it in
/// [`Project::middlewares()`](../../cot/project/trait.Project.html#method.middlewares) through the
/// [`RootHandlerBuilder::build()`](../../cot/project/struct.RootHandlerBuilder.html#method.build)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These links (and probably most others) don't work when you look at the docs from the cot crate. Is there really no way to avoid specifying exact paths? How do other projects handle this? I can see some discussion on this, e.g. rust-lang/docs.rs#1588

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be a skill issue on my side, but whatever I tried, I could not make it work properly. The issue I think is that cot is not a dependency of cot_core, so rustdoc has issues with linking to it. I attempted using unstable rustdoc-map feature with doc.extern-map.registries, but it did not seem to work. We could always just put there normal crates.io links to latest, but I don't like it. Could you assist?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about we move these docs to the main crate? If we mention high-level concepts in this doc, I think it's okay to move the docs to the high-level crate.

The problematic one is the new_redirect method inside the ResponseExt trait. I'm honestly thinking that this should be a separate struct (probably defined in cot) rather than a method in an extension trait, as it would be more consistent with the rest of our APIs now, and also would make the core types less "special". Basically instead of Response::new_redirect("link") we should have Redirect::new("link"). What do you think?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved them.

Here's redirect PR: #451

/// method.
///
/// # Examples
Expand Down
11 changes: 11 additions & 0 deletions cot-core/src/headers.rs
Original file line number Diff line number Diff line change
@@ -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";
File renamed without changes.
File renamed without changes.
Loading
Loading