diff --git a/src/app.rs b/src/app.rs index 0bcece8..b6b5a71 100644 --- a/src/app.rs +++ b/src/app.rs @@ -56,6 +56,9 @@ impl Application { // Registra las extensiones de la aplicación. extension::all::register_extensions(root_extension); + // Registra las acciones de las extensiones. + extension::all::register_actions(); + // Inicializa las extensiones. extension::all::initialize_extensions(); diff --git a/src/core.rs b/src/core.rs index 9f3d7b5..4b857fc 100644 --- a/src/core.rs +++ b/src/core.rs @@ -1,4 +1,4 @@ -//! Tipos y funciones esenciales para crear extensiones y temas. +//! Tipos y funciones esenciales para crear acciones, extensiones y temas. use std::any::Any; @@ -201,6 +201,9 @@ pub trait AnyCast: AnyInfo { /// Implementación automática para cualquier tipo que ya cumpla [`AnyInfo`]. impl AnyCast for T {} +// API para definir acciones que alteran el comportamiento predeterminado del código. +pub mod action; + // API para añadir nuevas funcionalidades usando extensiones. pub mod extension; diff --git a/src/core/action.rs b/src/core/action.rs new file mode 100644 index 0000000..8503ae9 --- /dev/null +++ b/src/core/action.rs @@ -0,0 +1,66 @@ +//! API para definir acciones que inyectan código en el flujo de la aplicación. +//! +//! Permite crear acciones en las librerías para que otros *crates* puedan inyectar código usando +//! funciones *ad hoc* que modifican el comportamiento predefinido en puntos concretos del flujo de +//! ejecución de la aplicación. + +mod definition; +pub use definition::{ActionBase, ActionBox, ActionKey, ActionTrait}; + +mod list; +use list::ActionsList; + +mod all; +pub(crate) use all::add_action; +pub use all::dispatch_actions; + +/// Crea una lista de acciones para facilitar la implementación del método +/// [`actions`](crate::core::extension::ExtensionTrait#method.actions). +/// +/// Esta macro crea vectores de [`ActionBox`], el tipo dinámico que encapsula cualquier acción que +/// implemente [`ActionTrait`]. Evita escribir repetidamente `Box::new(...)` para cada acción +/// inyectada, manteniendo el código más limpio. +/// +/// # Ejemplos +/// +/// Puede llamarse sin argumentos para crear un vector vacío: +/// +/// ```rust,ignore +/// let my_actions = inject_actions![]; +/// ``` +/// +/// O con una lista de acciones concretas: +/// +/// ```rust,ignore +/// let my_actions = inject_actions![ +/// MyFirstAction::new(), +/// MySecondAction::new().with_weight(10), +/// ]; +/// ``` +/// +/// Internamente, expande a un `vec![Box::new(...), ...]`. +/// +/// # Ejemplo típico en una extensión +/// +/// ```rust,ignore +/// impl ExtensionTrait for MyExtension { +/// fn actions(&self) -> Vec { +/// inject_actions![ +/// CustomizeLoginAction::new(), +/// ModifyHeaderAction::new().with_weight(-5), +/// ] +/// } +/// } +/// ``` +/// +/// Así, `PageTop` podrá registrar todas estas acciones durante la inicialización de la extensión y +/// posteriormente despacharlas según corresponda. +#[macro_export] +macro_rules! inject_actions { + () => { + Vec::::new() + }; + ( $($action:expr),+ $(,)? ) => {{ + vec![$(Box::new($action),)+] + }}; +} diff --git a/src/core/action/all.rs b/src/core/action/all.rs new file mode 100644 index 0000000..4dcb68d --- /dev/null +++ b/src/core/action/all.rs @@ -0,0 +1,66 @@ +use crate::core::action::{ActionBox, ActionKey, ActionTrait, ActionsList}; + +use std::collections::HashMap; +use std::sync::{LazyLock, RwLock}; + +// ACCIONES **************************************************************************************** + +static ACTIONS: LazyLock>> = + LazyLock::new(|| RwLock::new(HashMap::new())); + +// AÑADIR ACCIONES ********************************************************************************* + +// Registra una nueva acción en el sistema. +// +// Si ya existen acciones con la misma `ActionKey`, la acción se añade a la lista existente. Si no, +// se crea una nueva lista. +// +// # Uso típico +// +// Las extensiones llamarán a esta función durante su inicialización para instalar acciones +// personalizadas que modifiquen el comportamiento del núcleo o de otros componentes. +// +// ```rust,ignore +// add_action(Box::new(MyCustomAction::new())); +// ``` +pub fn add_action(action: ActionBox) { + let key = action.key(); + let mut actions = ACTIONS.write().unwrap(); + if let Some(list) = actions.get_mut(&key) { + list.add(action); + } else { + let mut list = ActionsList::new(); + list.add(action); + actions.insert(key, list); + } +} + +// DESPLEGAR ACCIONES ****************************************************************************** + +/// Despacha las funciones asociadas a un [`ActionKey`] y las ejecuta. +/// +/// Permite recorrer de forma segura y ordenada (por peso) la lista de funciones asociadas a una +/// acción específica. +/// +/// # Parámetros genéricos +/// - `A`: Tipo de acción que esperamos procesar. Debe implementar [`ActionTrait`]. +/// - `F`: Función o cierre que recibe cada acción y devuelve un valor de tipo `B`. +/// +/// # Ejemplo de uso +/// ```rust,ignore +/// dispatch_actions::(&some_key, |action| { +/// action.do_something(); +/// }); +/// ``` +/// +/// Esto permite a PageTop o a otros módulos aplicar lógica específica a las acciones de un contexto +/// determinado, manteniendo la flexibilidad del sistema. +pub fn dispatch_actions(key: &ActionKey, f: F) +where + A: ActionTrait, + F: FnMut(&A) -> B, +{ + if let Some(list) = ACTIONS.read().unwrap().get(key) { + list.iter_map(f); + } +} diff --git a/src/core/action/definition.rs b/src/core/action/definition.rs new file mode 100644 index 0000000..d662b2b --- /dev/null +++ b/src/core/action/definition.rs @@ -0,0 +1,102 @@ +use crate::core::AnyInfo; +use crate::{UniqueId, Weight}; + +/// Tipo dinámico para encapsular cualquier acción que implementa [`ActionTrait`]. +pub type ActionBox = Box; + +/// Identifica una acción con una clave que define las condiciones de selección de las funciones +/// asociadas a esa acción. +/// +/// Las funciones seleccionadas se van a [despachar](crate::core::action::dispatch_actions) y +/// ejecutar en un punto concreto del flujo de ejecución. +/// +/// # Campos +/// +/// - `action_type_id`: Tipo de la acción. +/// - `theme_type_id`: Opcional, filtra las funciones para un tema dado. +/// - `referer_type_id`: Opcional, filtra las funciones para un tipo dado (p.ej. para un tipo de +/// componente). +/// - `referer_id`: Opcional, filtra las funciones por el identificador de una instancia (p.ej. para +/// un formulario concreto). +#[derive(Eq, PartialEq, Hash)] +pub struct ActionKey { + action_type_id: UniqueId, + theme_type_id: Option, + referer_type_id: Option, + referer_id: Option, +} + +impl ActionKey { + /// Crea una nueva clave para un tipo de acción. + /// + /// Esta clave permite seleccionar las funciones a ejecutar para ese tipo de acción con filtros + /// opcionales por tema, un tipo de referencia, o una instancia concreta según su identificador. + pub fn new( + action_type_id: UniqueId, + theme_type_id: Option, + referer_type_id: Option, + referer_id: Option, + ) -> Self { + ActionKey { + action_type_id, + theme_type_id, + referer_type_id, + referer_id, + } + } +} + +/// Trait base que permite obtener la clave ([`ActionKey`]) asociada a una acción. +/// +/// Implementado automáticamente para cualquier tipo que cumpla [`ActionTrait`]. +pub trait ActionBase { + fn key(&self) -> ActionKey; +} + +/// Interfaz común que deben implementar las acciones del código que pueden ser modificadas. +/// +/// Este trait combina: +/// - [`AnyInfo`] para identificación única del tipo en tiempo de ejecución. +/// - `Send + Sync` para permitir uso concurrente seguro. +/// +/// # Métodos personalizables +/// - `theme_type_id()`: Asocia la acción a un tipo concreto de tema (si aplica). +/// - `referer_type_id()`: Asocia la acción a un tipo de objeto referente (si aplica). +/// - `referer_id()`: Asocia la acción a un identificador concreto. +/// - `weight()`: Controla el orden de aplicación de acciones; valores más bajos se ejecutan antes. +pub trait ActionTrait: ActionBase + AnyInfo + Send + Sync { + /// Especifica el tipo de tema asociado. Por defecto `None`. + fn theme_type_id(&self) -> Option { + None + } + + /// Especifica el tipo del objeto referente. Por defecto `None`. + fn referer_type_id(&self) -> Option { + None + } + + /// Especifica un identificador único del objeto referente. Por defecto `None`. + fn referer_id(&self) -> Option { + None + } + + /// Define el peso lógico de la acción para determinar el orden de aplicación. + /// + /// Acciones con pesos más bajos se aplicarán antes. Se pueden usar valores negativos. Por + /// defecto es `0`. + fn weight(&self) -> Weight { + 0 + } +} + +// Implementación automática que construye la clave `ActionKey` a partir de los métodos definidos. +impl ActionBase for A { + fn key(&self) -> ActionKey { + ActionKey { + action_type_id: self.type_id(), + theme_type_id: self.theme_type_id(), + referer_type_id: self.referer_type_id(), + referer_id: self.referer_id(), + } + } +} diff --git a/src/core/action/list.rs b/src/core/action/list.rs new file mode 100644 index 0000000..ec8149b --- /dev/null +++ b/src/core/action/list.rs @@ -0,0 +1,43 @@ +use crate::core::action::{ActionBox, ActionTrait}; +use crate::core::AnyCast; +use crate::trace; +use crate::AutoDefault; + +use std::sync::RwLock; + +#[derive(AutoDefault)] +pub struct ActionsList(RwLock>); + +impl ActionsList { + pub fn new() -> Self { + ActionsList::default() + } + + pub fn add(&mut self, action: ActionBox) { + let mut list = self.0.write().unwrap(); + list.push(action); + list.sort_by_key(|a| a.weight()); + } + + pub fn iter_map(&self, mut f: F) + where + Self: Sized, + A: ActionTrait, + F: FnMut(&A) -> B, + { + let _: Vec<_> = self + .0 + .read() + .unwrap() + .iter() + .rev() + .map(|a| { + if let Some(action) = (**a).downcast_ref::() { + f(action); + } else { + trace::error!("Failed to downcast action of type {}", (**a).type_name()); + } + }) + .collect(); + } +} diff --git a/src/core/extension/all.rs b/src/core/extension/all.rs index 63a178c..9012234 100644 --- a/src/core/extension/all.rs +++ b/src/core/extension/all.rs @@ -1,3 +1,4 @@ +use crate::core::action::add_action; use crate::core::extension::ExtensionRef; use crate::core::theme::all::THEMES; use crate::{service, trace}; @@ -105,6 +106,16 @@ fn add_to_dropped(list: &mut Vec, extension: ExtensionRef) { } } +// REGISTRO DE LAS ACCIONES ************************************************************************ + +pub fn register_actions() { + for extension in ENABLED_EXTENSIONS.read().unwrap().iter() { + for a in extension.actions().into_iter() { + add_action(a); + } + } +} + // INICIALIZA LAS EXTENSIONES ********************************************************************** pub fn initialize_extensions() { diff --git a/src/core/extension/definition.rs b/src/core/extension/definition.rs index 6b963a2..645afc8 100644 --- a/src/core/extension/definition.rs +++ b/src/core/extension/definition.rs @@ -1,7 +1,8 @@ +use crate::core::action::ActionBox; use crate::core::theme::ThemeRef; use crate::core::AnyInfo; use crate::locale::L10n; -use crate::service; +use crate::{inject_actions, service}; /// Representa una referencia a una extensión. /// @@ -70,6 +71,15 @@ pub trait ExtensionTrait: AnyInfo + Send + Sync { vec![] } + /// Devuelve la lista de acciones que la extensión va a registrar. + /// + /// Estas [acciones](crate::core::action) se despachan por orden de registro o por + /// [peso](crate::Weight), permitiendo personalizar el comportamiento de la aplicación en puntos + /// específicos. + fn actions(&self) -> Vec { + inject_actions![] + } + /// Inicializa la extensión durante la lógica de arranque de la aplicación. /// /// Se llama una sola vez, después de que todas las dependencias se han inicializado y antes de diff --git a/src/lib.rs b/src/lib.rs index 264fdbc..4ab3c6e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -65,6 +65,11 @@ impl Deref for StaticResources { } } +/// Identificador único de un tipo estático durante la ejecución de la aplicación. +/// +/// **Nota:** El valor es único sólo dentro del proceso actual y cambia en cada compilación. +pub type UniqueId = std::any::TypeId; + /// Representa el peso lógico de una instancia en una colección ordenada por pesos. /// /// Las instancias con pesos **más bajos**, incluyendo valores negativos (`-128..127`), se situarán @@ -87,7 +92,7 @@ pub mod html; pub mod locale; // Soporte a fechas y horas. pub mod datetime; -// Tipos y funciones esenciales para crear extensiones y temas. +// Tipos y funciones esenciales para crear acciones, extensiones y temas. pub mod core; // Gestión del servidor y servicios web. pub mod service; diff --git a/src/prelude.rs b/src/prelude.rs index 7244b91..22d9ea0 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -4,7 +4,7 @@ pub use crate::{builder_fn, html, main, test}; -pub use crate::{AutoDefault, StaticResources, Weight}; +pub use crate::{AutoDefault, StaticResources, UniqueId, Weight}; // MACROS. @@ -16,6 +16,8 @@ pub use crate::include_config; pub use crate::include_locales; // crate::service pub use crate::{include_files, include_files_service}; +// crate::core::action +pub use crate::inject_actions; // API. @@ -35,6 +37,7 @@ pub use crate::service; pub use crate::core::{AnyCast, AnyInfo, TypeInfo}; +pub use crate::core::action::*; pub use crate::core::extension::*; pub use crate::core::theme::*;