Añade API para extensiones con funcionalidades

Añade el interfaz común que debe implementar cualquier extensión de
PageTop para añadir nuevas funcionalidades a la aplicación en forma de
servicios web y API de uso.
This commit is contained in:
Manuel Cillero 2025-07-15 20:12:15 +02:00
parent 5d2f293942
commit 0df5f3f1c7
7 changed files with 436 additions and 2 deletions

View file

@ -2,6 +2,7 @@
mod figfont;
use crate::core::{extension, extension::ExtensionRef};
use crate::{global, locale, service, trace};
use substring::Substring;
@ -9,6 +10,12 @@ use substring::Substring;
use std::io::Error;
use std::sync::LazyLock;
/// Punto de entrada de una aplicación `PageTop`.
///
/// No almacena datos, pero **encapsula** el ciclo completo de configuración y puesta en marcha.
/// Para instanciarla se puede usar [`new`](Application::new) o [`prepare`](Application::prepare).
/// Después sólo hay que llamar a [`run`](Application::run) (o a [`test`](Application::test) si se
/// está preparando un entorno de pruebas).
pub struct Application;
impl Default for Application {
@ -20,6 +27,23 @@ impl Default for Application {
impl Application {
/// Crea una instancia de la aplicación.
pub fn new() -> Self {
Self::internal_prepare(None)
}
/// Prepara una instancia de la aplicación a partir de una extensión raíz.
///
/// Esa extensión suele declarar:
///
/// - Sus propias dependencias (que se habilitarán automáticamente).
/// - Una lista de extensiones que deben deshabilitarse si estuvieran activadas.
///
/// Esto simplifica el arranque en escenarios complejos.
pub fn prepare(root_extension: ExtensionRef) -> Self {
Self::internal_prepare(Some(root_extension))
}
// Método interno para preparar la aplicación, opcionalmente con una extensión.
fn internal_prepare(root_extension: Option<ExtensionRef>) -> Self {
// Al arrancar muestra una cabecera para la aplicación.
Self::show_banner();
@ -29,6 +53,12 @@ impl Application {
// Valida el identificador de idioma por defecto.
LazyLock::force(&locale::DEFAULT_LANGID);
// Registra las extensiones de la aplicación.
extension::all::register_extensions(root_extension);
// Inicializa las extensiones.
extension::all::initialize_extensions();
Self
}
@ -73,7 +103,10 @@ impl Application {
}
}
/// Ejecuta el servidor web de la aplicación.
/// Arranca el servidor web de la aplicación.
///
/// Devuelve [`std::io::Error`] si el *socket* no puede enlazarse (por puerto en uso, permisos,
/// etc.).
pub fn run(self) -> Result<service::Server, Error> {
// Prepara el servidor web.
Ok(service::HttpServer::new(move || {
@ -112,6 +145,6 @@ impl Application {
InitError = (),
>,
> {
service::App::new()
service::App::new().configure(extension::all::configure_services)
}
}

205
src/core.rs Normal file
View file

@ -0,0 +1,205 @@
//! Tipos y funciones esenciales para crear extensiones.
use std::any::Any;
/// Selector para identificar segmentos de la ruta de un tipo.
#[derive(Clone, Copy, Debug)]
pub enum TypeInfo {
/// Ruta completa tal y como la devuelve [`core::any::type_name`].
FullName,
/// Último segmento de la ruta por ejemplo `Vec<i32>` en lugar de `alloc::vec::Vec<i32>`.
ShortName,
/// Conserva todo **desde** `start` inclusive hasta el final.
NameFrom(isize),
/// Conserva todo **hasta e incluyendo** `end`.
NameTo(isize),
/// Conserva la subruta comprendida entre `start` y `end` (ambos inclusive).
PartialName(isize, isize),
}
impl TypeInfo {
/// Devuelve el segmento solicitado de la ruta para el tipo `T`.
pub fn of<T: ?Sized>(&self) -> &'static str {
let type_name = std::any::type_name::<T>();
match self {
TypeInfo::FullName => type_name,
TypeInfo::ShortName => Self::partial(type_name, -1, None),
TypeInfo::NameFrom(start) => Self::partial(type_name, *start, None),
TypeInfo::NameTo(end) => Self::partial(type_name, 0, Some(*end)),
TypeInfo::PartialName(start, end) => Self::partial(type_name, *start, Some(*end)),
}
}
// Extrae un rango de segmentos de `type_name` (tokens separados por `::`).
//
// Los argumentos `start` y `end` identifican los índices de los segmentos teniendo en cuenta:
//
// * Los índices positivos cuentan **desde la izquierda**, empezando en `0`.
// * Los índices negativos cuentan **desde la derecha**, `-1` es el último.
// * Si `end` es `None`, el corte llega hasta el último segmento.
// * Si la selección resulta vacía por índices desordenados o segmento inexistente, se devuelve
// la cadena vacía.
//
// Ejemplos (con `type_name = "alloc::vec::Vec<i32>"`):
//
// | Llamada | Resultado |
// |------------------------------|--------------------------|
// | `partial(..., 0, None)` | `"alloc::vec::Vec<i32>"` |
// | `partial(..., 1, None)` | `"vec::Vec<i32>"` |
// | `partial(..., -1, None)` | `"Vec<i32>"` |
// | `partial(..., 0, Some(-2))` | `"alloc::vec"` |
// | `partial(..., -5, None)` | `"alloc::vec::Vec<i32>"` |
//
// La porción devuelta vive tanto como `'static` porque `type_name` es `'static` y sólo se
// presta.
fn partial(type_name: &'static str, start: isize, end: Option<isize>) -> &'static str {
let maxlen = type_name.len();
// Localiza los límites de cada segmento a nivel 0 de `<…>`.
let mut segments = Vec::new();
let mut segment_start = 0; // Posición inicial del segmento actual.
let mut angle_brackets = 0; // Profundidad dentro de '<…>'.
let mut previous_char = '\0'; // Se inicializa a carácter nulo, no hay aún carácter previo.
for (idx, c) in type_name.char_indices() {
match c {
':' if angle_brackets == 0 => {
if previous_char == ':' {
if segment_start < idx - 1 {
segments.push((segment_start, idx - 1)); // No incluye último '::'.
}
segment_start = idx + 1; // Nuevo segmento tras '::'.
}
}
'<' => angle_brackets += 1,
'>' => angle_brackets -= 1,
_ => {}
}
previous_char = c;
}
// Incluye el último segmento si lo hubiese.
if segment_start < maxlen {
segments.push((segment_start, maxlen));
}
// Calcula la posición inicial.
let start_pos = segments
.get(if start >= 0 {
start as usize
} else {
segments.len().saturating_sub(start.unsigned_abs())
})
.map_or(0, |&(s, _)| s);
// Calcula la posición final.
let end_pos = segments
.get(if let Some(end) = end {
if end >= 0 {
end as usize
} else {
segments.len().saturating_sub(end.unsigned_abs())
}
} else {
segments.len() - 1
})
.map_or(maxlen, |&(_, e)| e);
// Devuelve la cadena parcial basada en las posiciones calculadas.
if start_pos >= end_pos {
return "";
}
&type_name[start_pos..end_pos]
}
}
/// Proporciona información de tipo en tiempo de ejecución y conversión dinámica de tipos.
///
/// Este *trait* se implementa automáticamente para **todos** los tipos que implementen [`Any`], de
/// modo que basta con traer [`AnyInfo`] al ámbito (`use crate::AnyInfo;`) para disponer de estos
/// métodos adicionales, o usar el [`prelude`](crate::prelude) de `PageTop`.
///
/// # Ejemplo
///
/// ```rust
/// use pagetop::prelude::*;
///
/// let n = 3u32;
/// assert_eq!(n.type_name(), "u32");
/// ```
pub trait AnyInfo: Any {
/// Devuelve el nombre totalmente cualificado del tipo.
fn type_name(&self) -> &'static str;
/// Devuelve el nombre corto del tipo (último segmento del nombre).
fn short_name(&self) -> &'static str;
/// Devuelve una referencia a `dyn Any` para la conversión dinámica de tipos.
fn as_any_ref(&self) -> &dyn Any;
/// Devuelve una referencia mutable a `dyn Any` para la conversión dinámica de tipos.
fn as_any_mut(&mut self) -> &mut dyn Any;
}
impl<T: Any> AnyInfo for T {
#[inline]
fn type_name(&self) -> &'static str {
TypeInfo::FullName.of::<T>()
}
#[inline]
fn short_name(&self) -> &'static str {
TypeInfo::ShortName.of::<T>()
}
#[inline]
fn as_any_ref(&self) -> &dyn Any {
self
}
#[inline]
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
/// Extiende [`AnyInfo`] con utilidades de *downcasting* para conversión de tipos.
///
/// Preferible a usar directamente `Any::downcast_ref` porque conserva el *trait bound* [`AnyInfo`],
/// lo que permite seguir llamando a `type_name`, etc.
pub trait AnyCast: AnyInfo {
/// Comprueba si la instancia subyacente es de tipo `T`.
#[inline]
fn is<T>(&self) -> bool
where
T: AnyInfo,
{
self.as_any_ref().is::<T>()
}
/// Intenta hacer *downcast* de un objeto para obtener una referencia de tipo `T`.
#[inline]
#[must_use]
fn downcast_ref<T>(&self) -> Option<&T>
where
T: AnyInfo,
{
self.as_any_ref().downcast_ref()
}
/// Intenta hacer *downcast* de un objeto para obtener una referencia mutable de tipo `T`.
#[inline]
#[must_use]
fn downcast_mut<T>(&mut self) -> Option<&mut T>
where
T: AnyInfo,
{
self.as_any_mut().downcast_mut()
}
}
/// Implementación automática para cualquier tipo que ya cumpla [`AnyInfo`].
impl<T: ?Sized + AnyInfo> AnyCast for T {}
// Infraestructura para ampliar funcionalidades mediante extensiones.
pub mod extension;

9
src/core/extension.rs Normal file
View file

@ -0,0 +1,9 @@
//! Infraestructura para ampliar funcionalidades mediante extensiones.
//!
//! Cada funcionalidad adicional que quiera incorporarse a una aplicación `PageTop` se debe modelar
//! como una **extensión**. Todas comparten la misma interfaz declarada en [`ExtensionTrait`].
mod definition;
pub use definition::{ExtensionRef, ExtensionTrait};
pub(crate) mod all;

104
src/core/extension/all.rs Normal file
View file

@ -0,0 +1,104 @@
use crate::core::extension::ExtensionRef;
use crate::{service, trace};
use std::sync::{LazyLock, RwLock};
// EXTENSIONES *************************************************************************************
static ENABLED_EXTENSIONS: LazyLock<RwLock<Vec<ExtensionRef>>> =
LazyLock::new(|| RwLock::new(Vec::new()));
static DROPPED_EXTENSIONS: LazyLock<RwLock<Vec<ExtensionRef>>> =
LazyLock::new(|| RwLock::new(Vec::new()));
// REGISTRO DE LAS EXTENSIONES *********************************************************************
pub fn register_extensions(root_extension: Option<ExtensionRef>) {
// Prepara la lista de extensiones habilitadas.
let mut enabled_list: Vec<ExtensionRef> = Vec::new();
// Si se proporciona una extensión raíz inicial, se añade a la lista de extensiones habilitadas.
if let Some(extension) = root_extension {
add_to_enabled(&mut enabled_list, extension);
}
// Guarda la lista final de extensiones habilitadas.
ENABLED_EXTENSIONS
.write()
.unwrap()
.append(&mut enabled_list);
// Prepara una lista de extensiones deshabilitadas.
let mut dropped_list: Vec<ExtensionRef> = Vec::new();
// Si se proporciona una extensión raíz, analiza su lista de dependencias.
if let Some(extension) = root_extension {
add_to_dropped(&mut dropped_list, extension);
}
// Guarda la lista final de extensiones deshabilitadas.
DROPPED_EXTENSIONS
.write()
.unwrap()
.append(&mut dropped_list);
}
fn add_to_enabled(list: &mut Vec<ExtensionRef>, extension: ExtensionRef) {
// Verifica que la extensión no esté en la lista para evitar duplicados.
if !list.iter().any(|e| e.type_id() == extension.type_id()) {
// Añade primero (en orden inverso) las dependencias de la extensión.
for d in extension.dependencies().iter().rev() {
add_to_enabled(list, *d);
}
// Añade la propia extensión a la lista.
list.push(extension);
}
}
fn add_to_dropped(list: &mut Vec<ExtensionRef>, extension: ExtensionRef) {
// Recorre las extensiones que la actual recomienda deshabilitar.
for d in &extension.drop_extensions() {
// Verifica que la extensión no esté ya en la lista.
if !list.iter().any(|e| e.type_id() == d.type_id()) {
// Comprueba si la extensión está habilitada. Si es así, registra una advertencia.
if ENABLED_EXTENSIONS
.read()
.unwrap()
.iter()
.any(|e| e.type_id() == extension.type_id())
{
trace::warn!(
"Trying to drop \"{}\" extension which is enabled",
extension.short_name()
);
} else {
// Si la extensión no está habilitada, se añade a la lista y registra la acción.
list.push(*d);
trace::debug!("Extension \"{}\" dropped", d.short_name());
// Añade recursivamente las dependencias de la extensión eliminada.
// De este modo, todas las dependencias se tienen en cuenta para ser deshabilitadas.
for dependency in &extension.dependencies() {
add_to_dropped(list, *dependency);
}
}
}
}
}
// INICIALIZA LAS EXTENSIONES **********************************************************************
pub fn initialize_extensions() {
trace::info!("Calling application bootstrap");
for extension in ENABLED_EXTENSIONS.read().unwrap().iter() {
extension.initialize();
}
}
// CONFIGURA LOS SERVICIOS *************************************************************************
pub fn configure_services(scfg: &mut service::web::ServiceConfig) {
for extension in ENABLED_EXTENSIONS.read().unwrap().iter() {
extension.configure_service(scfg);
}
}

View file

@ -0,0 +1,77 @@
use crate::core::AnyInfo;
use crate::locale::L10n;
use crate::service;
/// Representa una referencia a una extensión.
///
/// Las extensiones se definen como instancias estáticas globales para poder acceder a ellas desde
/// cualquier hilo de la ejecución sin necesidad de sincronización adicional.
pub type ExtensionRef = &'static dyn ExtensionTrait;
/// Interfaz común que debe implementar cualquier extensión de `PageTop`.
///
/// Este *trait* es fácil de implementar, basta con declarar la estructura de la extensión y
/// sobreescribir los métodos que sea necesario.
///
/// ```rust
/// use pagetop::prelude::*;
///
/// pub struct Blog;
///
/// impl ExtensionTrait for Blog {
/// fn name(&self) -> L10n { L10n::n("Blog") }
/// fn description(&self) -> L10n { L10n::n("Sistema de blogs") }
/// }
/// ```
pub trait ExtensionTrait: AnyInfo + Send + Sync {
/// Nombre legible para el usuario.
///
/// Predeterminado por el [`short_name`](AnyInfo::short_name) del tipo asociado a la extensión.
fn name(&self) -> L10n {
L10n::n(self.short_name())
}
/// Descripción corta para paneles, listados, etc.
fn description(&self) -> L10n {
L10n::default()
}
/// Otras extensiones que deben habilitarse **antes** de esta.
///
/// `PageTop` las resolverá automáticamente respetando el orden durante el arranque de la
/// aplicación.
fn dependencies(&self) -> Vec<ExtensionRef> {
vec![]
}
/// 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
/// aceptar cualquier petición HTTP.
fn initialize(&self) {}
/// Configura los servicios web de la extensión, como rutas, *middleware*, acceso a ficheros
/// estáticos, etc., usando [`ServiceConfig`](crate::service::web::ServiceConfig).
///
/// ```rust,ignore
/// use pagetop::prelude::*;
///
/// pub struct ExtensionSample;
///
/// impl ExtensionTrait for ExtensionSample {
/// fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
/// scfg.route("/sample", web::get().to(route_sample));
/// }
/// }
/// ```
#[allow(unused_variables)]
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {}
/// Permite crear extensiones para deshabilitar y desinstalar los recursos de otras extensiones
/// utilizadas en versiones anteriores de la aplicación.
///
/// Actualmente no se usa, pero se deja como *placeholder* para futuras implementaciones.
fn drop_extensions(&self) -> Vec<ExtensionRef> {
vec![]
}
}

View file

@ -74,6 +74,8 @@ pub mod html;
pub mod locale;
// Soporte a fechas y horas.
pub mod datetime;
// Tipos y funciones esenciales para crear extensiones.
pub mod core;
// Gestión del servidor y servicios web.
pub mod service;
// Prepara y ejecuta la aplicación.

View file

@ -33,4 +33,8 @@ pub use crate::datetime::*;
pub use crate::service;
pub use crate::core::{AnyCast, AnyInfo, TypeInfo};
pub use crate::core::extension::*;
pub use crate::app::Application;