Generador de código a partir de plantillas

Vamos a ver cómo funciona chicote un generador de código basado en plantillas. Hace tiempo tuve que hacer un trabajo muy repetitivo de migración de código de una tecnología a otra, al final el 90% del trabajo era repetitivo. No tan simple como copiar y pegar, pero nada que requiriera demasiado cerebro. Para esos casos cree chicote. Chicote es el nombre de un famoso chef español. Por eso el uso de términos relativos a la cocina.

Vamos a ver de que consta este sistema.

Motor de plantillas

Lo primero que necesita un generador basado en plantillas es un motor de plantillas que genere texto a partir de una plantilla.

Para este caso opte por mustache.js que es simple, ligero y muy fácil de aprender. Además permite personalizar varios aspectos que necesitaba.

Una de sus principales limitaciones es que no se le puede pasar “parámetros” a las etiquetas, para resolverlo tuve que modificar el código añadiendo esa posibilidad.

Operadores de texto

Otra cosa necesaria para generar código amigable para los seres humanos es poder operar sobre el texto. Es habitual que el mismo nombre se escriba en capital, mayúsculas, minúsculas, separado por guiones,… Los programadores somos unos maniáticos de esto y definimos documentos de estilo solo para especificar este tipo de cosas.

Para esto elegí dos librerías voca.js y pluralize.js y transforme sus operaciones en etiquetas que se puedan usar desde la plantilla.

Además son útiles funciones que actúen a nivel de línea para ordenarlas, limpiar espacios, eliminar líneas duplicadas o tabularlas.

Generador de datos falsos

Los datos falsos con cierta estructura son útiles para generar test, plantillas o demos. En este caso recurrí a la librería faker.js

Algunas ayudas más

Finalmente añadí alguna ayuda más con función habituales en programación como la fecha, la hora, un timestamp, números aleatorios o un contador

Directorios y ficheros

Otra necesidad de los generadores de código programador es poder generar directorios y nombres de archivos con algún tipo de plantilla. Es muy habitual usar la estructura de directorios para organizar el código.

El cocinero (Chicote)

Chicote es un generador agnóstico de código basado en plantillas. Es agnóstico puesto que sirve para cualquier lenguaje que use ficheros de texto. Necesita NodeJS para funcionar. No necesitas instalar nada, sólo descargar el código y ejecutar cooking.js

node cooking.js

La salida se genera en el directorio output

La receta (recipes.json)

Necesitas crear un archivo recipes.json, que contiene una o mas recetas, cada receta contiene todos los ingredientes y pasos para generar código. El único apartado obligatorio en cada receta es steps ,un array con donde se indican que pasos (steps) tiene que ejecutar para realizar la receta. Un step es el nombre de un archivo JSON en el directorio steps.

Ejemplo de fichero recipes.json con solo una receta:

[
{	
    "name": "Test",
	"author": "Bob",
	"fields": [
		{"name": "id", "type": "int"},
		{"name": "firstname", "type": "String"},
		{"name": "lastname", "type": "String"},
		{"name": "birthday", "type": "date"}
	],
	"names": ["HelloWorld", "helloWorld", "hello-world"],
	"steps": ["example"]
}
]

Pasos de la receta (step)

Un step es un archivo JSON en el directorio “steps” donde se preparan los ingredientes. Un paso tiene tres partes: variables (vars), directorios (directories), plantillas (templates)

  • vars son variables creadas a partir de los ingredientes. Actúan de forma similar a los ingredientes.
  • directories indica los directorios de salida.
  • plantillas archivos de texto en el directorio de plantillas. Se compone de un array de dos cadenas de texto, la primera indica la ruta de la plantilla en el directorio templates, la segunda la ruta del fichero generado en el directorio output

Se pueden usar los tags que más adelante veremos para calcular lo valores de estos apartados.

Ejemplo de step:

{
    "vars":[
        ["filename1", "exampleA-{{timestamp}}.code"],
        ["filename2", "exampleB-{{timestamp}}.code"],
        ["className", "{{#capital}}{{name}}{{/capital}}"],
        ["varName", "{{#decapital}}{{name}}{{/decapital}}"],
        ["moduleName", "{{#dash}}{{name}}Module{{/dash}}"]
    ],	
    "directories":[
        "example/example1",
        "example/example2",
        "examples/example1/example2"
    ],
    "templates":[
        ["example/example1.text", "text.txt"],
        ["example/example1.text", "example/example1/{{filename1}}"],
        ["example/example2.text", "example/example2/{{filename2}}"],
        ["example/example1.text", "examples/example1/{{filename1}}"],
        ["example/example2.text", "examples/example1/example2/{{filename2}}"]
    ]
}

El cocinado (templates)

Las plantillas son archivos de texto en el directorio “templates” que utilizan la sintaxis de mustache pero con algunas “etiquetas especiales”.

{{timestamp}}  
{{date}} - fecha en formato yyyy-mm-dd
{{time}} - hora en formato hh:mm:ss
{{year}} - año
{{month}} - mes
{{day}} - dia
{{hour}} - hora
{{minute}} - minuto
{{second}} - segundo
{{GUID}}   

{{#plural}}text{{/plural}}  
{{#camel}}text{{/camel}}  
{{#capital}}text{{/capital}}   
{{#decapital}}text{{/decapital}}  
{{#dash}}text{{/dash}}  
{{#snake}}text{{/snake}}  
{{#swap}}text{{/swap}}  
{{#title}}text{{/title}}  
{{#lower}}text{{/lower}}  
{{#upper}}text{{/upper}}  
{{#slug}}text{{/slug}}  
{{#reverse}}text{{/reverse}}  
{{#stripTags}}text{{/stripTags}}  
{{#escHtml}}text{{/escHtml}}  
{{#unHtml}}text{{/unHtml}}  
{{escRegExp}}text{{/escRegExp}}  
{{#trim}}text{{/trim}}  
{{#latin}}text{{/latin}}  

{{#count}}text{{/count}} - reemplaza texto por número de caracteres    
{{#countWords}}text{{/countWords}} - reemplaza el texto por el número de palabras  

{{#delSpaces}}text{{/delSpaces}} - suprimir todos los espacios  
{{#delDuplicateSpaces}}text{{/delDuplicateSpaces}}  - eliminar los espacios duplicados   
{{#delLast|C}}text{{/delLast|C}} - suprimir la última coincidencia de caracteres C  
{{#delFirst|C}}text{{/delFirst|C}} - suprimir la primera coincidencia de caracteres C  
{{#delEnd|N}}text{{/delEnd|N}} - suprimir N caracteres del final  
{{#delStart|N}}text{{/delStart|N}} - del N caracteres desde el inicio  
  
{{#repeat|N}}text{{/repeat2|N}} - repetir el texto N veces  
  
{{#sortAscL}}text{{/sortAscL}} - ordenar las líneas de forma ascendente   
{{#sortDescL}}text{{/sortDescL}} - líneas de ordenación descendente  
{{#naturalSortAscL}}text{{/naturalSortAscL}} - líneas de ordenación natural ascendente  
{{#naturalSortDescL}}text{{/naturalSortAscL}} - líneas de ordenación natural descendente  
{{#shuffleL}}text{{/shuffleL}} - barajar líneas  
{{#trimL}}text{{/trimL}} - recortar líneas  
{{#joinL}}text{{/joinL}} - unir líneas  
{{#removeDuplicateL}}text{{/removeDuplicateL}} - eliminar líneas duplicadas  
{{#spaceL|N}}text{{/spaceL|N}} - Añadir N espacios al principio de cada línea   
{{#tabL|N}}text{{/tabL|N}} - Añadir N tabulaciones al principio de cada línea  
{{#addStartL|C}}text{{/addStartL|C}} - Añadir el carácter C al principio de cada línea   
{{#addEndL|C}}text{{/addEndL|C}} - Añadir el carácter C al final de cada línea  

{{#log}}text{{/log}} - escribir texto en la consola   
{{#eval}}text{{/eval}} - evalúa el texto como código JS  
{{#R}}texto{{/R}} - renderizar texto  
  
{{#C=|N}}{{/C=|N}} - poner el contador en N  
{{#C+|N}}{{/C+|N}} - aumentar el contador en N  
{{#C-|N}}{{/C-|N}} - reducir el contador en N  
{{#C}}{/C}} - imprimir el contador  

{{#K}}texto{{/K}}  - cargar datos de la base de conocimiento (cookbook.json)  
  
{{!text}} - Comentario  

{{#faker}}data{{\faker}} - genera un dato aleatorio usando faker.js

{{=AA BB=}} - Cambia los caracteres para indicar que es un tag de {{ }} to AA BB

Ejemplo de uso de alguno de los tags:

helloWorld
plural: helloWorlds
camel: helloWorld
capital: HelloWorld
decapital: helloWorld
dash: hello-world
snake: hello_world
swap: HELLOwORLD
title: HelloWorld
lower: helloworld
upper: HELLOWORLD
escHtml: helloWorld
slug: hello-world
count: 10
countWords: 2

hello-world
plural: hello-worlds
camel: helloWorld
capital: Hello-world
decapital: hello-world
dash: hello-world
snake: hello_world
swap: HELLO-WORLD
title: Hello-World
lower: hello-world
upper: HELLO-WORLD
escHtml: hello-world
slug: hello-world
count: 11
countWords: 2

El código de Chicote incluye un ejemplo de plantillas.

El libro de cocina (cookbook.json)

Actúa como base de conocimiento para Chicote. Funciona como un sistema de clave valor.

Para leer los datos de la base de conocimiento se usan los tags {{#K}} y {{/K}}. Si tenemos {{#K}}texto{{/K}} texto será reemplazado por el valor que se encuentre en la base de conocimiento con la clave texto

Un ejemplo de base de conocimiento

{	
	"intDefaultValue": " = 0",
	"dateDefaultValue": " = new Date()",
	"stringDefaultValue": " = ''"
}

Ahora veamos un ejemplo de su uso en una plantilla:

var v{{#K}}{{#lower}}{{type}}{{/lower}}DefaultValue{{/K}};

En el ejemplo si type es date dará como resultado:

var v{{#K}}dateDefaultValue{{/K}};

Buscando esa clave en la base de conocimientos:

var v = new Date();

La foto del plato

Como pequeño resumen se puede ver este diagrama que trata de simplificar el funcionamiento de Chicote:

chicote

Bon appetit

Eventos personalizados en javascript

Los eventos fundamentalmente sirven para notificar a otras funciones que están “escuchando” esos eventos  que algo ha ocurrido. En las aplicaciones web estamos acostumbrados a usar eventos asociados a acciones del usuario como pulsar un botón, mover el ratón, hacer click, … Sin embargo podemos crear nuestros eventos asociados a cualquier acción que deseemos.

Para crear un evento solo necesitamos instanciar un objeto CustomEvent con el nombre del evento:

event = new CustomEvent("event");

Si queremos pasar parámetros  tenemos que usar el campo detail:

event = new CustomEvent("event", {
   detail: {
     value: 1
   }
});

El evento debe de ir asociado a un elemento del DOM o al document:

var button = document.getElementById("boton");
button.dispatchEvent(event);

Para “escuchar” un evento usamos addEventListen que asocia el evento a una función que se llama cuando este se dispara:

button.addEventListener("odd", function(e){console.log(e.detail.value);});

La función recibe como parámetro un objeto event con todos los datos del evento. Para acceder a los datos que hemos incluido se puede acceder a valor detail del parámetro.

Veamos un ejemplo completo que convierte un click del ratón en dos eventos uno que se lanza las veces pares y otro las impares:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
</head>
<body>
  <button id="boton"> Pulsame </button>
</body>
  
<script>
  
var button = document.getElementById("boton");
var clicks = 0;
button.addEventListener("click", 
function(e) { 
  console.log("click");
  clicks++;
  var event;
  if(clicks % 2){
    event = new CustomEvent("odd", {
    detail: {
      value: clicks
    }
    });   
  } else {
    event = new CustomEvent("even");
  }
  
  button.dispatchEvent(event);
  
});
button.addEventListener("odd", function(e){console.log(e.detail.value);});
button.addEventListener("even", function(e){console.log("even!");});
</script>
</html>

Los eventos permiten una fácil comunicación entre distintas partes de la aplicación. La parte que emite el evento se desvincula de quién está escuchándolo. A su vez los consumidores del evento escuchan o dejan de hacerlo cuando les interesa sin interferir entre ellos.

Memoization con tiempo de vida

La memoization ya vimos lo que era y como usarla. Ahora vamos a añadir una característica más que puede resultar útil en algunos casos: “tiempo de vida”. Cada respuesta memorizada solo va a ser válida durante un periodo de tiempo. Pasado ese tiempo el resultado deja de ser válido y si se vuelve a llamar

Todo lo que veamos en este texto esta publicado en la librería memo.js

El primer cambio que vamos a realizar sobre lo que vimos en el anterior post de memoization es que tendremos dos variables para almacenar los datos, una con la cache de los parámetros y sus valores y otra con el momento en que se almacenó esa cache.

La forma de funcionar es la misma que la memoization normal solo que cuando se va recuperar un valor de la cache se comprueba cuanto tiempo hace que se almaceno y si ha transcurrido un tiempo mayor que el tiempo de vida fijado se llama a la función y se actualiza la cache con el valor que devuelva.

Veamos algo de código partiendo de la librería de memoization que creamos.

Lo primero es extender la clase original:

class MemoTime extends Memo {

}

Modificamos el constructor para que cree una variable (lifetime) donde guardar cuando fue la última vez que se actualizo esa clave. También se le tiene que pasar al constructor el tiempo de vida (timeOfLife)

 constructor (func, timeOfLife, keyFunc) {
    super(func, keyFunc);
    this.timeOfLife = timeOfLife || 1000;
    this.lifetime = {};        
 }

También hay que modificar la función que añade una clave – valor para que almacene el momento (new Date()) en que se guarda esa clave.

add(key, value){
    this.lifetime[key] = new Date();
    super.add(key, value);
}

Por último la función que verifica si esta en la cache para que responda ‘false’ en el caso de que este pero haya caducado:

 isInCache(key){
   if(key in this.lifetime) {
      if((new Date() - this.lifetime[key]) < this.timeOfLife){ //¿ha caducado?
         return true;
      }
   } else {
      return false;
   }
}

El resultado final es el siguiente:

class MemoTime extends Memo {
    constructor (func, timeOfLife, keyFunc) {
        super(func, keyFunc);
        this.timeOfLife = timeOfLife || 1000;
        this.lifetime = {};        
    }

    isInCache(key){
        if(key in this.lifetime) {
            if((new Date() - this.lifetime[key]) < this.timeOfLife){
                return true;
            }
        } else {
            return false;
        }
    }

    add(key, value){
        this.lifetime[key] = new Date();
        super.add(key, value);
    }
}

Puede parecer contradictorio tener un mecanismo para almacenar los resultados de una función para no tener que volver a llamarla y que caduquen. Esto es útil cuando el resultado de la función cambia con el tiempo o queremos evitar que a una función se le llame muchas veces seguidas.

Por ejemplo si tenemos un servidor que nos devuelve un dato y no queremos saturarlo de peticiones podemos limitar el número de las mismas usando está técnica. Así cada vez que se llame a la función se tendrá una respuesta pero como mucho se le llamara una vez por cada periodo indicado en el tiempo de vida.

Por último vamos a ver un ejemplo de la clase que hemos creado:

function sum(a,b){  
    console.log("calculating "+a+" + "+b);  
    return a+b;  
}  

let memoTime = new MemoTime(sum, 2000);

console.log(memoTime.call(4,5)); //call sum

console.log(memoTime.call(4,5)); //no call sum
setTimeout(memoTime.call(4,5),1000); //no call sum
setTimeout(memoTime.call(4,5),3000); //call sum