From 8651b91ab2eb71587852cca5ead56d367584cb6c Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sun, 20 Sep 2026 14:33:43 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(minimal):=20Reimplementa?= =?UTF-8?q?=20`join!`=20y=20`join=5Fpair!`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Usa `[&str]::concat()` de la biblioteca estándar y elimina la dependencia de `concat-string`. Cada argumento se evalúa una sola vez y no se consume. --- Cargo.lock | 7 --- Cargo.toml | 1 - extensions/pagetop-admin/src/settings.rs | 5 +- .../src/theme/bs/dropdown/props.rs | 2 +- extensions/pagetop-htmx/src/hx.rs | 6 ++- .../component/admin/admin_password_form.rs | 4 +- .../src/component/admin/role_form.rs | 10 ++-- .../component/admin/role_permissions_form.rs | 8 +-- .../src/component/admin/role_table.rs | 12 ++--- .../src/component/admin/user_form.rs | 10 ++-- .../src/component/admin/user_roles_form.rs | 4 +- .../src/component/admin/user_table.rs | 9 ++-- .../pagetop-user/src/handlers/admin/roles.rs | 3 +- .../pagetop-user/src/handlers/admin/users.rs | 32 +++++------- extensions/pagetop-user/src/lib.rs | 10 ++++ extensions/pagetop-user/src/session.rs | 35 +++++++------ helpers/pagetop-minimal/Cargo.toml | 1 - helpers/pagetop-minimal/README.md | 5 -- helpers/pagetop-minimal/src/lib.rs | 49 ++++++++++++------- 19 files changed, 105 insertions(+), 108 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 30f588d2..dad698e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -396,12 +396,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "concat-string" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7439becb5fafc780b6f4de382b1a7a3e70234afe783854a4702ee8adbb838609" - [[package]] name = "concurrent-queue" version = "2.5.0" @@ -1892,7 +1886,6 @@ dependencies = [ name = "pagetop-minimal" version = "0.1.0" dependencies = [ - "concat-string", "indoc", "pastey", ] diff --git a/Cargo.toml b/Cargo.toml index 730922ca..3e0974fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,6 @@ base64ct = { version = "1.8", features = ["alloc"] } change-detection = "1.2" chrono = "0.4" colored = "3.1" -concat-string = "1.0" config = { version = "0.15", default-features = false, features = ["toml"] } figlet-rs = "1.0" fluent-templates = "0.14" diff --git a/extensions/pagetop-admin/src/settings.rs b/extensions/pagetop-admin/src/settings.rs index e2e38ee7..60f73637 100644 --- a/extensions/pagetop-admin/src/settings.rs +++ b/extensions/pagetop-admin/src/settings.rs @@ -2,8 +2,7 @@ //! //! Proporciona una API async para leer y escribir valores JSON en la tabla `settings`. -use pagetop::datetime::Utc; -use pagetop::{Getters, builder_impl}; +use pagetop::prelude::*; use pagetop_seaorm::db::{ ActiveModelTrait, ActiveValue, ColumnTrait, EntityTrait, QueryFilter, dbconn, }; @@ -231,6 +230,6 @@ impl SettingsSchema { /// Devuelve la clave completa de un campo: `"{scope}.{field_name}"`. pub fn key_for(&self, field_name: &str) -> String { - format!("{}.{}", self.scope, field_name) + util::join!(&self.scope, ".", field_name) } } diff --git a/extensions/pagetop-bootsier/src/theme/bs/dropdown/props.rs b/extensions/pagetop-bootsier/src/theme/bs/dropdown/props.rs index c07fcceb..948f349b 100644 --- a/extensions/pagetop-bootsier/src/theme/bs/dropdown/props.rs +++ b/extensions/pagetop-bootsier/src/theme/bs/dropdown/props.rs @@ -202,7 +202,7 @@ impl MenuPosition { #[inline] pub(crate) fn data_offset(self) -> Option { match self { - Self::Offset(x, y) => Some(format!("{x},{y}")), + Self::Offset(x, y) => Some(util::join!(x.to_string(), ",", y.to_string())), _ => None, } } diff --git a/extensions/pagetop-htmx/src/hx.rs b/extensions/pagetop-htmx/src/hx.rs index 1022d077..9db67530 100644 --- a/extensions/pagetop-htmx/src/hx.rs +++ b/extensions/pagetop-htmx/src/hx.rs @@ -81,6 +81,8 @@ //! .with_prop(PropsOp::set(hx::on_htmx("after-swap"), "console.log('done')")); //! ``` +use pagetop::prelude::*; + // **< HTTP Methods >******************************************************************************* /// Realiza una petición GET al servidor y aplica la respuesta al objetivo. @@ -356,7 +358,7 @@ pub const DISABLE: &str = "hx-disable"; /// .with_prop(PropsOp::set(hx::on("mouseenter"), "this.style.opacity='0.8'")); /// ``` pub fn on(event: &str) -> String { - format!("hx-on:{event}") + util::join!("hx-on:", event) } /// Genera `hx-on::{event}` para escuchar eventos propios de HTMX en línea. @@ -372,7 +374,7 @@ pub fn on(event: &str) -> String { /// .with_prop(PropsOp::set(hx::on_htmx("after-swap"), "initTooltips()")); /// ``` pub fn on_htmx(event: &str) -> String { - format!("hx-on::{event}") + util::join!("hx-on::", event) } // **< HTMX Request Headers >*********************************************************************** diff --git a/extensions/pagetop-user/src/component/admin/admin_password_form.rs b/extensions/pagetop-user/src/component/admin/admin_password_form.rs index 3c418660..5775d8b0 100644 --- a/extensions/pagetop-user/src/component/admin/admin_password_form.rs +++ b/extensions/pagetop-user/src/component/admin/admin_password_form.rs @@ -2,8 +2,8 @@ use pagetop::prelude::*; -use crate::ADMIN_USERS_PATH; use crate::LOCALES_USER; +use crate::user_path; use crate::component::{PasswordConfirm, error_banner}; @@ -22,7 +22,7 @@ impl Component for AdminPasswordForm { } async fn prepare(&self, cx: &mut Context) -> Result { - let action = format!("{ADMIN_USERS_PATH}/{}/password", self.user_id()); + let action = user_path(self.user_id(), "password"); let action = self.waypoint().append_to(cx.route(action)); let mut form = Form::new() diff --git a/extensions/pagetop-user/src/component/admin/role_form.rs b/extensions/pagetop-user/src/component/admin/role_form.rs index 5bbfea89..f132f233 100644 --- a/extensions/pagetop-user/src/component/admin/role_form.rs +++ b/extensions/pagetop-user/src/component/admin/role_form.rs @@ -4,6 +4,7 @@ use pagetop::prelude::*; use crate::ADMIN_ROLES_PATH; use crate::LOCALES_USER; +use crate::role_path; use crate::component::error_banner; @@ -35,13 +36,8 @@ impl Component for RoleForm { async fn prepare(&self, cx: &mut Context) -> Result { let action = match self.mode() { - RoleFormMode::New => format!("{ADMIN_ROLES_PATH}/new"), - RoleFormMode::Edit => { - format!( - "{ADMIN_ROLES_PATH}/{}/edit", - self.role_id().copied().unwrap_or_default() - ) - } + RoleFormMode::New => util::join!(ADMIN_ROLES_PATH, "/new"), + RoleFormMode::Edit => role_path(self.role_id().copied().unwrap_or_default(), "edit"), }; let action = self.waypoint().append_to(cx.route(action)); diff --git a/extensions/pagetop-user/src/component/admin/role_permissions_form.rs b/extensions/pagetop-user/src/component/admin/role_permissions_form.rs index ad1b237c..8efc0d34 100644 --- a/extensions/pagetop-user/src/component/admin/role_permissions_form.rs +++ b/extensions/pagetop-user/src/component/admin/role_permissions_form.rs @@ -4,8 +4,8 @@ use pagetop::prelude::*; -use crate::ADMIN_ROLES_PATH; use crate::LOCALES_USER; +use crate::role_path; use crate::component::admin::PermissionGroups; use crate::component::error_banner; @@ -26,7 +26,7 @@ impl Component for RolePermissionsForm { } async fn prepare(&self, cx: &mut Context) -> Result { - let action = format!("{ADMIN_ROLES_PATH}/{}/permissions", self.role_id()); + let action = role_path(self.role_id(), "permissions"); let action = self.waypoint().append_to(cx.route(action)); let mut form = Form::new() @@ -37,12 +37,12 @@ impl Component for RolePermissionsForm { for (idx, (group_label, perms)) in self.groups().iter().enumerate() { let mut field = form::check::Field::new() - .with_id(format!("permission-group-{idx}")) + .with_id(util::join!("permission-group-", idx.to_string())) .with_name("permission_keys"); for (key, label, checked) in perms { let text = label.lookup(cx).unwrap_or_default(); field = field.with_item( - form::check::Item::new(key, Lc::n(format!("{text} ({key})"))) + form::check::Item::new(key, Lc::n(util::join!(text, " (", key, ")"))) .with_checked(*checked), ); } diff --git a/extensions/pagetop-user/src/component/admin/role_table.rs b/extensions/pagetop-user/src/component/admin/role_table.rs index 6ce4397b..cc298c67 100644 --- a/extensions/pagetop-user/src/component/admin/role_table.rs +++ b/extensions/pagetop-user/src/component/admin/role_table.rs @@ -8,6 +8,7 @@ use pagetop_htmx::hx_table::sort_link; use crate::ADMIN_ROLES_PATH; use crate::LOCALES_USER; +use crate::role_path; use crate::service::role_admin::{RoleListItem, RoleSortField}; #[derive(AutoDefault, Clone, Debug, Getters)] @@ -108,7 +109,7 @@ impl Component for RoleTable { ); } - let new_href = waypoint.append_to(cx.route(format!("{ADMIN_ROLES_PATH}/new"))); + let new_href = waypoint.append_to(cx.route(util::join!(ADMIN_ROLES_PATH, "/new"))); Ok(html! { div (self.props().unpack(cx)) { @@ -212,7 +213,7 @@ fn label_cell(role: &RoleListItem, waypoint: &Waypoint) -> Html { let id = role.id; let waypoint = waypoint.clone(); Html::with(move |cx| { - let view_href = waypoint.append_to(cx.route(format!("{ADMIN_ROLES_PATH}/{id}/view"))); + let view_href = waypoint.append_to(cx.route(role_path(id, "view"))); html! { a href=(view_href) { (label.as_str()) } } @@ -234,8 +235,7 @@ async fn actions_cell( let locked = role.locked; let waypoint = waypoint.clone(); - let permissions_href = - waypoint.append_to(cx.route(format!("{ADMIN_ROLES_PATH}/{id}/permissions"))); + let permissions_href = waypoint.append_to(cx.route(role_path(id, "permissions"))); // Los botones se renderizan aquí, no dentro del `Html::with()` de abajo: necesitan pasar por // su propio ciclo de renderizado (`.render().await`) para que el tema activo los estilice @@ -253,7 +253,7 @@ async fn actions_cell( let (edit_button, delete_button) = if locked { (None, None) } else { - let edit_href = waypoint.append_to(cx.route(format!("{ADMIN_ROLES_PATH}/{id}/edit"))); + let edit_href = waypoint.append_to(cx.route(role_path(id, "edit"))); let edit_button = Button::anchor(Lc::t("btn-edit", &LOCALES_USER), edit_href) .with_style(button::Style::Solid(Intent::Primary)) .with_size(button::Size::Small) @@ -263,7 +263,7 @@ async fn actions_cell( // Viaja como query string para que, tanto si el borrado falla como si tiene éxito, la // tabla vuelva a mostrarse en la misma página/orden en que estaba, en vez de reiniciarse. let confirm_href = cx - .route(format!("{ADMIN_ROLES_PATH}/{id}/delete/confirm")) + .route(role_path(id, "delete/confirm")) .alter_param("sort", sort.as_str()) .alter_param("dir", dir) .alter_param("page", page.to_string()) diff --git a/extensions/pagetop-user/src/component/admin/user_form.rs b/extensions/pagetop-user/src/component/admin/user_form.rs index ecaf8217..621f5d9e 100644 --- a/extensions/pagetop-user/src/component/admin/user_form.rs +++ b/extensions/pagetop-user/src/component/admin/user_form.rs @@ -4,6 +4,7 @@ use pagetop::prelude::*; use crate::ADMIN_USERS_PATH; use crate::LOCALES_USER; +use crate::user_path; use crate::component::{PasswordConfirm, error_banner}; @@ -43,13 +44,8 @@ impl Component for UserForm { async fn prepare(&self, cx: &mut Context) -> Result { let action = match self.mode() { - UserFormMode::New => format!("{ADMIN_USERS_PATH}/new"), - UserFormMode::Edit => { - format!( - "{ADMIN_USERS_PATH}/{}/edit", - self.user_id().copied().unwrap_or_default() - ) - } + UserFormMode::New => util::join!(ADMIN_USERS_PATH, "/new"), + UserFormMode::Edit => user_path(self.user_id().copied().unwrap_or_default(), "edit"), }; let action = self.waypoint().append_to(cx.route(action)); diff --git a/extensions/pagetop-user/src/component/admin/user_roles_form.rs b/extensions/pagetop-user/src/component/admin/user_roles_form.rs index 927c07b0..9a3a8dc0 100644 --- a/extensions/pagetop-user/src/component/admin/user_roles_form.rs +++ b/extensions/pagetop-user/src/component/admin/user_roles_form.rs @@ -2,8 +2,8 @@ use pagetop::prelude::*; -use crate::ADMIN_USERS_PATH; use crate::LOCALES_USER; +use crate::user_path; use crate::component::error_banner; @@ -25,7 +25,7 @@ impl Component for UserRolesForm { } async fn prepare(&self, cx: &mut Context) -> Result { - let action = format!("{ADMIN_USERS_PATH}/{}/roles", self.user_id()); + let action = user_path(self.user_id(), "roles"); let action = self.waypoint().append_to(cx.route(action)); let mut form = Form::new() diff --git a/extensions/pagetop-user/src/component/admin/user_table.rs b/extensions/pagetop-user/src/component/admin/user_table.rs index f2122a19..56ab070d 100644 --- a/extensions/pagetop-user/src/component/admin/user_table.rs +++ b/extensions/pagetop-user/src/component/admin/user_table.rs @@ -11,6 +11,7 @@ use crate::LOCALES_USER; use crate::account::UserStatus; use crate::permission::UserPermission; use crate::service::user_admin::{UserListItem, UserSortField}; +use crate::user_path; #[derive(AutoDefault, Clone, Debug, Getters)] pub(crate) struct UserTable { @@ -84,7 +85,7 @@ impl Component for UserTable { ); } - let new_href = waypoint.append_to(cx.route(format!("{ADMIN_USERS_PATH}/new"))); + let new_href = waypoint.append_to(cx.route(util::join!(ADMIN_USERS_PATH, "/new"))); Ok(html! { div (self.props().unpack(cx)) { @@ -191,7 +192,7 @@ fn username_cell(user: &UserListItem, waypoint: &Waypoint) -> Html { let id = user.id; let waypoint = waypoint.clone(); Html::with(move |cx| { - let view_href = waypoint.append_to(cx.route(format!("{ADMIN_USERS_PATH}/{id}/view"))); + let view_href = waypoint.append_to(cx.route(user_path(id, "view"))); html! { a href=(view_href) { (username.as_str()) } } @@ -208,7 +209,7 @@ async fn actions_cell( cx: &mut Context, ) -> Html { let id = user.id; - let edit_href = waypoint.append_to(cx.route(format!("{ADMIN_USERS_PATH}/{id}/edit"))); + let edit_href = waypoint.append_to(cx.route(user_path(id, "edit"))); // El botón se renderiza aquí, no dentro del `Html::with()` de abajo: necesita pasar por su // propio ciclo de renderizado (`.render().await`) para que el tema activo lo estilice igual @@ -220,7 +221,7 @@ async fn actions_cell( .await; let roles_button = if can_assign_roles { - let roles_href = waypoint.append_to(cx.route(format!("{ADMIN_USERS_PATH}/{id}/roles"))); + let roles_href = waypoint.append_to(cx.route(user_path(id, "roles"))); Some( Button::anchor(Lc::t("btn-manage-roles", &LOCALES_USER), roles_href) .with_style(button::Style::Solid(Intent::Neutral)) diff --git a/extensions/pagetop-user/src/handlers/admin/roles.rs b/extensions/pagetop-user/src/handlers/admin/roles.rs index a2211ce9..4c63a698 100644 --- a/extensions/pagetop-user/src/handlers/admin/roles.rs +++ b/extensions/pagetop-user/src/handlers/admin/roles.rs @@ -15,6 +15,7 @@ use crate::config::SETTINGS; use crate::entity::role; use crate::handlers::admin::{back_link, frame, map_auth_error}; use crate::permission::{self, UserPermission}; +use crate::role_path; use crate::service::role_admin::{self, RolePageParams, RoleSortField}; #[derive(Deserialize)] @@ -451,7 +452,7 @@ pub(crate) async fn delete_confirm_get( let mut cx = Context::admin(request); let delete_href = cx - .route(format!("{ADMIN_ROLES_PATH}/{id}/delete")) + .route(role_path(id, "delete")) .alter_param( "sort", RoleSortField::from_query(query.sort.as_deref()).as_str(), diff --git a/extensions/pagetop-user/src/handlers/admin/users.rs b/extensions/pagetop-user/src/handlers/admin/users.rs index ed045684..40900feb 100644 --- a/extensions/pagetop-user/src/handlers/admin/users.rs +++ b/extensions/pagetop-user/src/handlers/admin/users.rs @@ -6,7 +6,6 @@ use pagetop::base::component::table::Row; use pagetop::prelude::*; use pagetop_htmx::prelude::*; -use crate::ADMIN_ROLES_PATH; use crate::ADMIN_USERS_PATH; use crate::ANONYMOUS_ROLE_ID; use crate::AUTHENTICATED_ROLE_ID; @@ -22,8 +21,10 @@ use crate::error::AuthError; use crate::handlers::admin::{back_link, frame, map_auth_error}; use crate::password; use crate::permission::UserPermission; +use crate::role_path; use crate::service::role_admin; use crate::service::user_admin::{self, UserListParams, UserSortField}; +use crate::user_path; #[derive(Deserialize)] pub(crate) struct UsersQuery { @@ -341,12 +342,10 @@ fn edit_actions( ("true", "btn-grant-admin", "confirm-grant-admin") }; - let roles_href = waypoint.append_to(cx.route(format!("{ADMIN_USERS_PATH}/{user_id}/roles"))); - let password_href = - waypoint.append_to(cx.route(format!("{ADMIN_USERS_PATH}/{user_id}/password"))); - let status_action = - waypoint.append_to(cx.route(format!("{ADMIN_USERS_PATH}/{user_id}/status"))); - let admin_action = waypoint.append_to(cx.route(format!("{ADMIN_USERS_PATH}/{user_id}/admin"))); + let roles_href = waypoint.append_to(cx.route(user_path(user_id, "roles"))); + let password_href = waypoint.append_to(cx.route(user_path(user_id, "password"))); + let status_action = waypoint.append_to(cx.route(user_path(user_id, "status"))); + let admin_action = waypoint.append_to(cx.route(user_path(user_id, "admin"))); let mut status_form = Form::new() .with_action(status_action.clone()) @@ -593,9 +592,10 @@ async fn user_view_roles(roles: &[role::Model], cx: &mut Context) -> Block { table.user-admin-table { tbody { @for (id, machine_name, label, system_badge) in &items { + @let href = cx.route(role_path(*id, "view")).to_string(); tr { td { - a href=(cx.route(format!("{ADMIN_ROLES_PATH}/{id}/view")).to_string()) { + a href=(href) { (label.as_str()) } } @@ -744,7 +744,7 @@ pub(crate) async fn status_post( match user_admin::set_user_status(id, new_status, account.id).await { Ok(()) => { let cx = Context::admin(request); - let edit_href = waypoint.append_to(cx.route(format!("{ADMIN_USERS_PATH}/{id}/edit"))); + let edit_href = waypoint.append_to(cx.route(user_path(id, "edit"))); if is_htmx { Ok(HtmxResponse::empty().redirect(edit_href).into_response()) } else { @@ -791,7 +791,7 @@ pub(crate) async fn admin_post( match user_admin::set_user_admin(id, new_is_admin, account.id).await { Ok(()) => { let cx = Context::admin(request); - let edit_href = waypoint.append_to(cx.route(format!("{ADMIN_USERS_PATH}/{id}/edit"))); + let edit_href = waypoint.append_to(cx.route(user_path(id, "edit"))); if is_htmx { Ok(HtmxResponse::empty().redirect(edit_href).into_response()) } else { @@ -816,10 +816,7 @@ pub(crate) async fn password_get( return Err(ErrorPage::NotFound(Some(request))); } let mut page = Page::admin(request); - let edit_href = waypoint.append_to( - page.context() - .route(format!("{ADMIN_USERS_PATH}/{id}/edit")), - ); + let edit_href = waypoint.append_to(page.context().route(user_path(id, "edit"))); let title = Lc::t("title-admin-user-password", &LOCALES_USER); Ok(page .with_title(title.clone()) @@ -861,15 +858,12 @@ pub(crate) async fn password_post( match result { Ok(()) => { let cx = Context::admin(request); - let edit_href = waypoint.append_to(cx.route(format!("{ADMIN_USERS_PATH}/{id}/edit"))); + let edit_href = waypoint.append_to(cx.route(user_path(id, "edit"))); Ok(Redirect::see_other(edit_href).into_response()) } Err(err) => { let mut page = Page::admin(request); - let edit_href = waypoint.append_to( - page.context() - .route(format!("{ADMIN_USERS_PATH}/{id}/edit")), - ); + let edit_href = waypoint.append_to(page.context().route(user_path(id, "edit"))); let title = Lc::t("title-admin-user-password", &LOCALES_USER); Ok(page .with_title(title.clone()) diff --git a/extensions/pagetop-user/src/lib.rs b/extensions/pagetop-user/src/lib.rs index 3cffd393..781a1cff 100644 --- a/extensions/pagetop-user/src/lib.rs +++ b/extensions/pagetop-user/src/lib.rs @@ -131,6 +131,16 @@ const ADMIN_ROLES_PATH: &str = "/admin/user/roles"; // Catálogo de permisos registrados, agrupado por extensión (solo lectura). const ADMIN_PERMISSIONS_PATH: &str = "/admin/user/permissions"; +// Ruta de una acción sobre el usuario `id`: `{ADMIN_USERS_PATH}/{id}/{tail}`. +fn user_path(id: i32, tail: &str) -> String { + util::join!(ADMIN_USERS_PATH, "/", id.to_string(), "/", tail) +} + +// Ruta de una acción sobre el rol `id`: `{ADMIN_ROLES_PATH}/{id}/{tail}`. +fn role_path(id: i32, tail: &str) -> String { + util::join!(ADMIN_ROLES_PATH, "/", id.to_string(), "/", tail) +} + // **< Registro en pagetop-admin (fijo) >*********************************************************** // Registra las páginas de usuarios, roles y permisos en el portal de `pagetop-admin`, bajo la diff --git a/extensions/pagetop-user/src/session.rs b/extensions/pagetop-user/src/session.rs index 485e3138..a7a2b14b 100644 --- a/extensions/pagetop-user/src/session.rs +++ b/extensions/pagetop-user/src/session.rs @@ -1,8 +1,6 @@ //! Gestión de sesiones de usuario (creación, carga, destrucción). -use pagetop::auth::CurrentUser; -use pagetop::datetime::{Duration, Utc}; -use pagetop::web::http::{HeaderMap, header}; +use pagetop::prelude::*; use pagetop_seaorm::db::{ ActiveModelTrait, ColumnTrait, DbErr, EntityTrait, QueryFilter, Set, dbconn, }; @@ -35,32 +33,33 @@ fn bytes_to_hex(bytes: &[u8]) -> String { /// Construye el valor de la cabecera `Set-Cookie` para la cookie de sesión. pub fn build_cookie(sid: &str, remember: bool) -> String { - let mut parts = vec![ - format!("{}={}", SETTINGS.session_cookie_name, sid), - "HttpOnly".into(), - "SameSite=Lax".into(), - "Path=/".into(), - ]; + let mut cookie = util::join!( + &SETTINGS.session_cookie_name, + "=", + sid, + "; HttpOnly; SameSite=Lax; Path=/" + ); if SETTINGS.secure_cookie { - parts.push("Secure".into()); + cookie.push_str("; Secure"); } if remember { - parts.push(format!("Max-Age={}", SETTINGS.session_ttl_secs)); + cookie.push_str("; Max-Age="); + cookie.push_str(&SETTINGS.session_ttl_secs.to_string()); } - parts.join("; ") + cookie } /// Construye la cookie de expiración (Max-Age=0) para borrar la sesión del navegador. pub fn expiry_cookie() -> String { - format!( - "{}=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0", - SETTINGS.session_cookie_name + util::join!( + &SETTINGS.session_cookie_name, + "=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0" ) } /// Extrae el session ID de las cabeceras HTTP de la petición, si existe. -pub fn extract_sid(headers: Option<&HeaderMap>) -> Option { - let cookie_str = headers?.get(header::COOKIE)?.to_str().ok()?; +pub fn extract_sid(headers: Option<&web::http::HeaderMap>) -> Option { + let cookie_str = headers?.get(web::http::header::COOKIE)?.to_str().ok()?; let name = SETTINGS.session_cookie_name.as_str(); for part in cookie_str.split(';') { @@ -78,7 +77,7 @@ pub fn extract_sid(headers: Option<&HeaderMap>) -> Option { /// /// Si no hay cookie o la sesión ha expirado, devuelve `(CurrentUser::Anonymous, None)`. /// Se llama desde el middleware de sesión, que es async. -pub async fn resolve_session(headers: &HeaderMap) -> (CurrentUser, Option) { +pub async fn resolve_session(headers: &web::http::HeaderMap) -> (CurrentUser, Option) { let Some(sid) = extract_sid(Some(headers)) else { return (CurrentUser::Anonymous, None); }; diff --git a/helpers/pagetop-minimal/Cargo.toml b/helpers/pagetop-minimal/Cargo.toml index dfb37a9d..c67e7587 100644 --- a/helpers/pagetop-minimal/Cargo.toml +++ b/helpers/pagetop-minimal/Cargo.toml @@ -16,6 +16,5 @@ license.workspace = true authors.workspace = true [dependencies] -concat-string.workspace = true indoc.workspace = true pastey.workspace = true diff --git a/helpers/pagetop-minimal/README.md b/helpers/pagetop-minimal/README.md index b7a17bc0..162bce18 100644 --- a/helpers/pagetop-minimal/README.md +++ b/helpers/pagetop-minimal/README.md @@ -28,11 +28,6 @@ concatenación de cadenas y el uso rápido de colecciones clave-valor. Las macros para texto multilínea **`indoc!`**, **`formatdoc!`** y **`concatdoc!`** se reexportan del *crate* [indoc](https://crates.io/crates/indoc) de [David Tolnay](https://crates.io/users/dtolnay). -Las macros para la concatenación de cadenas **`join!`** y **`join_pair!`** se apoyan internamente en -el *crate* [concat-string](https://crates.io/crates/concat_string), desarrollado por -[FaultyRAM](https://crates.io/users/FaultyRAM), para evitar el formato de cadenas cuando la -eficiencia pueda ser relevante. - La macro para generar identificadores dinámicos **`paste!`** se reexporta del *crate* [pastey](https://crates.io/crates/pastey), una implementación avanzada y soportada del popular `paste!` de [David Tolnay](https://crates.io/users/dtolnay). diff --git a/helpers/pagetop-minimal/src/lib.rs b/helpers/pagetop-minimal/src/lib.rs index eab70c84..14bd4211 100644 --- a/helpers/pagetop-minimal/src/lib.rs +++ b/helpers/pagetop-minimal/src/lib.rs @@ -29,11 +29,6 @@ concatenación de cadenas y el uso rápido de colecciones clave-valor. Las macros para texto multilínea **`indoc!`**, **`formatdoc!`** y **`concatdoc!`** se reexportan del *crate* [indoc](https://crates.io/crates/indoc) de [David Tolnay](https://crates.io/users/dtolnay). -Las macros para la concatenación de cadenas **`join!`** y **`join_pair!`** se apoyan internamente en -el *crate* [concat-string](https://crates.io/crates/concat_string), desarrollado por -[FaultyRAM](https://crates.io/users/FaultyRAM), para evitar el formato de cadenas cuando la -eficiencia pueda ser relevante. - La macro para generar identificadores dinámicos **`paste!`** se reexporta del *crate* [pastey](https://crates.io/crates/pastey), una implementación avanzada y soportada del popular `paste!` de [David Tolnay](https://crates.io/users/dtolnay). @@ -43,9 +38,6 @@ La macro para generar identificadores dinámicos **`paste!`** se reexporta del * html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/assets/favicon.ico" )] -#[doc(hidden)] -pub use concat_string::concat_string; - pub use indoc::{concatdoc, formatdoc, indoc}; /// Permite *pegar* tokens y generar identificadores a partir de otros. @@ -60,11 +52,17 @@ pub use pastey::paste; // La documentación anterior se copia en `pagetop::util::paste!` porque el *crate* original no la // define y `pagetop` no la hereda automáticamente. -/// Concatena eficientemente varios fragmentos en un [`String`]. +/// Concatena varios fragmentos en un [`String`] reservando una sola vez la memoria del resultado. /// -/// Esta macro exporta [`concat_string!`](https://docs.rs/concat-string). Acepta cualquier número de -/// fragmentos que implementen [`AsRef`] y construye un [`String`] con el tamaño óptimo, de -/// forma eficiente y evitando el uso de cadenas de formato que penalicen el rendimiento. +/// Acepta uno o más fragmentos que implementen [`AsRef`] (literales, `&str`, [`String`], +/// `Cow`...) y construye el resultado con la capacidad exacta, sin pasar por el formateo. Es +/// la forma preferida de componer texto a partir de fragmentos, y suele ser más rápida que +/// `format!`. +/// +/// Cada expresión se evalúa **una sola vez**, por lo que admite llamadas como `n.to_string()` o +/// cierres con efectos secundarios. Los valores que no son cadenas (números, tipos `Display`) se +/// convierten antes con `to_string()`. Si hace falta formato real (`{:02}`, `{:?}`, `{:.2}`...), +/// usar `format!`. /// /// # Ejemplo /// @@ -81,11 +79,26 @@ pub use pastey::paste; /// // Un único fragmento devuelve el mismo valor. /// let single_result = join!("Hello"); /// assert_eq!(single_result, "Hello".to_string()); +/// +/// // Los fragmentos pueden ser de tipos distintos; los números se convierten antes. +/// let name = String::from("item"); +/// let result_mixed = join!(&name, "-", 7.to_string()); +/// assert_eq!(result_mixed, "item-7".to_string()); +/// +/// // Cada expresión se evalúa una sola vez. +/// let mut calls = 0; +/// let mut next = || { +/// calls += 1; +/// calls.to_string() +/// }; +/// let result_once = join!("n", next()); +/// assert_eq!(result_once, "n1".to_string()); +/// assert_eq!(calls, 1); /// ``` #[macro_export] macro_rules! join { - ($($arg:expr),+) => { - $crate::concat_string!($($arg),+) + ($($arg:expr),+ $(,)?) => { + [$(::core::convert::AsRef::::as_ref(&$arg)),+].concat() }; } @@ -126,15 +139,15 @@ macro_rules! join_pair { let second_val = $second; let separator_val = $separator; - let first = AsRef::::as_ref(&first_val); - let second = AsRef::::as_ref(&second_val); + let first = ::core::convert::AsRef::::as_ref(&first_val); + let second = ::core::convert::AsRef::::as_ref(&second_val); let separator = if first.is_empty() || second.is_empty() { "" } else { - AsRef::::as_ref(&separator_val) + ::core::convert::AsRef::::as_ref(&separator_val) }; - $crate::concat_string!(first, separator, second) + [first, separator, second].concat() }}; }