⚡️ (core): Registro de acciones inmutable

This commit is contained in:
Manuel Cillero 2026-09-26 00:56:30 +02:00
parent 5b2ddeb6cc
commit 58d1fef4f1
15 changed files with 624 additions and 244 deletions

View file

@ -117,10 +117,7 @@ impl DeclareAdminSections {
}
pub(crate) fn dispatch(bag: &mut SectionBag) {
dispatch_actions(
&ActionKey::new(UniqueId::of::<Self>(), None, None),
|action: &Self| (action.f)(bag),
);
dispatch_actions(|action: &Self| (action.f)(bag));
}
}
@ -173,10 +170,7 @@ impl DeclareAdminPages {
}
pub(crate) fn dispatch(bag: &mut PageBag) {
dispatch_actions(
&ActionKey::new(UniqueId::of::<Self>(), None, None),
|action: &Self| (action.f)(bag),
);
dispatch_actions(|action: &Self| (action.f)(bag));
}
}
@ -229,10 +223,7 @@ impl DeclareAdminTasks {
}
pub(crate) fn dispatch(bag: &mut TaskBag) {
dispatch_actions(
&ActionKey::new(UniqueId::of::<Self>(), None, None),
|action: &Self| (action.f)(bag),
);
dispatch_actions(|action: &Self| (action.f)(bag));
}
}
@ -283,9 +274,6 @@ impl DeclareAdminActions {
}
pub(crate) fn dispatch(bag: &mut ActionBag) {
dispatch_actions(
&ActionKey::new(UniqueId::of::<Self>(), None, None),
|action: &Self| (action.f)(bag),
);
dispatch_actions(|action: &Self| (action.f)(bag));
}
}

View file

@ -58,10 +58,7 @@ impl DeclarePermissions {
/// Despacha todas las acciones `DeclarePermissions` registradas construyendo el catálogo.
pub(crate) fn dispatch(registry: &mut PermissionRegistry) {
dispatch_actions(
&ActionKey::new(UniqueId::of::<Self>(), None, None),
|action: &Self| (action.handler)(registry),
);
dispatch_actions(|action: &Self| (action.handler)(registry));
}
}

View file

@ -16,11 +16,11 @@
//!
//! [`Context`]: crate::core::component::Context
use crate::core::action::{ActionDispatcher, ActionKey, try_dispatch_actions};
use crate::core::action::{ActionDispatcher, try_dispatch_actions};
use crate::locale::Lc;
use crate::response::ErrorPage;
use crate::web::HttpRequest;
use crate::{CowStr, UniqueId, Weight};
use crate::{CowStr, Weight};
// **< CurrentUser >********************************************************************************
@ -205,17 +205,14 @@ impl CheckPermission {
#[inline]
pub(crate) fn check(request: &HttpRequest, perm: PermissionRef) -> bool {
let mut granted = false;
try_dispatch_actions(
&ActionKey::new(UniqueId::of::<Self>(), None, None),
|action: &Self| {
(action.f)(request, perm, &mut granted);
if granted {
std::ops::ControlFlow::Break(())
} else {
std::ops::ControlFlow::Continue(())
}
},
);
try_dispatch_actions(|action: &Self| {
(action.f)(request, perm, &mut granted);
if granted {
std::ops::ControlFlow::Break(())
} else {
std::ops::ControlFlow::Continue(())
}
});
granted
}
}

View file

@ -5,19 +5,14 @@ use super::FnActionWithComponent;
/// Ejecuta [`FnActionWithComponent`] después de renderizar un componente.
pub struct AfterRender<C: Component> {
f: FnActionWithComponent<C>,
referer_type_id: Option<UniqueId>,
referer_id: Option<String>,
referer: ActionReferer,
weight: Weight,
}
// Filtro para despachar `FnActionWithComponent` después de renderizar un componente `C`.
impl<C: Component> ActionDispatcher for AfterRender<C> {
fn referer_type_id(&self) -> Option<UniqueId> {
self.referer_type_id
}
fn referer_id(&self) -> Option<String> {
self.referer_id.clone()
fn referer(&self) -> Option<&ActionReferer> {
Some(&self.referer)
}
fn weight(&self) -> Weight {
@ -30,8 +25,7 @@ impl<C: Component> AfterRender<C> {
pub fn new(f: FnActionWithComponent<C>) -> Self {
AfterRender {
f,
referer_type_id: Some(UniqueId::of::<C>()),
referer_id: None,
referer: ActionReferer::of::<C>(),
weight: 0,
}
}
@ -39,7 +33,7 @@ impl<C: Component> AfterRender<C> {
/// Afina el registro para ejecutar la acción [`FnActionWithComponent`] sólo para el componente
/// `C` con identificador `id`.
pub fn filter_by_referer_id(mut self, id: impl AsRef<str>) -> Self {
self.referer_id = util::normalize_token(id);
self.referer = self.referer.with_id(id);
self
}
@ -52,18 +46,6 @@ impl<C: Component> AfterRender<C> {
/// Despacha las acciones.
#[inline]
pub(crate) fn dispatch(component: &mut C, cx: &mut Context) {
// Primero despacha las acciones para el tipo de componente.
dispatch_actions(
&ActionKey::new(UniqueId::of::<Self>(), Some(UniqueId::of::<C>()), None),
|action: &Self| (action.f)(component, cx),
);
// Y luego despacha las acciones para el tipo de componente con un identificador dado.
if let Some(id) = component.id() {
dispatch_actions(
&ActionKey::new(UniqueId::of::<Self>(), Some(UniqueId::of::<C>()), Some(id)),
|action: &Self| (action.f)(component, cx),
);
}
dispatch_referer(component, |c| c.id(), |action: &Self, c| (action.f)(c, cx));
}
}

View file

@ -5,19 +5,14 @@ use super::FnActionWithComponent;
/// Ejecuta [`FnActionWithComponent`] antes de renderizar el componente.
pub struct BeforeRender<C: Component> {
f: FnActionWithComponent<C>,
referer_type_id: Option<UniqueId>,
referer_id: Option<String>,
referer: ActionReferer,
weight: Weight,
}
// Filtro para despachar `FnActionWithComponent` antes de renderizar un componente `C`.
impl<C: Component> ActionDispatcher for BeforeRender<C> {
fn referer_type_id(&self) -> Option<UniqueId> {
self.referer_type_id
}
fn referer_id(&self) -> Option<String> {
self.referer_id.clone()
fn referer(&self) -> Option<&ActionReferer> {
Some(&self.referer)
}
fn weight(&self) -> Weight {
@ -30,8 +25,7 @@ impl<C: Component> BeforeRender<C> {
pub fn new(f: FnActionWithComponent<C>) -> Self {
BeforeRender {
f,
referer_type_id: Some(UniqueId::of::<C>()),
referer_id: None,
referer: ActionReferer::of::<C>(),
weight: 0,
}
}
@ -39,7 +33,7 @@ impl<C: Component> BeforeRender<C> {
/// Afina el registro para ejecutar la acción [`FnActionWithComponent`] sólo para el componente
/// `C` con identificador `id`.
pub fn filter_by_referer_id(mut self, id: impl AsRef<str>) -> Self {
self.referer_id = util::normalize_token(id);
self.referer = self.referer.with_id(id);
self
}
@ -52,18 +46,6 @@ impl<C: Component> BeforeRender<C> {
/// Despacha las acciones.
#[inline]
pub(crate) fn dispatch(component: &mut C, cx: &mut Context) {
// Primero despacha las acciones para el tipo de componente.
dispatch_actions(
&ActionKey::new(UniqueId::of::<Self>(), Some(UniqueId::of::<C>()), None),
|action: &Self| (action.f)(component, cx),
);
// Y luego despacha las aciones para el tipo de componente con un identificador dado.
if let Some(id) = component.id() {
dispatch_actions(
&ActionKey::new(UniqueId::of::<Self>(), Some(UniqueId::of::<C>()), Some(id)),
|action: &Self| (action.f)(component, cx),
);
}
dispatch_referer(component, |c| c.id(), |action: &Self, c| (action.f)(c, cx));
}
}

View file

@ -5,19 +5,14 @@ use super::FnActionTransformMarkup;
/// Ejecuta [`FnActionTransformMarkup`] para alterar el renderizado de componentes.
pub struct TransformMarkup<C: Component> {
f: FnActionTransformMarkup<C>,
referer_type_id: Option<UniqueId>,
referer_id: Option<String>,
referer: ActionReferer,
weight: Weight,
}
// Filtro para despachar `FnActionTransformMarkup` sobre el renderizado de un componente `C`.
impl<C: Component> ActionDispatcher for TransformMarkup<C> {
fn referer_type_id(&self) -> Option<UniqueId> {
self.referer_type_id
}
fn referer_id(&self) -> Option<String> {
self.referer_id.clone()
fn referer(&self) -> Option<&ActionReferer> {
Some(&self.referer)
}
fn weight(&self) -> Weight {
@ -30,8 +25,7 @@ impl<C: Component> TransformMarkup<C> {
pub fn new(f: FnActionTransformMarkup<C>) -> Self {
TransformMarkup {
f,
referer_type_id: Some(UniqueId::of::<C>()),
referer_id: None,
referer: ActionReferer::of::<C>(),
weight: 0,
}
}
@ -39,7 +33,7 @@ impl<C: Component> TransformMarkup<C> {
/// Afina el registro para ejecutar la acción [`FnActionTransformMarkup`] sólo para el
/// componente `C` con identificador `id`.
pub fn filter_by_referer_id(mut self, id: impl AsRef<str>) -> Self {
self.referer_id = util::normalize_token(id);
self.referer = self.referer.with_id(id);
self
}
@ -51,29 +45,16 @@ impl<C: Component> TransformMarkup<C> {
/// Despacha las acciones encadenando el [`Markup`] entre cada una.
#[inline]
pub(crate) fn dispatch(component: &C, cx: &Context, markup: Markup) -> Markup {
pub(crate) fn dispatch(component: &mut C, cx: &Context, markup: Markup) -> Markup {
let mut output = markup;
// Primero despacha las acciones para el tipo de componente.
dispatch_actions(
&ActionKey::new(UniqueId::of::<Self>(), Some(UniqueId::of::<C>()), None),
|action: &Self| {
dispatch_referer(
component,
|c| c.id(),
|action: &Self, c| {
let taken = std::mem::replace(&mut output, html! {});
output = (action.f)(component, cx, taken);
output = (action.f)(c, cx, taken);
},
);
// Y luego despacha las acciones para el tipo de componente con un identificador dado.
if let Some(id) = component.id() {
dispatch_actions(
&ActionKey::new(UniqueId::of::<Self>(), Some(UniqueId::of::<C>()), Some(id)),
|action: &Self| {
let taken = std::mem::replace(&mut output, html! {});
output = (action.f)(component, cx, taken);
},
);
}
output
}
}

View file

@ -37,9 +37,6 @@ impl AfterRenderBody {
/// Despacha las acciones.
#[inline]
pub(crate) fn dispatch(page: &mut Page) {
dispatch_actions(
&ActionKey::new(UniqueId::of::<Self>(), None, None),
|action: &Self| (action.f)(page),
);
dispatch_actions(|action: &Self| (action.f)(page));
}
}

View file

@ -37,9 +37,6 @@ impl BeforeRenderBody {
/// Despacha las acciones.
#[inline]
pub(crate) fn dispatch(page: &mut Page) {
dispatch_actions(
&ActionKey::new(UniqueId::of::<Self>(), None, None),
|action: &Self| (action.f)(page),
);
dispatch_actions(|action: &Self| (action.f)(page));
}
}

View file

@ -5,14 +5,14 @@
//! aplicación.
mod definition;
pub use definition::{ActionBox, ActionDispatcher, ActionKey};
pub use definition::{ActionBox, ActionDispatcher, ActionReferer};
mod list;
use list::ActionsList;
mod all;
pub(crate) use all::add_action;
pub use all::{dispatch_actions, try_dispatch_actions};
pub(crate) use all::publish_actions;
pub use all::{dispatch_actions, dispatch_referer, try_dispatch_actions};
// **< actions! >***********************************************************************************

View file

@ -1,46 +1,160 @@
use crate::core::action::{ActionBox, ActionDispatcher, ActionKey, ActionsList};
use parking_lot::RwLock;
use crate::UniqueId;
use crate::core::action::{ActionBox, ActionDispatcher, ActionReferer, ActionsList};
use std::collections::HashMap;
use std::sync::LazyLock;
use std::hash::{BuildHasherDefault, Hasher};
use std::sync::OnceLock;
// **< ACCIONES >***********************************************************************************
static ACTIONS: LazyLock<RwLock<HashMap<ActionKey, ActionsList>>> =
LazyLock::new(|| RwLock::new(HashMap::new()));
// Registro de acciones. Se construye una sola vez durante el arranque, al registrar las extensiones
// y antes de inicializarlas (`publish_actions()`). A partir de ahí es inmutable, por lo que
// despachar una acción no toma ningún bloqueo ni escribe en memoria compartida, evitando cualquier
// contención por más hilos que atiendan peticiones.
static ACTIONS: OnceLock<Registry> = OnceLock::new();
// **< AÑADIR ACCIONES >****************************************************************************
// Una clave del registro usa `TypeId`, que ya es un *hash* bien distribuido. No requiere SipHash.
type FastMap<K, V> = HashMap<K, V, BuildHasherDefault<FastHasher>>;
/// Registra una nueva acción en el sistema.
///
/// Si ya existen acciones con la misma `ActionKey`, la acción se añade a la misma lista. Si no, se
/// crea una nueva lista.
///
/// Las extensiones llamarán a esta función durante su inicialización para instalar acciones
/// personalizadas que modifiquen el comportamiento del *core* o de otros componentes.
pub(crate) fn add_action(action: ActionBox) {
let key = ActionKey::new(
action.type_id(),
action.referer_type_id(),
action.referer_id(),
);
let mut actions = ACTIONS.write();
if let Some(list) = actions.get_mut(&key) {
list.add(action);
} else {
let mut list = ActionsList::new();
list.add(action);
actions.insert(key, list);
struct Registry(FastMap<Slot, ActionEntry>);
// Tipo de acción y, si lo hay, tipo del referente (p. ej. el componente). Es `Copy`, así que
// buscarlo no reserva memoria; el identificador del referente se busca aparte, en `ActionEntry`.
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
struct Slot {
action_type_id: UniqueId,
referer_type_id: Option<UniqueId>,
}
// Acciones de un tipo para un tipo de referente: las generales y, aparte, las filtradas por
// identificador. Es de sólo lectura y se obtiene ya construida del registro, sin bloqueos.
#[derive(Default)]
pub(crate) struct ActionEntry {
general: ActionsList,
by_id: HashMap<String, ActionsList>,
}
impl ActionEntry {
// Acciones que se aplican a cualquier referente del tipo.
pub(crate) fn general(&self) -> &ActionsList {
&self.general
}
// Indica si hay alguna acción filtrada por identificador. Permite no calcular el identificador
// del referente (que puede reservar memoria) cuando no hay ninguna.
pub(crate) fn has_ids(&self) -> bool {
!self.by_id.is_empty()
}
// Acciones que sólo se aplican al referente con ese identificador.
pub(crate) fn with_id(&self, id: &str) -> Option<&ActionsList> {
self.by_id.get(id)
}
}
// **< DESPLEGAR ACCIONES >*************************************************************************
// Busca la entrada de un tipo de acción y de referente.
fn action_entry(
action_type_id: UniqueId,
referer_type_id: Option<UniqueId>,
) -> Option<&'static ActionEntry> {
ACTIONS.get()?.0.get(&Slot {
action_type_id,
referer_type_id,
})
}
/// Despacha y ejecuta las funciones asociadas a una [`ActionKey`].
// **< REGISTRAR ACCIONES >*************************************************************************
// Registra todas las acciones de las extensiones y congela el registro.
//
// Las extensiones instalan sus acciones durante el arranque, antes de atender ninguna petición, y
// sólo aquí. `collect` se ejecuta una única vez: si el registro ya está construido (p. ej. por otra
// llamada a `Application::prepare()` en el mismo proceso, como hacen las pruebas) no se vuelve a
// construir ni se duplican las acciones.
pub(crate) fn publish_actions(collect: impl FnOnce() -> Vec<ActionBox>) {
ACTIONS.get_or_init(|| {
let mut slots: FastMap<Slot, ActionEntry> = FastMap::default();
for action in collect() {
let referer = action.referer();
let slot = Slot {
action_type_id: action.type_id(),
referer_type_id: referer.map(ActionReferer::referer_type_id),
};
let entry = slots.entry(slot).or_default();
match referer.and_then(|r| r.id().map(str::to_owned)) {
None => entry.general.add(action),
Some(id) => entry.by_id.entry(id).or_default().add(action),
}
}
Registry(slots)
});
}
// Función de *hash* rápida para claves formadas por `TypeId` (multiplicación y rotación, como
// FxHash). No es apta para claves controladas por un atacante; aquí las claves son tipos.
#[derive(Default)]
struct FastHasher(u64);
impl FastHasher {
const SEED: u64 = 0x51_7c_c1_b7_27_22_0a_95;
#[inline]
fn add(&mut self, word: u64) {
self.0 = (self.0.rotate_left(5) ^ word).wrapping_mul(Self::SEED);
}
}
impl Hasher for FastHasher {
#[inline]
fn write(&mut self, bytes: &[u8]) {
let mut chunks = bytes.chunks_exact(8);
for chunk in &mut chunks {
self.add(u64::from_le_bytes(chunk.try_into().unwrap()));
}
let rest = chunks.remainder();
if !rest.is_empty() {
let mut last = [0u8; 8];
last[..rest.len()].copy_from_slice(rest);
self.add(u64::from_le_bytes(last));
}
}
#[inline]
fn write_u8(&mut self, n: u8) {
self.add(n as u64);
}
#[inline]
fn write_u64(&mut self, n: u64) {
self.add(n);
}
#[inline]
fn write_u128(&mut self, n: u128) {
self.add(n as u64);
self.add((n >> 64) as u64);
}
#[inline]
fn write_isize(&mut self, n: isize) {
self.add(n as u64);
}
#[inline]
fn finish(&self) -> u64 {
self.0.rotate_left(26)
}
}
// **< DESPACHAR ACCIONES >*************************************************************************
/// Despacha y ejecuta las funciones de las acciones de tipo `A` que no tienen referente.
///
/// Permite recorrer de forma segura y ordenada (por peso) la lista de funciones asociadas a una
/// acción específica.
/// Recorre de forma segura y ordenada (por peso) la lista de funciones registradas para esa acción.
///
/// Sólo alcanza las acciones que no declaran un [`ActionReferer`](super::ActionReferer) en
/// [`ActionDispatcher::referer()`]. Las que actúan sobre un tipo de objeto (y, opcionalmente, una
/// instancia concreta) se despachan con [`dispatch_referer()`].
///
/// # Parámetros genéricos
///
@ -50,28 +164,22 @@ pub(crate) fn add_action(action: ActionBox) {
/// # Ejemplo
///
/// ```rust,ignore
/// pub(crate) fn dispatch(component: &mut C, cx: &mut Context) {
/// dispatch_actions(
/// &ActionKey::new(
/// UniqueId::of::<Self>(),
/// Some(UniqueId::of::<C>()),
/// None,
/// ),
/// |action: &Self| (action.f)(component, cx),
/// );
/// pub(crate) fn dispatch(page: &mut Page) {
/// dispatch_actions(|action: &Self| (action.f)(page));
/// }
/// ```
pub fn dispatch_actions<A, F>(key: &ActionKey, f: F)
pub fn dispatch_actions<A, F>(f: F)
where
A: ActionDispatcher,
F: FnMut(&A),
{
if let Some(list) = ACTIONS.read().get(key) {
list.for_each(f);
if let Some(entry) = action_entry(UniqueId::of::<A>(), None) {
entry.general().for_each(f);
}
}
/// Despacha las funciones asociadas a una [`ActionKey`] con posible salida anticipada.
/// Despacha las funciones de las acciones de tipo `A` que no tienen referente, con posible salida
/// anticipada.
///
/// Funciona igual que [`dispatch_actions`], pero el closure puede devolver
/// [`std::ops::ControlFlow::Continue`] para continuar ejecutando la siguiente acción; o
@ -82,26 +190,70 @@ where
/// ```rust,ignore
/// pub(crate) fn check(cx: &Context, key: &str) -> bool {
/// let mut granted = false;
/// try_dispatch_actions(
/// &ActionKey::new(UniqueId::of::<Self>(), None, None),
/// |action: &Self| {
/// (action.f)(cx, key, &mut granted);
/// if granted {
/// std::ops::ControlFlow::Break(())
/// } else {
/// std::ops::ControlFlow::Continue(())
/// }
/// },
/// );
/// try_dispatch_actions(|action: &Self| {
/// (action.f)(cx, key, &mut granted);
/// if granted {
/// std::ops::ControlFlow::Break(())
/// } else {
/// std::ops::ControlFlow::Continue(())
/// }
/// });
/// granted
/// }
/// ```
pub fn try_dispatch_actions<A, F>(key: &ActionKey, f: F)
pub fn try_dispatch_actions<A, F>(f: F)
where
A: ActionDispatcher,
F: FnMut(&A) -> std::ops::ControlFlow<()>,
{
if let Some(list) = ACTIONS.read().get(key) {
list.try_for_each(f);
if let Some(entry) = action_entry(UniqueId::of::<A>(), None) {
entry.general().try_for_each(f);
}
}
/// Despacha las funciones de las acciones de tipo `A` asociadas a un referente de tipo `R` (p. ej.
/// un componente).
///
/// Se aplican primero las acciones del tipo `R` y después las que sólo afectan al referente con el
/// identificador que devuelva `id`, cada lista ordenada por peso. Sólo se llama a `id` (que suele
/// reservar memoria) si hay alguna acción filtrada por identificador para `R`.
///
/// El referente se presta a `id` y a `f` por turnos, así que `f` puede modificarlo.
///
/// # Parámetros genéricos
///
/// - `A`: Tipo de acción que esperamos procesar. Debe implementar [`ActionDispatcher`].
/// - `R`: Tipo del referente, el mismo que declara [`ActionDispatcher::referer()`] en su
/// [`ActionReferer`].
///
/// # Ejemplo
///
/// ```rust,ignore
/// pub(crate) fn dispatch(component: &mut C, cx: &mut Context) {
/// dispatch_referer(
/// component,
/// |c| c.id(),
/// |action: &Self, c| (action.f)(c, cx),
/// );
/// }
/// ```
pub fn dispatch_referer<A, R>(
referer: &mut R,
id: impl FnOnce(&R) -> Option<String>,
mut f: impl FnMut(&A, &mut R),
) where
A: ActionDispatcher,
R: 'static,
{
// Sin ninguna acción registrada para este tipo de referente no hay nada que hacer.
let Some(entry) = action_entry(UniqueId::of::<A>(), Some(UniqueId::of::<R>())) else {
return;
};
entry.general().for_each(|action: &A| f(action, referer));
if entry.has_ids()
&& let Some(id) = id(referer)
&& let Some(list) = entry.with_id(&id)
{
list.for_each(|action: &A| f(action, referer));
}
}

View file

@ -1,57 +1,68 @@
use crate::core::AnyInfo;
use crate::{UniqueId, Weight};
use crate::{Getters, UniqueId, Weight, util};
/// Tipo dinámico para encapsular cualquier acción que implementa [`ActionDispatcher`].
pub type ActionBox = Box<dyn ActionDispatcher>;
/// Clave para registrar las acciones y seleccionar las funciones asociadas.
/// Referente de una acción: el tipo de objeto sobre el que actúa (p. ej. un tipo de componente) y,
/// opcionalmente, el identificador de una instancia concreta.
///
/// Las funciones seleccionadas se van a [despachar](crate::core::action::dispatch_actions) y
/// ejecutar en un punto concreto del flujo de ejecución.
#[derive(Eq, PartialEq, Hash)]
pub struct ActionKey {
action_type_id: UniqueId,
referer_type_id: Option<UniqueId>,
referer_id: Option<String>,
/// Las acciones con referente se despachan con [`dispatch_referer()`]. Sin identificador afectan a
/// cualquier objeto del tipo; con identificador, sólo al que lo tiene.
///
/// # Ejemplo
///
/// ```rust
/// # use pagetop::prelude::*;
/// let any_button = ActionReferer::of::<Button>();
/// assert_eq!(any_button.id(), None);
///
/// // El identificador se normaliza; uno en blanco equivale a no tenerlo.
/// let one = ActionReferer::of::<Button>().with_id(" My Id ");
/// assert_eq!(one.id(), Some("my_id"));
/// assert_eq!(one.referer_type_id(), any_button.referer_type_id());
/// assert_eq!(one.with_id(" "), any_button);
/// ```
///
/// [`dispatch_referer()`]: crate::core::action::dispatch_referer
#[derive(Clone, Debug, Eq, Getters, PartialEq)]
pub struct ActionReferer {
#[getters(copy)]
referer_type_id: UniqueId,
#[getters(skip)]
id: Option<String>,
}
impl ActionKey {
/// Crea una nueva clave para un tipo de acción.
///
/// Se crea con los siguientes campos:
///
/// - `action_type_id`: Tipo de la acción.
/// - `referer_type_id`: Opcional, identificador de tipo ([`UniqueId`]) del componente referido.
/// - `referer_id`: Opcional, identificador de la instancia (p. ej. para asociar la acción a un
/// componente concreto).
///
/// Esta clave permitirá seleccionar las funciones a ejecutar para ese tipo de acción, con
/// filtros opcionales por componente o por una instancia concreta según su identificador.
pub fn new(
action_type_id: UniqueId,
referer_type_id: Option<UniqueId>,
referer_id: Option<String>,
) -> Self {
ActionKey {
action_type_id,
referer_type_id,
referer_id,
impl ActionReferer {
/// Referente para cualquier objeto del tipo `R`.
pub fn of<R: 'static>() -> Self {
ActionReferer {
referer_type_id: UniqueId::of::<R>(),
id: None,
}
}
}
/// Implementa el filtro predeterminado para despachar las funciones de una acción dada.
///
/// Las acciones tienen que sobrescribir los métodos para el filtro que apliquen. Por defecto
/// implementa un filtro nulo.
pub trait ActionDispatcher: AnyInfo + Send + Sync {
/// Devuelve el identificador de tipo ([`UniqueId`]) del objeto referido.
fn referer_type_id(&self) -> Option<UniqueId> {
None
/// Identificador de la instancia a la que se restringe el referente, si lo hay.
pub fn id(&self) -> Option<&str> {
self.id.as_deref()
}
/// Devuelve el identificador del objeto referido.
fn referer_id(&self) -> Option<String> {
/// Restringe el referente al objeto con el identificador indicado. Si el identificador queda
/// vacío tras normalizarlo, el referente vuelve a aplicarse a cualquier objeto del tipo.
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
self.id = util::normalize_token(id);
self
}
}
/// Define el comportamiento de una acción con su referente y su peso de ejecución.
///
/// Las acciones sobrescriben [`referer()`](Self::referer) si sólo se aplican a un tipo de objeto
/// (y, opcionalmente, a una instancia concreta). Por defecto no tienen referente.
pub trait ActionDispatcher: AnyInfo + Send + Sync {
/// Devuelve el [`ActionReferer`] de la acción, o `None` si no actúa sobre un tipo de objeto
/// concreto.
fn referer(&self) -> Option<&ActionReferer> {
None
}

View file

@ -3,20 +3,19 @@ use crate::core::AnyCast;
use crate::core::action::{ActionBox, ActionDispatcher};
use crate::trace;
use parking_lot::RwLock;
// Lista de acciones, ordenada por peso.
//
// Se construye al registrar las acciones, durante el arranque. A partir de ahí es de sólo lectura y
// recorrerla no aplica ningún bloqueo (ver `ActionEntry`).
#[derive(AutoDefault)]
pub struct ActionsList(RwLock<Vec<ActionBox>>);
pub struct ActionsList(Vec<ActionBox>);
impl ActionsList {
pub fn new() -> Self {
Self::default()
}
// Añade la acción y mantiene la lista ordenada por peso. La ordenación es estable: a igual peso
// se conserva el orden de registro.
pub fn add(&mut self, action: ActionBox) {
let mut list = self.0.write();
list.push(action);
list.sort_by_key(|a| a.weight());
self.0.push(action);
self.0.sort_by_key(|a| a.weight());
}
pub fn for_each<A, F>(&self, mut f: F)
@ -24,8 +23,7 @@ impl ActionsList {
A: ActionDispatcher,
F: FnMut(&A),
{
let list = self.0.read();
for a in list.iter() {
for a in self.0.iter() {
if let Some(action) = (**a).downcast_ref::<A>() {
f(action);
} else {
@ -39,8 +37,7 @@ impl ActionsList {
A: ActionDispatcher,
F: FnMut(&A) -> std::ops::ControlFlow<()>,
{
let list = self.0.read();
for a in list.iter() {
for a in self.0.iter() {
if let Some(action) = (**a).downcast_ref::<A>() {
if f(action).is_break() {
break;

View file

@ -1,4 +1,4 @@
use crate::core::action::add_action;
use crate::core::action::publish_actions;
use crate::core::extension::ExtensionRef;
use crate::core::theme::ThemeRef;
use crate::core::theme::all::THEMES;
@ -91,11 +91,14 @@ fn check_theme_parent_chain(theme: ThemeRef) {
// **< REGISTRO DE LAS ACCIONES >*******************************************************************
pub fn register_actions() {
for extension in EXTENSIONS.get().into_iter().flatten() {
for a in extension.actions() {
add_action(a);
}
}
publish_actions(|| {
EXTENSIONS
.get()
.into_iter()
.flatten()
.flat_map(|extension| extension.actions())
.collect()
});
}
// **< INICIALIZA LAS EXTENSIONES >*****************************************************************

View file

@ -101,9 +101,17 @@ pub trait Extension: AnyInfo + Send + Sync {
/// Devuelve la lista de acciones que la extensión registra.
///
/// Estas [acciones](crate::core::action) se despachan por orden de registro o por
/// [peso](crate::Weight) (ver [`actions!`](crate::actions)), permitiendo
/// personalizar el comportamiento de la aplicación en puntos específicos.
/// Estas [acciones] se despachan por orden de registro o por [peso] (ver [`actions!`]),
/// permitiendo personalizar el comportamiento de la aplicación en puntos específicos.
///
/// PageTop lo invoca una sola vez por proceso, al registrar las extensiones y antes de
/// [`initialize()`]. La lista queda fija a partir de ahí y no es posible añadir acciones más
/// tarde.
///
/// [acciones]: crate::core::action
/// [peso]: crate::Weight
/// [`actions!`]: crate::actions
/// [`initialize()`]: Self::initialize
fn actions(&self) -> Vec<ActionBox> {
actions![]
}

288
tests/action_dispatch.rs Normal file
View file

@ -0,0 +1,288 @@
use pagetop::prelude::*;
use std::sync::Mutex;
// The action registry is built once per process, at startup: all the actions of this test are
// declared in a single extension and the cases are different tests on top of it.
// Test action without referer, with configurable weight.
struct Probe {
name: &'static str,
weight: Weight,
}
impl ActionDispatcher for Probe {
fn weight(&self) -> Weight {
self.weight
}
}
// Test referers: `Node` has actions filtered by id; `Plain` only has general ones.
struct Node(&'static str);
struct Plain;
// Test action with referer `R`, with configurable weight and referer id.
struct Tagged {
name: &'static str,
weight: Weight,
referer: ActionReferer,
}
impl Tagged {
fn new<R: 'static>(name: &'static str, weight: Weight, referer_id: Option<&str>) -> Self {
let referer = ActionReferer::of::<R>();
Tagged {
name,
weight,
referer: match referer_id {
Some(id) => referer.with_id(id),
None => referer,
},
}
}
}
impl ActionDispatcher for Tagged {
fn referer(&self) -> Option<&ActionReferer> {
Some(&self.referer)
}
fn weight(&self) -> Weight {
self.weight
}
}
// Another action type, with nothing registered.
struct Unregistered;
impl ActionDispatcher for Unregistered {}
// Ids of the components the `BeforeRender` actions were applied to. Tests run concurrently and
// render the same component types, so they are checked by id and not with a shared counter.
static GENERAL_LOG: Mutex<Vec<String>> = Mutex::new(Vec::new());
static ID_LOG: Mutex<Vec<String>> = Mutex::new(Vec::new());
// Log of the transformations applied: (component id, which one).
static MARKUP_LOG: Mutex<Vec<(String, &'static str)>> = Mutex::new(Vec::new());
struct Ext;
#[async_trait]
impl Extension for Ext {
fn actions(&self) -> Vec<ActionBox> {
let probe = |name, weight| Probe { name, weight };
actions![
// Registered out of order: dispatch delivers them by weight and, at equal weight, in
// registration order.
probe("c-late", 10),
probe("a-early", -5),
probe("b-first-of-zero", 0),
probe("d-second-of-zero", 0),
// With referer: the ones for the type and, separately, the ones filtered by id.
Tagged::new::<Node>("general", 0, None),
Tagged::new::<Node>("x-2", 3, Some("x")),
Tagged::new::<Node>("x-1", 1, Some("x")),
Tagged::new::<Node>("y-1", 0, Some("y")),
Tagged::new::<Plain>("plain", 0, None),
action::component::BeforeRender::<Badge>::new(|badge, _cx| {
GENERAL_LOG
.lock()
.unwrap()
.push(badge.id().unwrap_or_default());
}),
action::component::BeforeRender::<Badge>::new(|badge, _cx| {
ID_LOG.lock().unwrap().push(badge.id().unwrap_or_default());
})
.filter_by_referer_id("special-before"),
action::component::TransformMarkup::<Badge>::new(|badge, _cx, markup| {
let id = badge.id().unwrap_or_default();
MARKUP_LOG.lock().unwrap().push((id, "general"));
html! { div { (markup) } }
}),
action::component::TransformMarkup::<Badge>::new(|badge, _cx, markup| {
let id = badge.id().unwrap_or_default();
MARKUP_LOG.lock().unwrap().push((id, "by-id"));
html! { section { (markup) } }
})
.filter_by_referer_id("special-markup"),
]
}
}
async fn start() {
// Starting more than once in the same process (as tests do) does not duplicate the actions.
let _ = Application::prepare(&Ext).await;
let _ = Application::prepare(&Ext).await;
}
fn names() -> Vec<&'static str> {
let mut seen = Vec::new();
dispatch_actions(|action: &Probe| seen.push(action.name));
seen
}
// Names of the `Node` actions dispatched for a referer with that id, and how many times the id was
// requested.
fn node_names(id: &'static str) -> (Vec<&'static str>, usize) {
let mut seen = Vec::new();
let mut id_calls = 0;
dispatch_referer(
&mut Node(id),
|node| {
id_calls += 1;
Some(node.0.to_owned())
},
|action: &Tagged, _| seen.push(action.name),
);
(seen, id_calls)
}
#[pagetop::test]
async fn actions_are_dispatched_by_weight_then_registration_order() {
start().await;
assert_eq!(
names(),
["a-early", "b-first-of-zero", "d-second-of-zero", "c-late"]
);
}
#[pagetop::test]
async fn referer_actions_run_for_the_type_then_for_the_matching_id() {
start().await;
// First the ones for the type; then the ones filtered by id, each list sorted by weight.
assert_eq!(node_names("x").0, ["general", "x-1", "x-2"]);
assert_eq!(node_names("y").0, ["general", "y-1"]);
// An id without actions only gets the ones for the type.
assert_eq!(node_names("nobody").0, ["general"]);
}
#[pagetop::test]
async fn the_id_is_only_requested_if_some_action_is_filtered_by_it() {
start().await;
assert_eq!(node_names("x").1, 1);
// `Plain` has no filtered action: there is no need to compute the id.
let mut seen = Vec::new();
let mut id_calls = 0;
dispatch_referer(
&mut Plain,
|_| {
id_calls += 1;
None
},
|action: &Tagged, _| seen.push(action.name),
);
assert_eq!(seen, ["plain"]);
assert_eq!(id_calls, 0);
}
#[pagetop::test]
async fn an_action_with_nothing_registered_dispatches_nothing() {
start().await;
let mut calls = 0;
dispatch_actions(|_: &Unregistered| calls += 1);
// Not even with a referer type that does not exist.
let mut id_calls = 0;
dispatch_referer(
&mut String::new(),
|_| {
id_calls += 1;
None
},
|_: &Tagged, _| calls += 1,
);
assert_eq!((calls, id_calls), (0, 0));
}
#[pagetop::test]
async fn try_dispatch_stops_when_asked() {
start().await;
let mut seen = Vec::new();
try_dispatch_actions(|action: &Probe| {
seen.push(action.name);
if action.name == "b-first-of-zero" {
std::ops::ControlFlow::Break(())
} else {
std::ops::ControlFlow::Continue(())
}
});
assert_eq!(seen, ["a-early", "b-first-of-zero"]);
}
// Component actions: the ones for the type apply to all of them; the ones filtered by id only to
// the component with that id.
#[pagetop::test]
async fn component_actions_run_for_the_type_and_for_the_matching_id_only() {
start().await;
let mut cx = Context::default();
let count = |log: &Mutex<Vec<String>>, id: &str| {
log.lock()
.unwrap()
.iter()
.filter(|logged| *logged == id)
.count()
};
let mut other = Badge::labeled(Lc::n("other")).with_id("before-other");
let _ = other.render(&mut cx).await;
assert_eq!(count(&GENERAL_LOG, "before-other"), 1);
assert_eq!(
count(&ID_LOG, "before-other"),
0,
"other id: the filtered one is not applied"
);
let mut special = Badge::labeled(Lc::n("special")).with_id("special-before");
let _ = special.render(&mut cx).await;
assert_eq!(count(&GENERAL_LOG, "special-before"), 1);
assert_eq!(
count(&ID_LOG, "special-before"),
1,
"same id: the filtered one is applied"
);
// Without an id only the general one applies, and the filtered one is applied to no one else.
let mut plain = Badge::labeled(Lc::n("plain"));
let _ = plain.render(&mut cx).await;
assert!(
ID_LOG
.lock()
.unwrap()
.iter()
.all(|logged| logged == "special-before")
);
// Another component type gets nothing.
let mut button = Button::plain(Lc::n("x")).with_id("before-button");
let _ = button.render(&mut cx).await;
assert_eq!(count(&GENERAL_LOG, "before-button"), 0);
}
// Each transformation receives the result of the previous one: first the ones for the type, then
// the ones for the id.
#[pagetop::test]
async fn markup_transformations_chain_in_order() {
start().await;
let mut cx = Context::default();
// With another id only the general one applies.
let mut other = Badge::labeled(Lc::n("chained")).with_id("chained-other");
let html = other.render(&mut cx).await.into_string();
assert!(html.starts_with("<div>"), "{html}");
assert!(!html.contains("<section>"), "{html}");
// With the id of the filtered action, after the general one.
let mut special = Badge::labeled(Lc::n("chained")).with_id("special-markup");
let html = special.render(&mut cx).await.into_string();
assert!(html.starts_with("<section><div>"), "{html}");
assert!(html.ends_with("</div></section>"), "{html}");
// Each one was applied once and in that order (the log is shared with the other tests, which
// run concurrently: it is filtered by id).
let log = MARKUP_LOG.lock().unwrap();
let applied = |id: &str| -> Vec<&'static str> {
log.iter()
.filter(|(logged, _)| logged == id)
.map(|(_, which)| *which)
.collect()
};
assert_eq!(applied("chained-other"), ["general"]);
assert_eq!(applied("special-markup"), ["general", "by-id"]);
}