Publi

Generando la salida de nuestros programas con plantillas en C++ con Silicon en pocas líneas

photo-1456983887044-b5ecb1059660

A medida que el hardware y el software van evolucionando, van cambiando las preferencias a la hora de desarrollar un producto. Hace unos años, el objetivo principal era que el producto final funcionara, intentar hacer el mínimo número de operaciones para que el software corriera en un ordenador de la mejor forma posible. Desde hace unos años, el hardware es capaz de hacer muchas cosas y muy rápido, los lenguajes han evolucionado y se han optimizado mucho; por lo tanto, podemos aprovechar y hacer que desde el punto de vista del mantenimiento, legibilidad del código, hacer que el código sea poesía y hacer que todas las piezas encajen perfectamente.
Es decir, ya no se suelen ver sentencias goto en ningún código (quien busca encuentra, pero a no ser que el desarrollador sepa lo que hace y sin ellas se vea muy mermado el rendimiento se intentan evitar). Pero aún así, es común que en diferentes programas encontremos uso de programación orientada a objetos, modelo-vista-controlador, identificadores en cadena de texto en lugar de numéricos (o enums) en medio del código para hacernos fácil el mantenimiento o representación y el hecho de compartir el código con otros, crear clases lo más generales posible y luego particularizarlas con nuestras necesidades, incluso, interpretar muchas partes a través de scripts (es decir, utilizar un lenguaje ya existente o inventarnos el nuestro para programar ciertas partes, o dejar que el usuario pueda programarlas).

En este caso, vengo a tratar el tema de las plantillas, o lo que es lo mismo: queremos generar una salida con un formato determinado, imaginémonos un texto:

Hey Jugador1, has conseguido 123 puntos y has perdido

Imaginemos que estamos haciendo un juego y queremos generar ese mensaje (empezamos por algo sencillo), pero a partir de aquí podemos obtener varias combinaciones de mensajes que podemos resumir sustituyendo parte del texto por palabras móviles, es decir, cosas que en el futuro cambiaremos por lo que más nos convenga:

Hey %nombreJugador%, has conseguido %puntos% puntos y has %destino%

Así el jugador se puede llamar como queramos, puede tener el nombre que quiera, conseguir los puntos que quiera y lo que ha pasado con el, puede haber ganado o haber perdido.
Esto me recuerda horrores a un simple printf (o sprintf) en el que la cadena es: “Hey %s, has conseguido %d puntos y has %destino%”. Pero claro, puede que en otros idiomas, o porque luego veamos que quede mejor, el mensaje ahora sea. Has sacado %puntos% puntos. %nombreJugador% has %perdido%. Aquí el caso del printf ya no nos sirve, porque importa el orden de los elementos.

Reemplazar texto

Lo primero que podemos probar para nuestro sistema de plantillas, podemos verlo aquí. Es decir, un sistema que nos permita reemplazar cadenas por otras cadenas. De esta forma cuando encontremos:

  • %nombreJugador% : lo cambiaremos por el nombre del jugador
  • %puntos% : lo cambiaremos por los puntos conseguidos.
  • %destino% : lo cambiaremos por un “ganado” o “perdido”

Ahora no nos importa que nos cambien el orden de las palabras clave, porque da igual dónde encontremos una, ésta se sustituirá por su equivalencia. Podemos ver un ejemplo aquí (C++11):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <iostream>
#include <string>
#include <map>

using namespace std;

string replace2(string source, std::map<string,string>strMap, int offset=0, int times=0)
{
  int total = 0;
  string::size_type pos=offset;
  string::size_type newPos;
  string::size_type lowerPos;

  do
    {
      string rep;
      for (std::map<string, string>::iterator i=strMap.begin(); i!=strMap.end(); ++i)
    {
      string fromStr = i->first;

      newPos = source.find(fromStr, pos);
      if ( (i==strMap.begin()) || (newPos<lowerPos) )
        {
          rep = fromStr;
          lowerPos = newPos;
        }
    }

      pos = lowerPos;
      if (pos == string::npos)
    break;

      string toStr = strMap[rep];

      source.replace(pos, rep.length(), toStr);
      pos+=toStr.size();

    } while ( (times==0) || (++total<times) );

  return source;
}

int main()
{
  int puntuacion = 100;
  int enemigo = 90;

  string original = "Hey %nombreJugador% has ganado %puntos% puntos y has %destino%";
  map<string,string> mapa = { { "%nombreJugador%", "Gaspar" }, {"%puntos%", to_string(puntuacion)}, {"%destino%", (puntuacion>enemigo)?"ganado":"perdido"}};

  cout << "Original string: "<<original<<endl;

  cout << "Resulting string: "<<replace2(original, mapa)<<endl;

  return 0;
}

Condicional en la plantilla

Por ahora nos vale, aunque poco a poco veremos la necesidad de hacer la lógica un poco más compleja. ¿Y si no nos limitamos a sustituir palabras, sino que introducimos algo de lógica en el sistema de plantillas? Esto nos permitiría entre otras cosas aislar por completo el idioma de nuestro código, eliminando las palabras ganado y perdido. En este caso, a la plantilla le damos todas las palabras, y ya el sistema de plantillas se encargará de devolvernos la cadena definitiva. Para este ejemplo usaré la biblioteca Silicon (la de arriba):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <string>
#include "silicon.h"

using namespace std;

int main()
{
  int puntuacion = 100;
  int enemigo = 90;

  string original = "Hey {{nombreJugador}} has ganado {{puntos}} puntos y has {%if puntos>enemigodestino}}ganado{/if}}{%if puntos<enemigodestino}}perdido{/if}}";
  Silicon t = Silicon::createFromStr(original);
  try
    {
      t.setKeyword("nombreJugador", "Gaspar");
      t.setKeyword("puntos", std::to_string(puntuacion));
      t.setKeyword("enemigo", std::to_string(enemigo));

      cout << t.render()<<std::endl;
    }
  catch (SiliconException &e)
    {
      cout << "Exception!!! "<<e.what() << std::endl;
    }
}

Ahora vemos que la plantilla ha cambiado un poco. Vemos por ejemplo que las palabras clave, ahora vienen entre dobles paréntesis y, además, encontramos una simple lógica en su representación. El propio sistema de plantillas evalúa la expresión y decide qué palabra mostrar (está en sus primeras versiones, por ahora no tiene else…), pero al menos, cuando le pasamos las palabras clave al sistema, no hay que poner los caracteres comodín (% antes, {{, }} ahora). Además, este sistema nos permite incluso modificar la lógica sin necesidad de compilar (en el caso en que las plantillas se encuentren en un fichero, o incluso las puedas descargar de Internet en tiempo real).

Llamar a funciones para obtener datos pesados

Otra cosa curiosa es que podemos configurar ciertos textos como funciones. Es decir, como estamos dando flexibilidad a la hora de crear plantillas. Podemos también definir muchos keywords, algunos pueden salir siempre, otros algunas veces. Y puede que otros, para obtener el dato se demoren un poco: pensemos en que tenga que ser un valor calculado o descargar de la red un cierto dato antes de representarlo. Para hacer la simulación, vamos a hacer que el dato de obtención del record mundial se retrase un par de segundos. En este caso, veremos dos plantillas, una con el récord y otra sin él, y sólo debe retrasarse la que muestra el récord.

Por ejemplo, podemos ver el siguiente código:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>
#include <string>
#include "silicon.h"
#include <thread>
#include <chrono>

using namespace std;

std::string worldRecord(Silicon* s, std::map<std::string, std::string> args, std::string input)
{
  std::this_thread::sleep_for(std::chrono::seconds(2));
  return "42";
}

int main()
{
  int puntuacion = 100;
  int enemigo = 110;

  string original = "Hey {{nombreJugador}} has ganado {{puntos}} puntos y has {%if puntos>enemigo}}ganado{/if}}{%if puntos<enemigo}}perdido{/if}}";
    //    "{!block template=bloque.html/}\n"
  Silicon t = Silicon::createFromStr(original);
  try
    {
      t.setKeyword("nombreJugador", "Gaspar");
      t.setKeyword("puntos", std::to_string(puntuacion));
      t.setKeyword("enemigo", std::to_string(enemigo));
      t.setFunction("worldRecord", worldRecord);

      cout << t.render()<<std::endl;
      t.setData(original+"\nEl récord mundial es de {!worldRecord/}");
      cout << t.render()<<std::endl;
    }
  catch (SiliconException &e)
    {
      cout << "Exception!!! "<<e.what() << std::endl;
    }
}

Esto nos permitirá hacer muchas definiciones de posibles elementos que serán plasmados en la plantilla, pero que no siempre estarán presentes y sólo se procesarán cuando aparezcan.

Un pequeño archivo XML

Para este ejemplo me voy a basar en un archivo sitemap.xml. El mismo tendrá un formato tal como este:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">
  <url>
    <loc>http://dominio.com/section1</loc>
    <priority>0.70</priority>
    <changefreq>daily</changefreq>
    <lastmod>2015-02-13T15:32:42+0200</lastmod>
  </url>
  <url>
    <loc>http://dominio.com/section2</loc>
    <priority>0.70</priority>
    <changefreq>daily</changefreq>
    <lastmod>2015-02-13T15:32:42+0200</lastmod>
  </url>
  <url>
    <loc>http://dominio.com/section3</loc>
    <priority>0.70</priority>
    <changefreq>daily</changefreq>
    <lastmod>2015-02-13T15:32:42+0200</lastmod>
  </url>
...
</urlset>

En este caso quiero separar toda la parte de XML de mi código, por lo que el XML será un archivo aparte, lo leeremos y lo usaremos para mostrar la plantilla. Aunque en este caso nos encontramos con una lista de elementos que puede ser muy grande. Vamos a crear dos archivos, sitemap.xml:

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">
{%collection var=sites}}
  <url>
    <loc>{{sites.loc}}</loc>
{%if sites.prio}}    <priority>{{sites.prio}}</priority>{/if}}
{%if sites.changefreq}}    <changefreq>{{sites.changefreq}}</changefreq>{/if}}
{%if sites.lastmod}}    <lastmod>{{sites.lastmod}}</lastmod>{/if}}
  </url>
{/collection}}
</urlset>

y ahora nuestro archivo C++:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
#include <string>
#include "silicon.h"

using namespace std;

int main()
{
  Silicon t = Silicon::createFromFile("sitemap.xml");
  try
    {
      std::vector<Silicon::StringMap> sites = { { { "loc", "http://dominio.com/section1" }, { "changefreq", "daily" } } ,
                        { { "loc", "http://dominio.com/section2" }, { "changefreq", "daily" }, { "prio", "0.2" } },
                        { { "loc", "http://dominio.com/section3" }, { "changefreq", "weekly" } },
                        { { "loc", "http://dominio.com/section4" }, { "changefreq", "weekly" }, { "lastmod", "2016-06-01T12:27:19+0200" } } };

      t.addCollection("sites", sites);

      std::cout << t.render()<<std::endl;
    }
  catch (SiliconException &e)
    {
      cout << "Exception!!! "<<e.what() << std::endl;
    }
}

De esta forma, deberá generarse un archivo XML con el formato especificado en el primer archivo, pero con los datos que tenemos en nuestro archivo C++. Tenemos que ver que dentro de la muestra de la colección hemos estado aplicando condiciones.

Generación de páginas web

Esta me la reservo para otro post, ya que podrá ser muy extensa, y tengo alguna que otra cosa preparada… Estad atentos.

Colaboración

Esta biblioteca podéis utilizarla como queráis para lo que queráis, sin garantías “as is”, podéis crear trabajos derivados, romperla, destrozarla y reconstuirla. Eso sí, aunque no es obligatorio, agradecería que me enviaseis cambios, correcciones, bugs, etc.
Hay partes del código que bien podrían ser reescritas, otras deberán ser mejoradas y actualizadas, pero poco a poco 🙂

Foto principal: Redd Angelo

También podría interesarte....

Leave a Reply