Package Exports
- estructura-js
Readme
Estructura
Estructura es un framework de JavaScript, ligero y sin dependencias, que implementa el despacho múltiple (multiple dispatch) basado en un sistema de tipado dinámico y extensible.
En lugar de la programación orientada a objetos tradicional, donde los métodos pertenecen a clases estáticas (miInstancia.metodo()), Estructura te permite definir funciones que se "adjuntan" a un objeto dinámico basado en los tipos de los argumentos. Esto resulta en una sintaxis de uso fluida (_e(datos).metodo()), pero con una lógica de despacho mucho más flexible y polimórfica.
Características Principales
- Despacho Múltiple Basado en Tipos: Selecciona la lógica a ejecutar basándose en la combinación de todos los tipos de los argumentos, no solo del primero.
- Sistema de Tipos Extensible: Define tus propios tipos y jerarquías (
subtype) para cualquier estructura de datos, yendo mucho más allá de los tipos primitivos de JavaScript. - Instancias Aisladas (Sandboxing): Crea múltiples instancias de Estructura (
instance) que no interfieren entre sí, cada una con su propio registro de tipos y funciones. - Ligero y sin Dependencias: Menos de 30 KB (menos de 10 KB minificado), ideal para el navegador o Node.js sin añadir peso innecesario.
- Robusto y Predecible: Las llamadas al despachador son deterministas. Para una misma configuración de tipos y funciones registradas, una llamada a
_e(arg1, arg2)siempre devolverá el mismo conjunto de métodos, evitando efectos secundarios inesperados.
¿Por qué usar Estructura?
Estructura es ideal cuando necesitas manejar lógica compleja que depende de la naturaleza de tus datos. Es ideal para:
- APIs Polimórficas: Crear funciones como
render(shape),render(shape, context),render(arrayOfShapes)que se resuelven automáticamente. - Sistemas de Plugins: Permitir que extensiones de terceros registren manejadores para nuevos tipos de datos sin modificar el núcleo de tu aplicación.
- Procesamiento de Datos: Escribir tuberías de datos limpias que reaccionan a diferentes formatos de entrada (JSON, XML, CSV, etc.).
- Refactorizar Código Complejo: Reemplazar largas cadenas de
if/elseoswitchque compruebantypeofeinstanceof.
Instalación
Puedes instalar Estructura a través de npm:
npm install estructura-jsO usarlo directamente en el navegador a través de un CDN:
<script src="https://unpkg.com/estructura-js"></script>Compatibilidad
Estructura está diseñado para ser casi universalmente compatible, funcionando sin problemas en una amplia gama de entornos de JavaScript, desde navegadores modernos y antiguos hasta Node.js, etc.
Formatos de Módulo
El paquete se distribuye en múltiples formatos para asegurar una integración sencilla con cualquier sistema de módulos:
Módulos ES (ESM): El formato principal y moderno. Ideal para usar con
importen Node.js con herramientas de compilación como Vite, Rollup, Webpack, o en navegadores actualizados.- Node.js:
import _e from 'estructura-js';
- Navegadores actuales:
<script type="module"> import _e from 'https://unpkg.com/estructura-js'; </script>
UMD (Universal Module Definition): Proporciona máxima compatibilidad.
- Navegadores (Global): Si se incluye con un tag
<script>normal, crea una variable global_e.<script src="https://unpkg.com/estructura-js"></script> <script> // La variable global "_e" se ha cargado correctamente. const estructura = _e; // Ejemplo para mostrar los tipos de datos en la consola. let tipos = _e.type('hola'); console.log(tipos); //> [ 'String', String: true ] </script>
- CommonJS (Node.js): Funciona de forma nativa en Node.js con
require().const _e = require('estructura-js');
- AMD (Asynchronous Module Definition): Compatible con cargadores de módulos como RequireJS.
- Navegadores (Global): Si se incluye con un tag
Compatibilidad con Navegadores
El código de la distribución UMD está escrito en sintaxis compatible con ES3/ES5, lo que garantiza su funcionamiento en todos los navegadores modernos y en la mayoría de los antiguos, incluyendo Internet Explorer 9+, sin necesidad de transpilación.
También incluye subtipos predefinidos para elementos del navegador y el DOM, para importarlos en una instancia, como en el siguiente ejemplo: _e.subtype('browser-dom').
Nota: Los ejemplos utilizan sintaxis de ES6 por brevedad, pero pueden ser fácilmente convertidos a funciones function() { ... } para su uso en entornos ES5.
Compatibilidad con Node.js
Estructura funciona perfectamente en cualquier versión de Node.js que soporte la sintaxis ES5.
Guía de Inicio Rápido
El concepto central es simple: defines funciones para tipos o combinaciones de tipos y luego llamas al despachador principal _e() con tus datos.
import _e from 'estructura-js';
// 1. Definir funciones para tipos específicos.
_e.fn({
Array: {
log: (args) => console.log(`Un array: ${args[0]}`)
},
Number: {
log: (args) => console.log(`Un número: ${args[0]}`)
},
String: {
Number: {
combine: (args) => console.log(`Combinado: ${args[0]} y ${args[1]}`)
}
}
});
// 2. Llamar al despachador.
_e(['hola']).log(); //> "Un array: hola"
_e(12345).log(); //> "Un número: 12345"
_e('texto', 67890).combine(); //> "Combinado: texto y 67890"Documentación de la API
_e(arg1, arg2, ...)
La función despachadora principal.
- Analiza los tipos de los argumentos proporcionados.
- Busca una función que coincida con toda la secuencia de tipos.
- Devuelve un nuevo objeto con los métodos correspondientes adjuntos.
.fn(definitions)
Registra las funciones o métodos para los tipos y sus combinaciones.
definitions: Un objeto anidado donde las claves son nombres de tipos y cada valor debe ser una función o un objeto con más definiciones.
_e.fn({
Object: {
keys: (args) => Object.keys(args[0])
}
});
const keys = _e({ a: 1, b: 2 }).keys();
console.log(keys); //> ["a", "b"]Los métodos que registras siempre reciben los argumentos originales del despachador como un array en la primera posición.
_e.fn({
String: {
repeat: (args, times) => args[0].repeat(times)
}
});
const repeated = _e("hola ").repeat(3);
console.log(repeated); //> "hola hola hola "También puedes definir métodos "globales" para una instancia y estarán disponibles sin importar el tipo de los argumentos (Any). Para ello, regístralos en el primer nivel del objeto de definiciones:
_e.fn({
// Este método estará disponible en todas las llamadas a '_e()'.
timestamp: () => `Procesado a las: ${Date.now()}`
});
console.log(_e(123).timestamp()); //> "Procesado a las: 1700000000000"
console.log(_e("abc").timestamp()); //> "Procesado a las: 1700000000001"Nota sobre Precedencia y Colisiones:
Cuando un valor (
input) corresponde a múltiples tipos todos sus métodos se fusionan en el objeto resultante. Si dos o más tipos definen un método con el mismo nombre, se producirá una colisión. En este caso, el método del tipo más general (menos específico) prevalecerá, sobrescribiendo al del tipo más específico.El orden de sobrescritura es el siguiente (el de abajo sobrescribe al de arriba):
- Métodos de subtipos o alias: Definidos con
.subtype(). Ejemplo:_e.fn({ MiTipoColeccion: { miMetodo: ... }})
- Métodos de tipos base: Tipos primitivos del valor (
Object,Array, etc.). Ejemplo:_e.fn({ Array: { miMetodo: ... }})
- Métodos globales: Definidos en la raíz del objeto con
.fn(). Ejemplo:_e.fn({ miMetodo: ... })Esto significa, por ejemplo, que un método para
Arraysobrescribirá a uno con el mismo nombre enMiTipoColeccion. Se puede saber el orden de especificidad con.type().
Fusión de Definiciones (Registro Aditivo)
Si llamas a .fn() varias veces con definiciones para el mismo tipo, estas se fusionan en lugar de sobreescribirse. Este comportamiento aditivo es ideal para sistemas de plugins o para organizar el código en módulos, ya que permite añadir nueva funcionalidad de forma segura.
// 1. Registro inicial en el núcleo de la aplicación.
_e.fn({
String: {
log: (args) => console.log(`[LOG]: ${args[0]}`)
}
});
// 2. Más tarde, un "plugin" añade nueva funcionalidad al tipo String.
_e.fn({
String: {
wordCount: (args) => args[0].split(' ').length
}
});
// 3. Ambos métodos están ahora disponibles gracias a la fusión.
const miFrase = _e("Estructura es muy flexible");
miFrase.log(); //> [LOG]: Estructura es muy flexible
console.log(miFrase.wordCount()); //> 4Nota sobre la Fusión con Nodos Híbridos:
La fusión también se aplica a los nodos híbridos, pero con una regla importante: una vez que un tipo se define como un nodo híbrido, siempre se comportará como tal.
Si posteriormente registras un objeto de métodos para ese mismo tipo, los nuevos métodos se añadirán a la función del nodo híbrido, pero esta no perderá su capacidad de auto-ejecutarse.
Ejemplo (Fusión con un Nodo Híbrido):
// 1. Definimos un nodo híbrido para 'Number'.
_e.fn({
Number: (num) => console.log(`Nodo híbrido ejecutado para: ${num}`)
});
// 2. Fusionamos un objeto con un nuevo método.
_e.fn({
Number: {
isEven: (args) => args[0] % 2 === 0
}
});
// 3. El nodo se auto-ejecuta y tiene el nuevo método disponible.
const miNumero = _e(10); //> Nodo híbrido ejecutado para: 10
console.log(miNumero.isEven()); //> true.subtype(definitions)
Extiende el sistema de tipos de Estructura. Es la característica más potente.
definitions: Un objeto donde las claves son nombres de tipos existentes o predefinidos y los valores pueden ser:
- Una función que devuelve un nuevo nombre de tipo.
- Una cadena de texto para crear un alias simple.
- Un
arrayde cadenas de texto para asignar múltiples alias nuevos a la vez.
Si es función de subtipo, esta recibe el input y debe devolver:
- Un
stringcon el nombre del nuevo subtipo. truesi el nombre de la definición debe usarse como el nombre del subtipo.falseoundefinedsi no hay coincidencia.
Además, definitions puede ser una cadena de texto como 'browser-dom', que sirve para cargar a la instancia subtipos predefinidos para el DOM de navegadores, por ejemplo: _e.subtype('browser-dom').
// Crear subtipos.
_e.subtype({
// 1. Con una función.
Object: (input) => (input.userId ? 'User' : false),
// 2. Con un string (un alias simple).
RegExp: 'RegexPattern',
// 3. Con un array (múltiples alias).
// Un Array ahora también es de tipo 'Coleccion' y 'ListaOrdenada'.
Array: ['Coleccion', 'ListaOrdenada']
});
// Registrar funciones para los nuevos tipos.
_e.fn({
User: {
hello: (args) => console.log(`Hello, ${args[0].name}!`)
},
RegexPattern: {
test: (args, str) => args[0].test(str)
},
// Se pueden registrar métodos para CUALQUIER alias.
Coleccion: {
esColeccion: () => true
},
ListaOrdenada: {
count: (args) => args[0].length
}
});
const user = { userId: 'u-123', name: 'Alex' };
_e(user).hello(); //> "Hello, Alex!"
const hasNumber = _e(/\d+/).test('abc-123');
console.log(hasNumber); //> true
// Ahora los métodos de ambos alias están disponibles.
const miLista = _e([1, 2, 3]);
console.log(miLista.count()); //> 3
console.log(miLista.esColeccion()); //> true.instance(name)
Crea o recupera una instancia aislada de Estructura. Esto es ideal para evitar colisiones en aplicaciones grandes o al crear librerías.
const miApi = _e.instance('miApi');
const otraApi = _e.instance('otraApi');
// Las definiciones en miApi no afectan a otraApi.
miApi.fn({ String: { log: () => console.log('Log de miApi') } });
otraApi.fn({ String: { log: () => console.log('Log de otraApi') } });
miApi('test').log(); //> "Log de miApi"
otraApi('test').log(); //> "Log de otraApi".type(input)
Una herramienta de utilidad que te permite saber los tipos de cualquier variable. Devuelve un array/mapa de todos los tipos detectados.
const types = _e.type({ id: 1 });
console.log(types); //> [ 'Object', Object: true ]Una Distinción Crucial: Cómo se Pasan los Argumentos
Antes de explorar los conceptos avanzados, es vital entender una diferencia clave en cómo tus funciones reciben los argumentos, dependiendo de cómo las registres.
- Métodos Estándar (dentro de un objeto):
Reciben todos los argumentos originales de la llamada a
_e()agrupados en un único array como primer parámetro.
_e.fn({
String: {
// 'args' es ['Hola']
miMetodo: (args) => console.log(`El primer argumento es: ${args[0]}`)
}
});
_e('Hola').miMetodo(); //> "El primer argumento es: Hola"- Nodos de Función Híbridos (funciones directas):
Reciben los argumentos originales de
_e()de forma desplegada y directa.
_e.fn({
// 'arg1' es 'Hola'
String: (arg1) => { console.log(`Recibí directamente: ${arg1}`); }
});
_e('Hola'); //> "Recibí directamente: Hola"Subtipos Predefinidos: 'browser-dom'
Este conjunto de subtipos simplifica drásticamente la manipulación del DOM al agrupar cientos de tipos de objetos específicos del navegador en unas pocas categorías potentes y genéricas.
Cómo Importar o Activar browser-dom
// Activar en la instancia por defecto.
_e.subtype('browser-dom');
// O en una instancia nombrada.
const domAPI = _e.instance('domAPI');
domAPI.subtype('browser-dom');
/*
* Ejemplos de uso en un navegador.
* Nota: El resultado de .type() se muestra aquí ordenado desde el
* tipo más específico al más general para mayor claridad.
*/
console.log(domAPI.type(document.head)); //> [ "Node.HEAD", "Node", "HTMLHeadElement", "Object" ]
console.log(domAPI.type(document)); //> [ "Document", "Browser", "HTMLDocument", "Object" ]
console.log(domAPI.type(window)); //> [ "Browser", "Window", "Object" ]Resumen de Tipos Identificados
1. Node
Representa cualquier elemento o nodo individual en el DOM. Esta es la categoría más amplia y abarca:
- Todos los Elementos HTML: Desde
HTMLHtmlElementhastaHTMLDivElement,HTMLInputElement,HTMLTemplateElement, etc. (para cualquier etiqueta que puedas escribir). - Elementos SVG y MathML: Como
SVGSVGElementyMathMLMathElement. - Nodos que no son elementos: Incluye nodos de texto (
Text), comentarios (Comment), fragmentos de documento (DocumentFragment) y atributos (Attr). - Elementos desconocidos u obsoletos: Como
HTMLUnknownElementyHTMLMarqueeElement.
Subtipo Dinámico:
Node.<TAG_NAME>La característica más potente de este conjunto es la creación de subtipos dinámicos. Después de que un elemento es identificado como
Node, el framework crea un subtipo adicional usando su propiedadtagName. Esto permite un despacho increíblemente granular.
- Ejemplo: Un elemento
<div>se clasifica como[ "Node.DIV", "Node", "HTMLDivElement", "Object" ].- Ejemplo: Un elemento
<button>se clasifica como[ "Node.BUTTON", "Node", "HTMLButtonElement", "Object" ].
2. Nodes
Representa colecciones o listas de nodos, que típicamente son el resultado de consultas al DOM.
NodeList(devuelto pordocument.querySelectorAll()).HTMLCollection(devuelto pordocument.getElementsByTagName()oelement.children).HTMLAllCollection(una colección legacy).
3. Document
Identifica específicamente el objeto document principal de la página.
- Se activa cuando el tipo del objeto es
HTMLDocument.
4. Browser
Identifica objetos globales de alto nivel del entorno del navegador que no son parte del contenido del DOM.
Window(el objeto globalwindow).Navigator(el objetonavigatorcon información del navegador).Screen(el objetoscreencon información de la pantalla).Location(el objetolocationcon información de la URL).History(el objetohistorypara la navegación).- También se aplica al objeto
Documentcomo un tipo más general.
Conceptos Avanzados: Nodos de Función Híbridos
Además de registrar objetos con métodos, Estructura permite registrar una función directamente como un nodo en el mapa de despacho. Estas funciones se comportan de manera especial y ofrecen una gran flexibilidad.
Un nodo de función es "híbrido" porque puede hacer dos cosas a la vez:
Auto-ejecutarse: Si la secuencia completa de tipos coincide con la función, esa función se ejecutará automáticamente. Los argumentos del despachador se pasan directamente a esta función.
Contener más definiciones: Al ser una función (que en JavaScript es un objeto), puede tener propiedades adjuntas que actúen como sub-nodos para un despacho más profundo.
1. Auto-ejecución de Nodos
Puedes registrar una función que se dispare en cuanto los tipos de los argumentos coincidan.
// Definimos una función que se ejecutará para cualquier 'String'.
// Nota: La función recibe los argumentos del despachador directamente.
const logString = (str) => {
console.log(`[LOG]: La string "${str}" fue procesada.`);
};
// Registramos la función directamente bajo el tipo 'String'.
_e.fn({
String: logString
});
// Al llamar a _e con una string, la función se ejecuta automáticamente.
_e("Mi primer evento"); //> "[LOG]: La string "Mi primer evento" fue procesada."
_e("Otro evento más"); //> "[LOG]: La string "Otro evento más" fue procesada."2. Cascada de Ejecución y Herencia de Tipos
Cuando un valor (input) pertenece a múltiples tipos (como un Array, que también es un Object), se ejecutarán en cascada todos los nodos híbridos que coincidan, desde el más específico hasta el más general.
Esto permite crear "middleware" o capas de lógica que se construyen unas sobre otras.
Nota sobre el Nodo Híbrido Raíz: Puedes registrar un nodo híbrido que se ejecute para cualquier llamada a
_e()pasándole una función directamente:_e.fn(function() { ... })
Ejemplo de cascada:
// Nodo para Arrays (muy específico).
_e.fn({
Array: (arr) => console.log(`[LOG]: Nodo para Array ejecutado para: ${arr}`)
});
// Nodo para Objects (menos específico, ya que Array es un Object).
_e.fn({
Object: () => console.log('[LOG]: Nodo para Object ejecutado.')
});
// Nodo híbrido raíz (el más general).
_e.fn(function(){
console.log('[LOG]: Nodo raíz ejecutado.');
});
// Al llamar con un array, se disparan los tres nodos en orden de especificidad.
_e(['a', 'b']);
//> "[LOG]: Nodo para Array ejecutado para: a,b"
//> "[LOG]: Nodo para Object ejecutado."
//> "[LOG]: Nodo raíz ejecutado."3. Sobrescribir Métodos Dinámicamente
Un nodo de función híbrido puede, además, devolver un objeto. Si lo hace, los métodos de ese objeto sobrescribirán el conjunto de métodos que el despachador está construyendo.
Esto permite crear APIs dinámicas donde el resultado de una función puede cambiar los métodos disponibles.
Ejemplo: Un validador que devuelve métodos diferentes según el resultado.
// Subtipo para identificar emails.
_e.subtype({
String: (input) => (input.includes('@') ? 'Email' : false)
});
// Nodo híbrido para el tipo 'Email'.
// Recibe el email como primer argumento, no envuelto en un array.
const validateEmail = (email) => {
console.log(`Validando: ${email}`);
if (email.endsWith('@gmail.com')) {
// Si es un email de Gmail, devuelve métodos específicos.
return {
send: () => console.log('Enviando con la API de Gmail...'),
addToContacts: () => console.log('Añadiendo a contactos de Google.')
};
} else {
// Para otros emails, devuelve un método genérico.
return {
send: () => console.log('Enviando con SMTP genérico...')
};
}
};
// Registramos el nodo híbrido.
_e.fn({
Email: validateEmail
});
// CASO 1: Email de Gmail.
const gmailUser = _e('test@gmail.com');
//> "Validando: test@gmail.com"
gmailUser.send(); //> "Enviando con la API de Gmail..."
gmailUser.addToContacts(); //> "Añadiendo a contactos de Google."
// CASO 2: Otro email.
const otherUser = _e('user@outlook.com');
//> "Validando: user@outlook.com"
otherUser.send(); //> "Enviando con SMTP genérico..."
// otherUser.addToContacts(); // Esto causa error, porque el método no fue devuelto.Esta característica avanzada te permite construir mecanismos y flujos de trabajo de una manera increíblemente declarativa y potente.
Licencia
MIT © 2025 OKZGN