⚡️ (core): Registro de acciones inmutable
This commit is contained in:
parent
5b2ddeb6cc
commit
58d1fef4f1
15 changed files with 624 additions and 244 deletions
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
23
src/auth.rs
23
src/auth.rs
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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! >***********************************************************************************
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 >*****************************************************************
|
||||
|
|
|
|||
|
|
@ -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
288
tests/action_dispatch.rs
Normal 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"]);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue