Compare commits

..

No commits in common. "0127d17459be19a92e508c8e4f70d2e5df6ede04" and "3bf058b8a54f66f6fcc3dc9c940bf7ea9c190013" have entirely different histories.

9 changed files with 352 additions and 616 deletions

View file

@ -39,7 +39,7 @@ mod smart_default;
use proc_macro::TokenStream;
use quote::{quote, quote_spanned};
use syn::{parse_macro_input, spanned::Spanned, DeriveInput};
use syn::{parse_macro_input, spanned::Spanned, DeriveInput, ItemFn};
/// Macro para escribir plantillas HTML (basada en [Maud](https://docs.rs/maud)).
#[proc_macro]
@ -107,216 +107,114 @@ pub fn derive_auto_default(input: TokenStream) -> TokenStream {
/// `alter_...()`, que permitirá más adelante modificar instancias existentes.
#[proc_macro_attribute]
pub fn builder_fn(_: TokenStream, item: TokenStream) -> TokenStream {
use syn::{parse2, FnArg, Ident, ImplItemFn, Pat, ReturnType, TraitItemFn, Type};
let ts: proc_macro2::TokenStream = item.clone().into();
enum Kind {
Impl(ImplItemFn),
Trait(TraitItemFn),
}
// Detecta si estamos en `impl` o `trait`.
let kind = if let Ok(it) = parse2::<ImplItemFn>(ts.clone()) {
Kind::Impl(it)
} else if let Ok(tt) = parse2::<TraitItemFn>(ts.clone()) {
Kind::Trait(tt)
} else {
return quote! {
compile_error!("#[builder_fn] only supports methods in `impl` blocks or `trait` items");
}
.into();
};
// Extrae piezas comunes (sig, attrs, vis, bloque?, es_trait?).
let (sig, attrs, vis, body_opt, is_trait) = match &kind {
Kind::Impl(m) => (&m.sig, &m.attrs, Some(&m.vis), Some(&m.block), false),
Kind::Trait(t) => (&t.sig, &t.attrs, None, t.default.as_ref(), true),
};
let with_name = sig.ident.clone();
let with_name_str = sig.ident.to_string();
let fn_with = parse_macro_input!(item as ItemFn);
let fn_with_name = fn_with.sig.ident.clone();
let fn_with_name_str = fn_with.sig.ident.to_string();
// Valida el nombre del método.
if !with_name_str.starts_with("with_") {
return quote_spanned! {
sig.ident.span() => compile_error!("expected a named `with_...()` method");
}
.into();
}
// Sólo se exige `pub` en `impl` (en `trait` no aplica).
let vis_pub = match (is_trait, vis) {
(false, Some(v)) => quote! { #v },
_ => quote! {},
};
// Validaciones comunes.
if sig.asyncness.is_some() {
return quote_spanned! {
sig.asyncness.span() => compile_error!("`with_...()` cannot be `async`");
}
.into();
}
if sig.constness.is_some() {
return quote_spanned! {
sig.constness.span() => compile_error!("`with_...()` cannot be `const`");
}
.into();
}
if sig.abi.is_some() {
return quote_spanned! {
sig.abi.span() => compile_error!("`with_...()` cannot be `extern`");
}
.into();
}
if sig.unsafety.is_some() {
return quote_spanned! {
sig.unsafety.span() => compile_error!("`with_...()` cannot be `unsafe`");
}
.into();
}
// En `impl` se exige exactamente `mut self`; y en `trait` se exige `self` (sin &).
let receiver_ok = match sig.inputs.first() {
Some(FnArg::Receiver(r)) => {
// Rechaza `self: SomeType`.
if r.colon_token.is_some() {
false
} else if is_trait {
// Exactamente `self` (sin &, sin mut).
r.reference.is_none() && r.mutability.is_none()
} else {
// Exactamente `mut self`.
r.reference.is_none() && r.mutability.is_some()
}
}
_ => false,
};
if !receiver_ok {
let msg = if is_trait {
"expected `self` (not `mut self`, `&self` or `&mut self`) in trait method"
} else {
"expected first argument to be exactly `mut self`"
if !fn_with_name_str.starts_with("with_") {
let expanded = quote_spanned! {
fn_with.sig.ident.span() =>
compile_error!("expected a \"pub fn with_...(mut self, ...) -> Self\" method");
};
let err = sig
.inputs
.first()
.map(|a| a.span())
.unwrap_or(sig.ident.span());
return expanded.into();
}
// Valida que el método es público.
if !matches!(fn_with.vis, syn::Visibility::Public(_)) {
return quote_spanned! {
err => compile_error!(#msg);
fn_with.sig.ident.span() => compile_error!("expected method to be `pub`");
}
.into();
}
// Valida que el método devuelve exactamente `Self`.
match &sig.output {
ReturnType::Type(_, ty) => match ty.as_ref() {
Type::Path(p) if p.qself.is_none() && p.path.is_ident("Self") => {}
_ => {
return quote_spanned! {
ty.span() => compile_error!("expected return type to be exactly `Self`");
}
.into();
}
},
_ => {
// Valida que el primer argumento es exactamente `mut self`.
if let Some(syn::FnArg::Receiver(receiver)) = fn_with.sig.inputs.first() {
if receiver.mutability.is_none() || receiver.reference.is_some() {
return quote_spanned! {
sig.output.span() => compile_error!("expected return type to be exactly `Self`");
receiver.span() => compile_error!("expected `mut self` as the first argument");
}
.into();
}
} else {
return quote_spanned! {
fn_with.sig.ident.span() => compile_error!("expected `mut self` as the first argument");
}
.into();
}
// Valida que el método devuelve exactamente `Self`.
if let syn::ReturnType::Type(_, ty) = &fn_with.sig.output {
if let syn::Type::Path(type_path) = ty.as_ref() {
if type_path.qself.is_some() || !type_path.path.is_ident("Self") {
return quote_spanned! { ty.span() =>
compile_error!("expected return type to be exactly `Self`");
}
.into();
}
} else {
return quote_spanned! { ty.span() =>
compile_error!("expected return type to be exactly `Self`");
}
.into();
}
} else {
return quote_spanned! {
fn_with.sig.output.span() => compile_error!("expected method to return `Self`");
}
.into();
}
// Genera el nombre del método alter_...().
let stem = with_name_str.strip_prefix("with_").expect("validated");
let alter_ident = Ident::new(&format!("alter_{stem}"), with_name.span());
let fn_alter_name_str = fn_with_name_str.replace("with_", "alter_");
let fn_alter_name = syn::Ident::new(&fn_alter_name_str, fn_with.sig.ident.span());
// Extrae genéricos y cláusulas where.
let generics = &sig.generics;
let where_clause = &sig.generics.where_clause;
let fn_generics = &fn_with.sig.generics;
let where_clause = &fn_with.sig.generics.where_clause;
// Extrae identificadores de los argumentos para la llamada (sin `mut` ni patrones complejos).
let args: Vec<_> = sig.inputs.iter().skip(1).collect();
let call_idents: Vec<Ident> = {
let mut v = Vec::new();
for arg in sig.inputs.iter().skip(1) {
match arg {
FnArg::Typed(pat) => {
if let Pat::Ident(pat_ident) = pat.pat.as_ref() {
v.push(pat_ident.ident.clone());
} else {
return quote_spanned! {
pat.pat.span() => compile_error!(
"each parameter must be a simple identifier, e.g. `value: T`"
);
}
.into();
}
}
_ => {
return quote_spanned! {
arg.span() => compile_error!("unexpected receiver in parameter list");
}
.into();
}
}
}
v
};
// Extrae atributos descartando la documentación para incluir en `alter_...()`.
let non_doc_attrs: Vec<_> = attrs
// Extrae argumentos y parámetros de llamada.
let args: Vec<_> = fn_with.sig.inputs.iter().skip(1).collect();
let params: Vec<_> = fn_with
.sig
.inputs
.iter()
.cloned()
.filter(|a| !a.path().is_ident("doc"))
.skip(1)
.map(|arg| match arg {
syn::FnArg::Typed(pat) => &pat.pat,
_ => panic!("unexpected argument type"),
})
.collect();
// Documentación del método alter_...().
let alter_doc =
format!("Equivalente a [`Self::{with_name_str}()`], pero fuera del patrón *builder*.");
// Extrae bloque del método.
let fn_with_block = &fn_with.block;
// Extrae documentación y otros atributos del método.
let fn_with_attrs = &fn_with.attrs;
// Genera el método alter_...() con el código del método with_...().
let fn_alter_doc =
format!("Equivalente a [`Self::{fn_with_name_str}()`], pero sin usar el patrón *builder*.");
let fn_alter = quote! {
#[doc = #fn_alter_doc]
pub fn #fn_alter_name #fn_generics(&mut self, #(#args),*) -> &mut Self #where_clause {
#fn_with_block
}
};
// Redefine el método with_...() para que llame a alter_...().
let fn_with = quote! {
#(#fn_with_attrs)*
#[inline]
pub fn #fn_with_name #fn_generics(mut self, #(#args),*) -> Self #where_clause {
self.#fn_alter_name(#(#params),*);
self
}
};
// Genera el código final.
let expanded = match body_opt {
None => {
quote! {
#(#attrs)*
fn #with_name #generics (self, #(#args),*) -> Self #where_clause;
#(#non_doc_attrs)*
#[doc = #alter_doc]
fn #alter_ident #generics (&mut self, #(#args),*) -> &mut Self #where_clause;
}
}
Some(body) => {
let with_fn = if is_trait {
quote! {
#vis_pub fn #with_name #generics (self, #(#args),*) -> Self #where_clause {
let mut s = self;
s.#alter_ident(#(#call_idents),*);
s
}
}
} else {
quote! {
#vis_pub fn #with_name #generics (mut self, #(#args),*) -> Self #where_clause {
self.#alter_ident(#(#call_idents),*);
self
}
}
};
quote! {
#(#attrs)*
#with_fn
#(#non_doc_attrs)*
#[doc = #alter_doc]
#vis_pub fn #alter_ident #generics (&mut self, #(#args),*) -> &mut Self #where_clause {
#body
}
}
}
let expanded = quote! {
#fn_with
#[inline]
#fn_alter
};
expanded.into()
}

View file

@ -50,11 +50,12 @@ pub struct App {
pub theme: String,
/// Idioma por defecto para la aplicación.
///
/// Si no está definido o no es válido, el idioma efectivo para el renderizado se resolverá
/// según la implementación de [`LangId`](crate::locale::LangId) en este orden: primero intenta
/// con el establecido en [`Contextual::with_langid()`](crate::html::Contextual::with_langid);
/// pero si no se ha definido explícitamente, usará el indicado en la cabecera `Accept-Language`
/// del navegador; y, si ninguno aplica, se empleará el idioma de respaldo ("en-US").
/// Si no se especifica un valor válido, normalmente se usará el idioma devuelto por la
/// implementación de [`LangId`](crate::locale::LangId) para [`Context`](crate::html::Context),
/// en el siguiente orden: primero, el idioma establecido explícitamente con
/// [`Context::with_langid()`](crate::html::Context::with_langid); si no se ha definido, se
/// usará el indicado en la cabecera `Accept-Language` del navegador; y, si ninguno aplica, se
/// empleará el idioma de respaldo ("en-US").
pub language: String,
/// Banner ASCII mostrado al inicio: *"Off"* (desactivado), *"Slant"*, *"Small"*, *"Speed"* o
/// *"Starwars"*.

View file

@ -9,12 +9,12 @@ mod assets;
pub use assets::favicon::Favicon;
pub use assets::javascript::JavaScript;
pub use assets::stylesheet::{StyleSheet, TargetMedia};
pub use assets::{Asset, Assets};
pub(crate) use assets::Assets;
// HTML DOCUMENT CONTEXT ***************************************************************************
mod context;
pub use context::{AssetsOp, Context, Contextual, ErrorParam};
pub use context::{AssetsOp, Context, ErrorParam};
// HTML ATTRIBUTES *********************************************************************************

View file

@ -5,49 +5,22 @@ pub mod stylesheet;
use crate::html::{html, Markup, Render};
use crate::{AutoDefault, Weight};
/// Representación genérica de un *script* [`JavaScript`](crate::html::JavaScript) o una hoja de
/// estilos [`StyleSheet`](crate::html::StyleSheet).
///
/// Estos recursos se incluyen en los conjuntos de recursos ([`Assets`]) que suelen renderizarse en
/// un documento HTML.
///
/// Cada recurso se identifica por un **nombre único** ([`Asset::name()`]), usado como clave; y un
/// **peso** ([`Asset::weight()`]), que determina su orden relativo de renderizado.
pub trait Asset: Render {
/// Devuelve el nombre del recurso, utilizado como clave única.
pub trait AssetsTrait: Render {
// Devuelve el nombre del recurso, utilizado como clave única.
fn name(&self) -> &str;
/// Devuelve el peso del recurso, usado para ordenar el renderizado de menor a mayor peso.
// Devuelve el peso del recurso, durante el renderizado se procesan de menor a mayor peso.
fn weight(&self) -> Weight;
}
/// Gestión común para conjuntos de recursos como [`JavaScript`](crate::html::JavaScript) y
/// [`StyleSheet`](crate::html::StyleSheet).
///
/// Se emplea normalmente para agrupar, administrar y renderizar los recursos de un documento HTML.
/// Cada recurso se identifica por un nombre único ([`Asset::name()`]) y tiene asociado un peso
/// ([`Asset::weight()`]) que determina su orden de renderizado.
///
/// Durante el renderizado, los recursos se procesan en orden ascendente de peso. En caso de
/// igualdad, se respeta el orden de inserción.
#[derive(AutoDefault)]
pub struct Assets<T>(Vec<T>);
pub(crate) struct Assets<T>(Vec<T>);
impl<T: Asset> Assets<T> {
/// Crea un nuevo conjunto vacío de recursos.
///
/// Normalmente no se instancia directamente, sino como parte de la gestión de recursos que
/// hacen páginas o temas.
impl<T: AssetsTrait> Assets<T> {
pub fn new() -> Self {
Self(Vec::new())
Assets::<T>(Vec::<T>::new())
}
/// Inserta un recurso.
///
/// Si no existe otro con el mismo nombre, lo añade. Si ya existe y su peso era mayor, lo
/// reemplaza. Y si su peso era menor o igual, entonces no realiza ningún cambio.
///
/// Devuelve `true` si el recurso fue insertado o reemplazado.
pub fn add(&mut self, asset: T) -> bool {
match self.0.iter().position(|x| x.name() == asset.name()) {
Some(index) => {
@ -66,9 +39,6 @@ impl<T: Asset> Assets<T> {
}
}
/// Elimina un recurso por nombre.
///
/// Devuelve `true` si el recurso existía y fue eliminado.
pub fn remove(&mut self, name: impl AsRef<str>) -> bool {
if let Some(index) = self.0.iter().position(|x| x.name() == name.as_ref()) {
self.0.remove(index);
@ -79,14 +49,14 @@ impl<T: Asset> Assets<T> {
}
}
impl<T: Asset> Render for Assets<T> {
impl<T: AssetsTrait> Render for Assets<T> {
fn render(&self) -> Markup {
let mut assets = self.0.iter().collect::<Vec<_>>();
assets.sort_by_key(|a| a.weight());
html! {
@for a in assets {
(a)
(a.render())
}
}
}

View file

@ -1,4 +1,4 @@
use crate::html::assets::Asset;
use crate::html::assets::AssetsTrait;
use crate::html::{html, Markup, Render};
use crate::{join, join_pair, AutoDefault, Weight};
@ -137,10 +137,8 @@ impl JavaScript {
}
}
impl Asset for JavaScript {
/// Devuelve el nombre del recurso, utilizado como clave única.
///
/// Para *scripts* externos es la ruta del recurso; para *scripts* embebidos, un identificador.
impl AssetsTrait for JavaScript {
// Para *scripts* externos es la ruta; para *scripts* embebidos, un identificador.
fn name(&self) -> &str {
match &self.source {
Source::From(path) => path,

View file

@ -1,4 +1,4 @@
use crate::html::assets::Asset;
use crate::html::assets::AssetsTrait;
use crate::html::{html, Markup, PreEscaped, Render};
use crate::{join_pair, AutoDefault, Weight};
@ -142,10 +142,8 @@ impl StyleSheet {
}
}
impl Asset for StyleSheet {
/// Devuelve el nombre del recurso, utilizado como clave única.
///
/// Para hojas de estilos externas es la ruta del recurso; para las embebidas, un identificador.
impl AssetsTrait for StyleSheet {
// Para hojas de estilos externas es la ruta; para las embebidas, un identificador.
fn name(&self) -> &str {
match &self.source {
Source::From(path) => path,

View file

@ -10,7 +10,7 @@ use crate::{builder_fn, join};
use std::any::Any;
use std::collections::HashMap;
/// Operaciones para modificar el contexto ([`Context`]) de un documento.
/// Operaciones para modificar el contexto ([`Context`]) del documento.
pub enum AssetsOp {
// Favicon.
/// Define el *favicon* del documento. Sobrescribe cualquier valor anterior.
@ -47,101 +47,12 @@ pub enum ErrorParam {
},
}
/// Interfaz para gestionar el **contexto de renderizado** de un documento HTML.
/// Representa el contexto de un documento HTML.
///
/// `Contextual` extiende [`LangId`] y define los métodos para:
///
/// - Establecer el **idioma** del documento.
/// - Almacenar la **solicitud HTTP** de origen.
/// - Seleccionar **tema** y **composición** (*layout*) de renderizado.
/// - Administrar **recursos** del documento como el icono [`Favicon`], las hojas de estilo
/// [`StyleSheet`] o los *scripts* [`JavaScript`] mediante [`AssetsOp`].
/// - Leer y mantener **parámetros dinámicos tipados** de contexto.
/// - Generar **identificadores únicos** por tipo de componente.
///
/// Lo implementan, típicamente, estructuras que representan el contexto de renderizado, como
/// [`Context`](crate::html::Context) o [`Page`](crate::response::page::Page).
///
/// # Ejemplo
///
/// ```rust
/// use pagetop::prelude::*;
///
/// fn prepare_context<C: Contextual>(cx: C) -> C {
/// cx.with_langid(&LangMatch::resolve("es-ES"))
/// .with_theme("aliner")
/// .with_layout("default")
/// .with_assets(AssetsOp::SetFavicon(Some(Favicon::new().with_icon("/favicon.ico"))))
/// .with_assets(AssetsOp::AddStyleSheet(StyleSheet::from("/css/app.css")))
/// .with_assets(AssetsOp::AddJavaScript(JavaScript::defer("/js/app.js")))
/// .with_param("usuario_id", 42_i32)
/// }
/// ```
pub trait Contextual: LangId {
// Contextual BUILDER **************************************************************************
/// Establece el idioma del documento.
#[builder_fn]
fn with_langid(self, language: &impl LangId) -> Self;
/// Almacena la solicitud HTTP de origen en el contexto.
#[builder_fn]
fn with_request(self, request: Option<HttpRequest>) -> Self;
/// Especifica el tema para renderizar el documento.
#[builder_fn]
fn with_theme(self, theme_name: &'static str) -> Self;
/// Especifica la composición para renderizar el documento.
#[builder_fn]
fn with_layout(self, layout_name: &'static str) -> Self;
/// Añade o modifica un parámetro dinámico del contexto.
#[builder_fn]
fn with_param<T: 'static>(self, key: &'static str, value: T) -> Self;
/// Define los recursos del contexto usando [`AssetsOp`].
#[builder_fn]
fn with_assets(self, op: AssetsOp) -> Self;
// Contextual GETTERS **************************************************************************
/// Devuelve una referencia a la solicitud HTTP asociada, si existe.
fn request(&self) -> Option<&HttpRequest>;
/// Devuelve el tema que se usará para renderizar el documento.
fn theme(&self) -> ThemeRef;
/// Devuelve la composición para renderizar el documento. Por defecto es `"default"`.
fn layout(&self) -> &str;
/// Recupera un parámetro como [`Option`].
fn param<T: 'static>(&self, key: &'static str) -> Option<&T>;
/// Devuelve el Favicon de los recursos del contexto.
fn favicon(&self) -> Option<&Favicon>;
/// Devuelve las hojas de estilo de los recursos del contexto.
fn stylesheets(&self) -> &Assets<StyleSheet>;
/// Devuelve los *scripts* JavaScript de los recursos del contexto.
fn javascripts(&self) -> &Assets<JavaScript>;
// Contextual HELPERS **************************************************************************
/// Genera un identificador único por tipo (`<tipo>-<n>`) cuando no se aporta uno explícito.
///
/// Es útil para componentes u otros elementos HTML que necesitan un identificador predecible si
/// no se proporciona ninguno.
fn required_id<T>(&mut self, id: Option<String>) -> String;
}
/// Implementa un **contexto de renderizado** para un documento HTML.
///
/// Extiende [`Contextual`] con métodos para **instanciar** y configurar un nuevo contexto,
/// **renderizar los recursos** del documento (incluyendo el [`Favicon`], las hojas de estilo
/// [`StyleSheet`] y los *scripts* [`JavaScript`]), o extender el uso de **parámetros dinámicos
/// tipados** con nuevos métodos.
/// Se crea internamente para manejar información relevante del documento, como la solicitud HTTP de
/// origen, el idioma, tema y composición para el renderizado, los recursos *favicon* ([`Favicon`]),
/// hojas de estilo ([`StyleSheet`]) y *scripts* ([`JavaScript`]), así como *parámetros dinámicos
/// heterogéneos* de contexto definidos en tiempo de ejecución.
///
/// # Ejemplos
///
@ -196,7 +107,7 @@ pub struct Context {
favicon : Option<Favicon>, // Favicon, si se ha definido.
stylesheets: Assets<StyleSheet>, // Hojas de estilo CSS.
javascripts: Assets<JavaScript>, // Scripts JavaScript.
params : HashMap<&'static str, (Box<dyn Any>, &'static str)>, // Parámetros en ejecución.
params : HashMap<&'static str, (Box<dyn Any>, &'static str)>, // Parámetros definidos en tiempo de ejecución.
id_counter : usize, // Contador para generar identificadores únicos.
}
@ -240,6 +151,80 @@ impl Context {
}
}
// Context BUILDER *****************************************************************************
/// Modifica la fuente de idioma del documento.
#[builder_fn]
pub fn with_langid(mut self, language: &impl LangId) -> Self {
self.langid = language.langid();
self
}
/// Modifica el tema que se usará para renderizar el documento.
///
/// Localiza el tema por su [`short_name()`](crate::core::AnyInfo::short_name), y si no aplica
/// ninguno entonces usará el tema por defecto.
#[builder_fn]
pub fn with_theme(mut self, theme_name: &'static str) -> Self {
self.theme = theme_by_short_name(theme_name).unwrap_or(*DEFAULT_THEME);
self
}
/// Modifica la composición para renderizar el documento.
#[builder_fn]
pub fn with_layout(mut self, layout_name: &'static str) -> Self {
self.layout = layout_name;
self
}
/// Define los recursos del contexto usando [`AssetsOp`].
#[builder_fn]
pub fn with_assets(mut self, op: AssetsOp) -> Self {
match op {
// Favicon.
AssetsOp::SetFavicon(favicon) => {
self.favicon = favicon;
}
AssetsOp::SetFaviconIfNone(icon) => {
if self.favicon.is_none() {
self.favicon = Some(icon);
}
}
// Stylesheets.
AssetsOp::AddStyleSheet(css) => {
self.stylesheets.add(css);
}
AssetsOp::RemoveStyleSheet(path) => {
self.stylesheets.remove(path);
}
// JavaScripts.
AssetsOp::AddJavaScript(js) => {
self.javascripts.add(js);
}
AssetsOp::RemoveJavaScript(path) => {
self.javascripts.remove(path);
}
}
self
}
// Context GETTERS *****************************************************************************
/// Devuelve una referencia a la solicitud HTTP asociada, si existe.
pub fn request(&self) -> Option<&HttpRequest> {
self.request.as_ref()
}
/// Devuelve el tema que se usará para renderizar el documento.
pub fn theme(&self) -> ThemeRef {
self.theme
}
/// Devuelve la composición para renderizar el documento. Por defecto es `"default"`.
pub fn layout(&self) -> &str {
self.layout
}
// Context RENDER ******************************************************************************
/// Renderiza los recursos del contexto.
@ -255,6 +240,61 @@ impl Context {
// Context PARAMS ******************************************************************************
/// Añade o modifica un parámetro dinámico del contexto.
///
/// El valor se guarda conservando el *nombre del tipo* real para mejorar los mensajes de error
/// posteriores.
///
/// # Ejemplos
///
/// ```rust
/// use pagetop::prelude::*;
///
/// let cx = Context::new(None)
/// .with_param("usuario_id", 42_i32)
/// .with_param("titulo", String::from("Hola"))
/// .with_param("flags", vec!["a", "b"]);
/// ```
#[builder_fn]
pub fn with_param<T: 'static>(mut self, key: &'static str, value: T) -> Self {
let type_name = TypeInfo::FullName.of::<T>();
self.params.insert(key, (Box::new(value), type_name));
self
}
/// Recupera un parámetro como [`Option`], simplificando el acceso.
///
/// A diferencia de [`get_param`](Self::get_param), que devuelve un [`Result`] con información
/// detallada de error, este método devuelve `None` tanto si la clave no existe como si el valor
/// guardado no coincide con el tipo solicitado.
///
/// Resulta útil en escenarios donde sólo interesa saber si el valor existe y es del tipo
/// correcto, sin necesidad de diferenciar entre error de ausencia o de tipo.
///
/// # Ejemplo
///
/// ```rust
/// use pagetop::prelude::*;
///
/// let cx = Context::new(None).with_param("username", String::from("Alice"));
///
/// // Devuelve Some(&String) si existe y coincide el tipo.
/// assert_eq!(cx.param::<String>("username").map(|s| s.as_str()), Some("Alice"));
///
/// // Devuelve None si no existe o si el tipo no coincide.
/// assert!(cx.param::<i32>("username").is_none());
/// assert!(cx.param::<String>("missing").is_none());
///
/// // Acceso con valor por defecto.
/// let user = cx.param::<String>("missing")
/// .cloned()
/// .unwrap_or_else(|| "visitor".to_string());
/// assert_eq!(user, "visitor");
/// ```
pub fn param<T: 'static>(&self, key: &'static str) -> Option<&T> {
self.get_param::<T>(key).ok()
}
/// Recupera una *referencia tipada* al parámetro solicitado.
///
/// Devuelve:
@ -288,6 +328,23 @@ impl Context {
})
}
/// Elimina un parámetro del contexto. Devuelve `true` si la clave existía y se eliminó.
///
/// Devuelve `false` en caso contrario. Usar cuando solo interesa borrar la entrada.
///
/// # Ejemplos
///
/// ```rust
/// use pagetop::prelude::*;
///
/// let mut cx = Context::new(None).with_param("temp", 1u8);
/// assert!(cx.remove_param("temp"));
/// assert!(!cx.remove_param("temp")); // ya no existe
/// ```
pub fn remove_param(&mut self, key: &'static str) -> bool {
self.params.remove(key).is_some()
}
/// Recupera el parámetro solicitado y lo elimina del contexto.
///
/// Devuelve:
@ -323,21 +380,30 @@ impl Context {
})
}
/// Elimina un parámetro del contexto. Devuelve `true` si la clave existía y se eliminó.
// Context EXTRAS ******************************************************************************
/// Genera un identificador único si no se proporciona uno explícito.
///
/// Devuelve `false` en caso contrario. Usar cuando solo interesa borrar la entrada.
///
/// # Ejemplos
///
/// ```rust
/// use pagetop::prelude::*;
///
/// let mut cx = Context::new(None).with_param("temp", 1u8);
/// assert!(cx.remove_param("temp"));
/// assert!(!cx.remove_param("temp")); // ya no existe
/// ```
pub fn remove_param(&mut self, key: &'static str) -> bool {
self.params.remove(key).is_some()
/// Si no se proporciona un `id`, se genera un identificador único en la forma `<tipo>-<número>`
/// donde `<tipo>` es el nombre corto del tipo en minúsculas (sin espacios) y `<número>` es un
/// contador interno incremental.
pub fn required_id<T>(&mut self, id: Option<String>) -> String {
if let Some(id) = id {
id
} else {
let prefix = TypeInfo::ShortName
.of::<T>()
.trim()
.replace(' ', "_")
.to_lowercase();
let prefix = if prefix.is_empty() {
"prefix".to_owned()
} else {
prefix
};
self.id_counter += 1;
join!(prefix, "-", self.id_counter.to_string())
}
}
}
@ -357,173 +423,3 @@ impl LangId for Context {
self.langid
}
}
impl Contextual for Context {
// Contextual BUILDER **************************************************************************
#[builder_fn]
fn with_request(mut self, request: Option<HttpRequest>) -> Self {
self.request = request;
self
}
#[builder_fn]
fn with_langid(mut self, language: &impl LangId) -> Self {
self.langid = language.langid();
self
}
/// Asigna el tema para renderizar el documento.
///
/// Localiza el tema por su [`short_name()`](crate::core::AnyInfo::short_name), y si no aplica
/// ninguno entonces usará el tema por defecto.
#[builder_fn]
fn with_theme(mut self, theme_name: &'static str) -> Self {
self.theme = theme_by_short_name(theme_name).unwrap_or(*DEFAULT_THEME);
self
}
#[builder_fn]
fn with_layout(mut self, layout_name: &'static str) -> Self {
self.layout = layout_name;
self
}
/// Añade o modifica un parámetro dinámico del contexto.
///
/// El valor se guarda conservando el *nombre del tipo* real para mejorar los mensajes de error
/// posteriores.
///
/// # Ejemplos
///
/// ```rust
/// use pagetop::prelude::*;
///
/// let cx = Context::new(None)
/// .with_param("usuario_id", 42_i32)
/// .with_param("titulo", String::from("Hola"))
/// .with_param("flags", vec!["a", "b"]);
/// ```
#[builder_fn]
fn with_param<T: 'static>(mut self, key: &'static str, value: T) -> Self {
let type_name = TypeInfo::FullName.of::<T>();
self.params.insert(key, (Box::new(value), type_name));
self
}
#[builder_fn]
fn with_assets(mut self, op: AssetsOp) -> Self {
match op {
// Favicon.
AssetsOp::SetFavicon(favicon) => {
self.favicon = favicon;
}
AssetsOp::SetFaviconIfNone(icon) => {
if self.favicon.is_none() {
self.favicon = Some(icon);
}
}
// Stylesheets.
AssetsOp::AddStyleSheet(css) => {
self.stylesheets.add(css);
}
AssetsOp::RemoveStyleSheet(path) => {
self.stylesheets.remove(path);
}
// JavaScripts.
AssetsOp::AddJavaScript(js) => {
self.javascripts.add(js);
}
AssetsOp::RemoveJavaScript(path) => {
self.javascripts.remove(path);
}
}
self
}
// Contextual GETTERS **************************************************************************
fn request(&self) -> Option<&HttpRequest> {
self.request.as_ref()
}
fn theme(&self) -> ThemeRef {
self.theme
}
fn layout(&self) -> &str {
self.layout
}
/// Recupera un parámetro como [`Option`], simplificando el acceso.
///
/// A diferencia de [`get_param`](Self::get_param), que devuelve un [`Result`] con información
/// detallada de error, este método devuelve `None` tanto si la clave no existe como si el valor
/// guardado no coincide con el tipo solicitado.
///
/// Resulta útil en escenarios donde sólo interesa saber si el valor existe y es del tipo
/// correcto, sin necesidad de diferenciar entre error de ausencia o de tipo.
///
/// # Ejemplo
///
/// ```rust
/// use pagetop::prelude::*;
///
/// let cx = Context::new(None).with_param("username", String::from("Alice"));
///
/// // Devuelve Some(&String) si existe y coincide el tipo.
/// assert_eq!(cx.param::<String>("username").map(|s| s.as_str()), Some("Alice"));
///
/// // Devuelve None si no existe o si el tipo no coincide.
/// assert!(cx.param::<i32>("username").is_none());
/// assert!(cx.param::<String>("missing").is_none());
///
/// // Acceso con valor por defecto.
/// let user = cx.param::<String>("missing")
/// .cloned()
/// .unwrap_or_else(|| "visitor".to_string());
/// assert_eq!(user, "visitor");
/// ```
fn param<T: 'static>(&self, key: &'static str) -> Option<&T> {
self.get_param::<T>(key).ok()
}
fn favicon(&self) -> Option<&Favicon> {
self.favicon.as_ref()
}
fn stylesheets(&self) -> &Assets<StyleSheet> {
&self.stylesheets
}
fn javascripts(&self) -> &Assets<JavaScript> {
&self.javascripts
}
// Contextual HELPERS **************************************************************************
/// Devuelve un identificador único dentro del contexto para el tipo `T`, si no se proporciona
/// un `id` explícito.
///
/// Si no se proporciona un `id`, se genera un identificador único en la forma `<tipo>-<número>`
/// donde `<tipo>` es el nombre corto del tipo en minúsculas (sin espacios) y `<número>` es un
/// contador interno incremental.
fn required_id<T>(&mut self, id: Option<String>) -> String {
if let Some(id) = id {
id
} else {
let prefix = TypeInfo::ShortName
.of::<T>()
.trim()
.replace(' ', "_")
.to_lowercase();
let prefix = if prefix.is_empty() {
"prefix".to_owned()
} else {
prefix
};
self.id_counter += 1;
join!(prefix, "-", self.id_counter.to_string())
}
}
}

View file

@ -8,8 +8,7 @@ use crate::builder_fn;
use crate::core::component::{Child, ChildOp, Component};
use crate::core::theme::{ChildrenInRegions, ThemeRef, REGION_CONTENT};
use crate::html::{html, Markup, DOCTYPE};
use crate::html::{Assets, Favicon, JavaScript, StyleSheet};
use crate::html::{AssetsOp, Context, Contextual};
use crate::html::{AssetsOp, Context};
use crate::html::{AttrClasses, ClassesOp};
use crate::html::{AttrId, AttrL10n};
use crate::locale::{CharacterDirection, L10n, LangId, LanguageIdentifier};
@ -26,9 +25,9 @@ pub struct Page {
description : AttrL10n,
metadata : Vec<(&'static str, &'static str)>,
properties : Vec<(&'static str, &'static str)>,
context : Context,
body_id : AttrId,
body_classes: AttrClasses,
context : Context,
regions : ChildrenInRegions,
}
@ -44,9 +43,9 @@ impl Page {
description : AttrL10n::default(),
metadata : Vec::default(),
properties : Vec::default(),
context : Context::new(request),
body_id : AttrId::default(),
body_classes: AttrClasses::default(),
context : Context::new(request),
regions : ChildrenInRegions::default(),
}
}
@ -81,6 +80,40 @@ impl Page {
self
}
/// Modifica la fuente de idioma de la página ([`Context::with_langid()`]).
#[builder_fn]
pub fn with_langid(mut self, language: &impl LangId) -> Self {
self.context.alter_langid(language);
self
}
/// Modifica el tema que se usará para renderizar la página ([`Context::with_theme()`]).
#[builder_fn]
pub fn with_theme(mut self, theme_name: &'static str) -> Self {
self.context.alter_theme(theme_name);
self
}
/// Modifica la composición para renderizar la página ([`Context::with_layout()`]).
#[builder_fn]
pub fn with_layout(mut self, layout_name: &'static str) -> Self {
self.context.alter_layout(layout_name);
self
}
/// Define los recursos de la página usando [`AssetsOp`].
#[builder_fn]
pub fn with_assets(mut self, op: AssetsOp) -> Self {
self.context.alter_assets(op);
self
}
#[builder_fn]
pub fn with_param<T: 'static>(mut self, key: &'static str, value: T) -> Self {
self.context.alter_param(key, value);
self
}
/// Establece el atributo `id` del elemento `<body>`.
#[builder_fn]
pub fn with_body_id(mut self, id: impl AsRef<str>) -> Self {
@ -172,6 +205,25 @@ impl Page {
&self.properties
}
/// Devuelve la solicitud HTTP asociada.
pub fn request(&self) -> Option<&HttpRequest> {
self.context.request()
}
/// Devuelve el tema que se usará para renderizar la página.
pub fn theme(&self) -> ThemeRef {
self.context.theme()
}
/// Devuelve la composición para renderizar la página. Por defecto es `"default"`.
pub fn layout(&self) -> &str {
self.context.layout()
}
pub fn param<T: 'static>(&self, key: &'static str) -> Option<&T> {
self.context.param(key)
}
/// Devuelve el identificador del elemento `<body>`.
pub fn body_id(&self) -> &AttrId {
&self.body_id
@ -181,19 +233,19 @@ impl Page {
pub fn body_classes(&self) -> &AttrClasses {
&self.body_classes
}
/// Devuelve una referencia mutable al [`Context`] de la página.
///
/// El [`Context`] actúa como intermediario para muchos métodos de `Page` (idioma, tema,
/// *layout*, recursos, solicitud HTTP, etc.). Resulta especialmente útil cuando un componente
/// o un tema necesita recibir el contexto como parámetro.
pub fn context(&mut self) -> &mut Context {
&mut self.context
}
/*
/// Devuelve una referencia mutable al [`Context`] de la página.
///
/// El [`Context`] actúa como intermediario para muchos métodos de `Page` (idioma, tema,
/// *layout*, recursos, solicitud HTTP, etc.). Resulta especialmente útil cuando un componente
/// o un tema necesita recibir el contexto como parámetro.
pub fn context(&mut self) -> &mut Context {
&mut self.context
}
*/
// Page RENDER *********************************************************************************
/// Renderiza los componentes de una región (`region_name`) de la página.
/// Renderiza los componentes de una región (`regiona_name`) de la página.
pub fn render_region(&mut self, region_name: &'static str) -> Markup {
self.regions
.merge_all_components(self.context.theme(), region_name)
@ -250,79 +302,3 @@ impl LangId for Page {
self.context.langid()
}
}
impl Contextual for Page {
// Contextual BUILDER **************************************************************************
#[builder_fn]
fn with_request(mut self, request: Option<HttpRequest>) -> Self {
self.context.alter_request(request);
self
}
#[builder_fn]
fn with_langid(mut self, language: &impl LangId) -> Self {
self.context.alter_langid(language);
self
}
#[builder_fn]
fn with_theme(mut self, theme_name: &'static str) -> Self {
self.context.alter_theme(theme_name);
self
}
#[builder_fn]
fn with_layout(mut self, layout_name: &'static str) -> Self {
self.context.alter_layout(layout_name);
self
}
#[builder_fn]
fn with_param<T: 'static>(mut self, key: &'static str, value: T) -> Self {
self.context.alter_param(key, value);
self
}
#[builder_fn]
fn with_assets(mut self, op: AssetsOp) -> Self {
self.context.alter_assets(op);
self
}
// Contextual GETTERS **************************************************************************
fn request(&self) -> Option<&HttpRequest> {
self.context.request()
}
fn theme(&self) -> ThemeRef {
self.context.theme()
}
fn layout(&self) -> &str {
self.context.layout()
}
fn param<T: 'static>(&self, key: &'static str) -> Option<&T> {
self.context.param(key)
}
fn favicon(&self) -> Option<&Favicon> {
self.context.favicon()
}
fn stylesheets(&self) -> &Assets<StyleSheet> {
self.context.stylesheets()
}
fn javascripts(&self) -> &Assets<JavaScript> {
self.context.javascripts()
}
// Contextual HELPERS **************************************************************************
fn required_id<T>(&mut self, id: Option<String>) -> String {
self.context.required_id::<T>(id)
}
}

View file

@ -1,5 +1,4 @@
use crate::base::component::Html;
use crate::html::Contextual;
use crate::locale::L10n;
use crate::response::ResponseError;
use crate::service::http::{header::ContentType, StatusCode};