diff --git a/Cargo.lock b/Cargo.lock index a6a6877..242fc5b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -250,9 +250,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be" dependencies = [ "serde", ] @@ -341,9 +341,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.24" +version = "4.5.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9560b07a799281c7e0958b9296854d6fafd4c5f31444a7e5bb1ad6dde5ccf1bd" +checksum = "a8eb5e908ef3a6efbe1ed62520fb7287959888c88485abe072543190ecc66783" dependencies = [ "clap_builder", "clap_derive", @@ -361,9 +361,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.24" +version = "4.5.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874e0dd3eb68bf99058751ac9712f622e61e6f393a94f7128fa26e3f02f5c7cd" +checksum = "96b01801b5fc6a0a232407abc821660c9c6d25a1cafc0d4f85f29fb8d9afc121" dependencies = [ "anstream", "anstyle", @@ -479,10 +479,11 @@ dependencies = [ "sqlx", "subtle", "sync_wrapper", - "thiserror 2.0.9", + "thiserror 2.0.10", "time", "tokio", "tower", + "tower-livereload", "tower-sessions", "tracing", ] @@ -829,6 +830,7 @@ name = "example-admin" version = "0.1.0" dependencies = [ "cot", + "rinja", ] [[package]] @@ -2289,7 +2291,7 @@ dependencies = [ "serde_json", "sha2 0.10.8", "smallvec", - "thiserror 2.0.9", + "thiserror 2.0.10", "tokio", "tokio-stream", "tracing", @@ -2373,7 +2375,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.9", + "thiserror 2.0.10", "tracing", "whoami", ] @@ -2411,7 +2413,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.9", + "thiserror 2.0.10", "tracing", "whoami", ] @@ -2471,9 +2473,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.95" +version = "2.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a" +checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" dependencies = [ "proc-macro2", "quote", @@ -2543,11 +2545,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.9" +version = "2.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc" +checksum = "a3ac7f54ca534db81081ef1c1e7f6ea8a3ef428d2fc069097c079443d24124d3" dependencies = [ - "thiserror-impl 2.0.9", + "thiserror-impl 2.0.10", ] [[package]] @@ -2563,9 +2565,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.9" +version = "2.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4" +checksum = "9e9465d30713b56a37ede7185763c3492a91be2f5fa68d958c44e41ab9248beb" dependencies = [ "proc-macro2", "quote", @@ -2748,6 +2750,19 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" +[[package]] +name = "tower-livereload" +version = "0.9.6-wip" +source = "git+https://github.com/leotaku/tower-livereload.git?rev=106cc96f91b11a1eca6d3dfc86be4e766a90a415#106cc96f91b11a1eca6d3dfc86be4e766a90a415" +dependencies = [ + "bytes", + "http", + "http-body", + "pin-project-lite", + "tokio", + "tower", +] + [[package]] name = "tower-service" version = "0.3.3" diff --git a/Cargo.toml b/Cargo.toml index a6061a0..fce21b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,6 +76,8 @@ thiserror = "2" time = { version = "0.3.35", default-features = false } tokio = { version = "1.41", default-features = false } tower = "0.5.2" +# TODO switch back to the published version when https://github.com/leotaku/tower-livereload/pull/24 is released +tower-livereload = { git = "https://github.com/leotaku/tower-livereload.git", rev = "106cc96f91b11a1eca6d3dfc86be4e766a90a415" } tower-sessions = { version = "0.13", default-features = false } tracing = { version = "0.1", default-features = false } tracing-subscriber = "0.3" diff --git a/LICENSE-MIT b/LICENSE-MIT index d90e3e6..3f98ee6 100644 --- a/LICENSE-MIT +++ b/LICENSE-MIT @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 Cot Authors +Copyright (c) 2024-2025 Cot contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/cot/Cargo.toml b/cot/Cargo.toml index 305939a..b6e4be9 100644 --- a/cot/Cargo.toml +++ b/cot/Cargo.toml @@ -43,6 +43,7 @@ thiserror.workspace = true time.workspace = true tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } tower = { workspace = true, features = ["util"] } +tower-livereload = { workspace = true, optional = true } tower-sessions = { workspace = true, features = ["memory-store"] } tracing.workspace = true @@ -67,9 +68,11 @@ ignored = [ [features] default = ["sqlite", "postgres", "mysql", "json"] +full = ["default", "fake", "live-reload"] fake = ["dep:fake"] db = [] sqlite = ["db", "sea-query/backend-sqlite", "sea-query-binder/sqlx-sqlite", "sqlx/sqlite"] postgres = ["db", "sea-query/backend-postgres", "sea-query-binder/sqlx-postgres", "sqlx/postgres"] mysql = ["db", "sea-query/backend-mysql", "sea-query-binder/sqlx-mysql", "sqlx/mysql"] json = ["serde_json"] +live-reload = ["dep:tower-livereload"] diff --git a/cot/src/admin.rs b/cot/src/admin.rs index bcc9f4c..e624021 100644 --- a/cot/src/admin.rs +++ b/cot/src/admin.rs @@ -18,7 +18,7 @@ use crate::forms::{ use crate::request::{Request, RequestExt}; use crate::response::{Response, ResponseExt}; use crate::router::Router; -use crate::{reverse, static_files, Body, CotApp, Render, StatusCode}; +use crate::{reverse_redirect, static_files, Body, CotApp, Render, StatusCode}; #[derive(Debug, Form)] struct LoginForm { @@ -60,7 +60,7 @@ async fn index(mut request: Request) -> cot::Result { Body::fixed(template.render()?), )) } else { - Ok(reverse!(request, "login")) + Ok(reverse_redirect!(request, "login")) } } @@ -72,7 +72,7 @@ async fn login(mut request: Request) -> cot::Result { match login_form { FormResult::Ok(login_form) => { if authenticate(&mut request, login_form).await? { - return Ok(reverse!(request, "index")); + return Ok(reverse_redirect!(request, "index")); } let mut context = LoginForm::build_context(&mut request).await?; @@ -139,7 +139,7 @@ async fn view_model(mut request: Request) -> cot::Result { Body::fixed(template.render()?), )) } else { - Ok(reverse!(request, "login")) + Ok(reverse_redirect!(request, "login")) } } diff --git a/cot/src/error.rs b/cot/src/error.rs index 69e3a8c..2c3173a 100644 --- a/cot/src/error.rs +++ b/cot/src/error.rs @@ -26,6 +26,14 @@ impl Error { } } + #[must_use] + pub fn custom(error: E) -> Self + where + E: Into>, + { + Self::new(ErrorRepr::Custom(error.into())) + } + #[must_use] pub(crate) fn backtrace(&self) -> &CotBacktrace { &self.backtrace @@ -78,13 +86,16 @@ impl_error_from_repr!(serde_json::Error); #[derive(Debug, Error)] #[non_exhaustive] pub(crate) enum ErrorRepr { + /// A custom user error occurred. + #[error("{0}")] + Custom(#[source] Box), /// An error occurred while trying to start the server. #[error("Could not start server: {source}")] StartServer { source: std::io::Error }, /// An error occurred while trying to read the request body. #[error("Could not retrieve request body: {source}")] ReadRequestBody { - #[from] + #[source] source: Box, }, /// The request body had an invalid `Content-Type` header. @@ -120,6 +131,12 @@ pub(crate) enum ErrorRepr { #[error("JSON error: {0}")] #[cfg(feature = "json")] JsonError(#[from] serde_json::Error), + /// An error occurred inside a middleware-wrapped view. + #[error("{source}")] + MiddlewareWrapped { + #[source] + source: Box, + }, } #[cfg(test)] diff --git a/cot/src/error_page.rs b/cot/src/error_page.rs index d41dd7c..8a929d2 100644 --- a/cot/src/error_page.rs +++ b/cot/src/error_page.rs @@ -301,7 +301,7 @@ fn build_cot_failure_page() -> axum::response::Response { axum::response::Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) .body(axum::body::Body::from(FAILURE_PAGE)) - .expect("Building the Cot failure page should not fail") + .expect("Building the Cot failure page should never fail") } thread_local! { @@ -310,7 +310,6 @@ thread_local! { } pub(super) fn error_page_panic_hook(info: &PanicHookInfo<'_>) { - // TODO print out the panic as well let location = info.location().map(|location| format!("{location}")); PANIC_LOCATION.replace(location); diff --git a/cot/src/lib.rs b/cot/src/lib.rs index e04b2e7..8d22d4e 100644 --- a/cot/src/lib.rs +++ b/cot/src/lib.rs @@ -74,6 +74,7 @@ use std::task::{Context, Poll}; use async_trait::async_trait; use axum::handler::HandlerWithoutStateExt; +pub use bytes; use bytes::Bytes; pub use cot_macros::main; use derive_more::{Debug, Deref, Display, From}; @@ -82,11 +83,12 @@ use futures_core::Stream; use futures_util::FutureExt; use http::request::Parts; use http_body::{Frame, SizeHint}; +use http_body_util::combinators::BoxBody; use request::Request; use router::{Route, Router}; use sync_wrapper::SyncWrapper; use tower::util::BoxCloneService; -use tower::Service; +use tower::{Layer, Service}; use tracing::info; use crate::admin::AdminModelManager; @@ -99,6 +101,7 @@ use crate::db::migrations::{DynMigration, MigrationEngine}; use crate::db::Database; use crate::error::ErrorRepr; use crate::error_page::{CotDiagnostics, ErrorPageTrigger}; +use crate::middleware::{IntoCotError, IntoCotErrorLayer, IntoCotResponse, IntoCotResponseLayer}; use crate::response::Response; use crate::router::RouterService; @@ -169,6 +172,7 @@ pub trait CotApp: Send + Sync { vec![] } + /// Returns a list of static files that the app serves. fn static_files(&self) -> Vec<(String, Bytes)> { vec![] } @@ -197,6 +201,7 @@ enum BodyInner { Fixed(Bytes), Streaming(SyncWrapper> + Send>>>), Axum(SyncWrapper), + Wrapper(BoxBody), } impl Debug for BodyInner { @@ -205,6 +210,7 @@ impl Debug for BodyInner { Self::Fixed(data) => f.debug_tuple("Fixed").field(data).finish(), Self::Streaming(_) => f.debug_tuple("Streaming").field(&"...").finish(), Self::Axum(axum_body) => f.debug_tuple("Axum").field(axum_body).finish(), + Self::Wrapper(_) => f.debug_tuple("Wrapper").field(&"...").finish(), } } } @@ -280,6 +286,11 @@ impl Body { fn axum(inner: axum::body::Body) -> Self { Self::new(BodyInner::Axum(SyncWrapper::new(inner))) } + + #[must_use] + pub(crate) fn wrapper(inner: BoxBody) -> Self { + Self::new(BodyInner::Wrapper(inner)) + } } impl Default for Body { @@ -322,6 +333,14 @@ impl http_body::Body for Body { .into() }) } + BodyInner::Wrapper(ref mut http_body) => { + Pin::new(http_body).poll_frame(cx).map_err(|error| { + ErrorRepr::ReadRequestBody { + source: Box::new(error), + } + .into() + }) + } } } @@ -329,6 +348,7 @@ impl http_body::Body for Body { match &self.inner { BodyInner::Fixed(data) => data.is_empty(), BodyInner::Streaming(_) | BodyInner::Axum(_) => false, + BodyInner::Wrapper(http_body) => http_body.is_end_stream(), } } @@ -336,6 +356,7 @@ impl http_body::Body for Body { match &self.inner { BodyInner::Fixed(data) => SizeHint::with_exact(data.len() as u64), BodyInner::Streaming(_) | BodyInner::Axum(_) => SizeHint::new(), + BodyInner::Wrapper(http_body) => http_body.size_hint(), } } } @@ -458,17 +479,23 @@ impl CotProjectBuilder { } #[must_use] - pub fn middleware>( + pub fn middleware( self, middleware: M, - ) -> CotProjectBuilder { + ) -> CotProjectBuilder>> + where + M: Layer, + { self.into_builder_with_service().middleware(middleware) } #[must_use] - pub fn middleware_with_context(self, get_middleware: F) -> CotProjectBuilder + pub fn middleware_with_context( + self, + get_middleware: F, + ) -> CotProjectBuilder>> where - M: tower::Layer, + M: Layer, F: FnOnce(&AppContext) -> M, { self.into_builder_with_service() @@ -499,18 +526,33 @@ where S::Future: Send, { #[must_use] - pub fn middleware>(self, middleware: M) -> CotProjectBuilder { + pub fn middleware( + self, + middleware: M, + ) -> CotProjectBuilder>::Service>>> + where + M: Layer, + { + let layer = ( + IntoCotErrorLayer::new(), + IntoCotResponseLayer::new(), + middleware, + ); + CotProjectBuilder { context: self.context, urls: vec![], - handler: middleware.layer(self.handler), + handler: layer.layer(self.handler), } } #[must_use] - pub fn middleware_with_context(self, get_middleware: F) -> CotProjectBuilder + pub fn middleware_with_context( + self, + get_middleware: F, + ) -> CotProjectBuilder>::Service>>> where - M: tower::Layer, + M: Layer, F: FnOnce(&AppContext) -> M, { let middleware = get_middleware(&self.context); @@ -659,7 +701,12 @@ pub async fn run_at(project: CotProject, listener: tokio::net::TcpListener) -> R .map_err(|e| ErrorRepr::StartServer { source: e })? ); if config::REGISTER_PANIC_HOOK { - std::panic::set_hook(Box::new(error_page::error_page_panic_hook)); + let current_hook = std::panic::take_hook(); + let new_hook = move |hook_info: &std::panic::PanicHookInfo| { + current_hook(hook_info); + error_page::error_page_panic_hook(hook_info); + }; + std::panic::set_hook(Box::new(new_hook)); } axum::serve(listener, handler.into_make_service()) .await diff --git a/cot/src/middleware.rs b/cot/src/middleware.rs index 1054324..892e466 100644 --- a/cot/src/middleware.rs +++ b/cot/src/middleware.rs @@ -4,8 +4,136 @@ //! are used to add functionality to the request/response cycle, such as //! session management, adding security headers, and more. +use std::task::{Context, Poll}; + +use bytes::Bytes; +use futures_util::TryFutureExt; +use http_body_util::combinators::BoxBody; +use http_body_util::BodyExt; +use tower::Service; use tower_sessions::{MemoryStore, SessionManagerLayer}; +use crate::error::ErrorRepr; +use crate::request::Request; +use crate::response::Response; +use crate::{Body, Error}; + +#[derive(Debug, Copy, Clone)] +pub struct IntoCotResponseLayer; + +impl IntoCotResponseLayer { + #[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 } + } +} + +#[derive(Debug, Clone)] +pub struct IntoCotResponse { + inner: S, +} + +impl Service for IntoCotResponse +where + S: Service>, + B: 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 + B: 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 { + #[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 } + } +} + +#[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, +{ + Error::new(ErrorRepr::MiddlewareWrapped { + source: Box::new(error), + }) +} + #[derive(Debug, Copy, Clone)] pub struct SessionMiddleware; @@ -32,4 +160,32 @@ impl tower::Layer for SessionMiddleware { } } +#[cfg(feature = "live-reload")] +#[derive(Debug, Clone)] +pub struct LiveReloadMiddleware(tower_livereload::LiveReloadLayer); + +#[cfg(feature = "live-reload")] +impl LiveReloadMiddleware { + #[must_use] + pub fn new() -> Self { + Self(tower_livereload::LiveReloadLayer::new()) + } +} + +#[cfg(feature = "live-reload")] +impl Default for LiveReloadMiddleware { + fn default() -> Self { + Self::new() + } +} + +#[cfg(feature = "live-reload")] +impl tower::Layer for LiveReloadMiddleware { + type Service = >::Service; + + fn layer(&self, inner: S) -> Self::Service { + self.0.layer(inner) + } +} + // TODO: add Cot ORM-based session store diff --git a/cot/src/private.rs b/cot/src/private.rs index b2b8780..0b88173 100644 --- a/cot/src/private.rs +++ b/cot/src/private.rs @@ -11,4 +11,5 @@ pub use bytes::Bytes; pub use tokio; // used in the CLI +#[cfg(feature = "db")] pub use crate::utils::graph::apply_permutation; diff --git a/cot/src/request.rs b/cot/src/request.rs index 0311b65..25bbb78 100644 --- a/cot/src/request.rs +++ b/cot/src/request.rs @@ -54,6 +54,9 @@ pub trait RequestExt: private::Sealed { #[must_use] fn router(&self) -> &Router; + #[must_use] + fn route_name(&self) -> Option<&str>; + #[must_use] fn path_params(&self) -> &PathParams; @@ -139,6 +142,12 @@ impl RequestExt for Request { self.context().router() } + fn route_name(&self) -> Option<&str> { + self.extensions() + .get::() + .map(|RouteName(name)| name.as_str()) + } + fn path_params(&self) -> &PathParams { self.extensions() .get::() @@ -213,6 +222,10 @@ impl RequestExt for Request { } } +#[repr(transparent)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct RouteName(pub(crate) String); + #[derive(Debug, Clone)] pub struct PathParams { params: IndexMap, diff --git a/cot/src/router.rs b/cot/src/router.rs index a3599b2..8bcc5b0 100644 --- a/cot/src/router.rs +++ b/cot/src/router.rs @@ -9,7 +9,7 @@ use std::task::{Context, Poll}; use axum::http::StatusCode; use bytes::Bytes; -use cot::request::PathParams; +use cot::request::{PathParams, RouteName}; use derive_more::Debug; use tracing::debug; @@ -25,7 +25,7 @@ pub mod path; #[derive(Clone, Debug)] pub struct Router { urls: Vec, - names: HashMap>, + names: HashMap>, } impl Router { @@ -57,6 +57,9 @@ impl Router { path_params.insert(key.clone(), value.clone()); } request.extensions_mut().insert(path_params); + if let Some(name) = result.name { + request.extensions_mut().insert(name); + } result.handler.handle(request).await } else { debug!("Not found: {}", request_path); @@ -74,6 +77,7 @@ impl Router { if matches_fully { return Some(HandlerFound { handler: &**handler, + name: route.name.clone(), params: Self::matches_to_path_params(&matches, Vec::new()), }); } @@ -82,6 +86,7 @@ impl Router { if let Some(result) = router.get_handler(matches.remaining_path) { return Some(HandlerFound { handler: result.handler, + name: result.name, params: Self::matches_to_path_params(&matches, result.params), }); } @@ -121,8 +126,8 @@ impl Router { /// Get a URL for a view by name. /// /// Instead of using this method directly, consider using the - /// [`reverse!`](crate::reverse) macro which provides much more - /// ergonomic way to call this. + /// [`reverse!`](crate::reverse) macro which provides much more ergonomic + /// way to call this. /// /// # Errors /// @@ -147,7 +152,10 @@ impl Router { /// This method returns an error if the URL cannot be generated because of /// missing parameters. pub fn reverse_option(&self, name: &str, params: &ReverseParamMap) -> Result> { - let url = self.names.get(name).map(|matcher| matcher.reverse(params)); + let url = self + .names + .get(&RouteName(String::from(name))) + .map(|matcher| matcher.reverse(params)); if let Some(url) = url { return Ok(Some(url.map_err(ErrorRepr::from)?)); } @@ -185,6 +193,7 @@ impl Default for Router { struct HandlerFound<'a> { #[debug("handler(...)")] handler: &'a (dyn RequestHandler + Send + Sync), + name: Option, params: Vec<(String, String)>, } @@ -219,7 +228,7 @@ impl tower::Service for RouterService { pub struct Route { url: Arc, view: RouteInner, - name: Option, + name: Option, } impl Route { @@ -241,7 +250,7 @@ impl Route { Self { url: Arc::new(PathMatcher::new(url)), view: RouteInner::Handler(Arc::new(view)), - name: Some(name.into()), + name: Some(RouteName(name.into())), } } @@ -261,7 +270,7 @@ impl Route { #[must_use] pub fn name(&self) -> Option<&str> { - self.name.as_deref() + self.name.as_ref().map(|name| name.0.as_str()) } #[must_use] @@ -319,14 +328,14 @@ impl Debug for RouteInner { /// use cot::request::Request; /// use cot::response::{Response, ResponseExt}; /// use cot::router::{Route, Router}; -/// use cot::{reverse_str, Body, StatusCode}; +/// use cot::{reverse, Body, StatusCode}; /// /// async fn home(request: Request) -> cot::Result { /// Ok(Response::new_html( /// StatusCode::OK, /// Body::fixed(format!( /// "Hello! The URL for this view is: {}", -/// reverse_str!(request, "home") +/// reverse!(request, "home") /// )), /// )) /// } @@ -334,12 +343,12 @@ impl Debug for RouteInner { /// let router = Router::with_urls([Route::with_handler_and_name("/", home, "home")]); /// ``` #[macro_export] -macro_rules! reverse_str { - ($request:expr, $view_name:literal $(, $($key:expr => $value:expr),*)?) => {{ +macro_rules! reverse { + ($request:expr, $view_name:literal $(, $($key:ident = $value:expr),*)?) => {{ use $crate::request::RequestExt; $request .router() - .reverse($view_name, &$crate::reverse_param_map!($( $($key => $value),* )?))? + .reverse($view_name, &$crate::reverse_param_map!($( $($key = $value),* )?))? }}; } @@ -347,27 +356,27 @@ macro_rules! reverse_str { /// a redirect. /// /// This macro is a shorthand for creating a response with a redirect to a URL -/// generated by the [`reverse_str!`] macro. +/// generated by the [`reverse!`] macro. /// /// # Examples /// /// ``` /// use cot::request::Request; /// use cot::response::Response; -/// use cot::reverse; +/// use cot::reverse_redirect; /// use cot::router::{Route, Router}; /// /// async fn infinite_loop(request: Request) -> cot::Result { -/// Ok(reverse!(request, "home")) +/// Ok(reverse_redirect!(request, "home")) /// } /// /// let router = Router::with_urls([Route::with_handler_and_name("/", infinite_loop, "home")]); /// ``` #[macro_export] -macro_rules! reverse { +macro_rules! reverse_redirect { ($request:expr, $view_name:literal $(, $($key:expr => $value:expr),*)?) => { <$crate::response::Response as $crate::response::ResponseExt>::new_redirect( - $crate::reverse_str!( + $crate::reverse!( $request, $view_name, $( $($key => $value),* )? @@ -493,7 +502,7 @@ mod tests { fn route_with_handler_and_name() { let route = Route::with_handler_and_name("/test", MockHandler, "test"); assert_eq!(route.url.to_string(), "/test"); - assert_eq!(route.name.as_deref(), Some("test")); + assert_eq!(route.name, Some(RouteName("test".to_string()))); } #[test] diff --git a/cot/src/router/path.rs b/cot/src/router/path.rs index 32e277c..eb466f9 100644 --- a/cot/src/router/path.rs +++ b/cot/src/router/path.rs @@ -178,10 +178,10 @@ impl ReverseParamMap { #[doc(hidden)] #[macro_export] macro_rules! reverse_param_map { - ($($key:expr => $value:expr),*) => {{ + ($($key:ident = $value:expr),*) => {{ #[allow(unused_mut)] let mut map = $crate::router::path::ReverseParamMap::new(); - $( map.insert($key, $value); )* + $( map.insert(stringify!($key), $value); )* map }}; } diff --git a/cot/src/utils.rs b/cot/src/utils.rs index 07ebd31..c16ca17 100644 --- a/cot/src/utils.rs +++ b/cot/src/utils.rs @@ -1 +1,2 @@ +#[cfg(feature = "db")] pub(crate) mod graph; diff --git a/cot/templates/admin/model_list.html b/cot/templates/admin/model_list.html index 3e40c92..fb02718 100644 --- a/cot/templates/admin/model_list.html +++ b/cot/templates/admin/model_list.html @@ -5,6 +5,6 @@ {% block content %} {% let request = request %} {% for model in model_managers %} - model.url_name()) }}">{{ model.name() }} + {{ model.name() }} {% endfor %} {% endblock %} diff --git a/examples/admin/Cargo.toml b/examples/admin/Cargo.toml index 071b240..8400256 100644 --- a/examples/admin/Cargo.toml +++ b/examples/admin/Cargo.toml @@ -7,3 +7,4 @@ edition = "2021" [dependencies] cot = { path = "../../cot" } +rinja = "0.3.5" diff --git a/examples/admin/src/main.rs b/examples/admin/src/main.rs index f8a2b3e..028e127 100644 --- a/examples/admin/src/main.rs +++ b/examples/admin/src/main.rs @@ -8,9 +8,19 @@ use cot::response::{Response, ResponseExt}; use cot::router::{Route, Router}; use cot::static_files::StaticFilesMiddleware; use cot::{AppContext, Body, CotApp, CotProject, StatusCode}; +use rinja::Template; -async fn hello(_request: Request) -> cot::Result { - Ok(Response::new_html(StatusCode::OK, Body::fixed("xd"))) +#[derive(Debug, Template)] +#[template(path = "index.html")] +struct IndexTemplate<'a> { + request: &'a Request, +} + +async fn index(request: Request) -> cot::Result { + let index_template = IndexTemplate { request: &request }; + let rendered = index_template.render()?; + + Ok(Response::new_html(StatusCode::OK, Body::fixed(rendered))) } struct HelloApp; @@ -32,7 +42,7 @@ impl CotApp for HelloApp { } fn router(&self) -> Router { - Router::with_urls([Route::with_handler("/", hello)]) + Router::with_urls([Route::with_handler("/", index)]) } } diff --git a/examples/admin/templates/index.html b/examples/admin/templates/index.html index 63d6992..5a35af4 100644 --- a/examples/admin/templates/index.html +++ b/examples/admin/templates/index.html @@ -5,10 +5,10 @@ - Sessions example + Admin Panel example

Hello!

-

Your name is: {{ name }}

+

Go to the admin panel.

diff --git a/examples/admin/templates/name.html b/examples/admin/templates/name.html index 150e9fe..b6f7509 100644 --- a/examples/admin/templates/name.html +++ b/examples/admin/templates/name.html @@ -9,7 +9,7 @@

Hello!

-
+
diff --git a/examples/sessions/src/main.rs b/examples/sessions/src/main.rs index 312c1f0..0a2368c 100644 --- a/examples/sessions/src/main.rs +++ b/examples/sessions/src/main.rs @@ -3,7 +3,7 @@ use cot::middleware::SessionMiddleware; use cot::request::{Request, RequestExt}; use cot::response::{Response, ResponseExt}; use cot::router::{Route, Router}; -use cot::{reverse, Body, CotApp, CotProject, StatusCode}; +use cot::{reverse_redirect, Body, CotApp, CotProject, StatusCode}; use rinja::Template; #[derive(Debug, Template)] @@ -33,7 +33,7 @@ async fn hello(request: Request) -> cot::Result { .expect("Invalid session value") .unwrap_or_default(); if name.is_empty() { - return Ok(reverse!(request, "name")); + return Ok(reverse_redirect!(request, "name")); } let template = IndexTemplate { @@ -56,7 +56,7 @@ async fn name(mut request: Request) -> cot::Result { .await .unwrap(); - return Ok(reverse!(request, "index")); + return Ok(reverse_redirect!(request, "index")); } let template = NameTemplate { request: &request }; diff --git a/examples/sessions/templates/name.html b/examples/sessions/templates/name.html index 150e9fe..b6f7509 100644 --- a/examples/sessions/templates/name.html +++ b/examples/sessions/templates/name.html @@ -9,7 +9,7 @@

Hello!

-
+
diff --git a/examples/todo-list/src/main.rs b/examples/todo-list/src/main.rs index 5ca9e4f..1326c73 100644 --- a/examples/todo-list/src/main.rs +++ b/examples/todo-list/src/main.rs @@ -7,7 +7,7 @@ use cot::forms::Form; use cot::request::{Request, RequestExt}; use cot::response::{Response, ResponseExt}; use cot::router::{Route, Router}; -use cot::{reverse, Body, CotApp, CotProject, StatusCode}; +use cot::{reverse_redirect, Body, CotApp, CotProject, StatusCode}; use rinja::Template; #[derive(Debug, Clone)] @@ -53,7 +53,7 @@ async fn add_todo(mut request: Request) -> cot::Result { .await?; } - Ok(reverse!(request, "index")) + Ok(reverse_redirect!(request, "index")) } async fn remove_todo(request: Request) -> cot::Result { @@ -69,7 +69,7 @@ async fn remove_todo(request: Request) -> cot::Result { .await?; } - Ok(reverse!(request, "index")) + Ok(reverse_redirect!(request, "index")) } struct TodoApp; diff --git a/examples/todo-list/templates/index.html b/examples/todo-list/templates/index.html index 4bb8ada..ced109b 100644 --- a/examples/todo-list/templates/index.html +++ b/examples/todo-list/templates/index.html @@ -9,7 +9,7 @@

TODO List

-
+
@@ -17,7 +17,7 @@

TODO List

{% for todo in todo_items %}
  • {% let todo_id = todo.id %} -
    todo_id) }}" method="post"> + {{ todo.title }}