jueves, 2 de abril de 2009

Parseando en Scala (6): Generando la salida

Nuestro propósito era parsear código PL/SQL, cosa que ya vimos en los posts anteriores, y generar como salida un diagrama estructurado. 

Vamos a usar el siguiente código PL/SQL para probar el experimento. No importa lo que hace, es cualquier cosa que se me ocurrió.

PROCEDURE MYPROC (Param IN NUMBER) IS
 AUXVAR VARCHAR2(10) := NULL;
begin
 IF Param = 1 THEN
  AUXVAR = 'A';
 ELSIF Param = 2 THEN
  AUXVAR = 'B';
 END IF;

 if AUXVAR IS NOT NULL
 THEN
  FOR RECORD IN (SELECT * FROM MYTABLE WHERE FLD = AUXVAR)
  LOOP
   IF RECORD.EXPIRY <= SYSDATE 
   THEN
    LOG_EXPIRED(RECORD.ID);
   END IF; 
  END LOOP;   
 END IF;      
 
 dbms_output.put_line('END OF MYPROC WITH PARAM = ' || Param); 
 
 exception  
 when others then 
  dbms_output.put_line('OOPS!'); 
end; 

Y vemos antes que nada el resultado que obtuve, tal como lo veo en el navegador. Como decíamos, es una versión HTML del diagrama Nassi-Schneidermann donde tenemos una representación visual de la estructura lógica. 


Cada IF es un recuadro, con un encabezado que indica la condición (seguida de un signo de pregunta), y se divide en dos secciones: por convención, la izquierda corresponde a verdadero y la derecha a falso (THEN y ELSE respectivamente). Se puede ver cómo los IF encadenados con ELSIF quedaron resueltos como IFs anidados. Los ELSE vacíos también se dibujan, con "---". Los ciclos, como el FOR del ejemplo, son recuadros que muestran a la izquierda y en verde la cláusula correspondiente, y a la derecha el código. La sección EXCEPTION se muestra al final del recuadro del bloque debajo de una línea punteada. 

Esto es generado por un módulo a partir del AST  obtenido por el parseador. Pero el generador de HTML no tiene por qué ser la única opción de generación del diagrama, de hecho ya tengo un generador alternativo que comentaré más adelante. El punto es que para permitir la modularización, armamos una jerarquía de traits. El siguiente trait es el padre de los generadores.

trait Generator[T] {
 def nullResult:T
 def generate(o:Option[Node]):T = o match {
  case Some(x) => generate(x)
  case _ => nullResult
 }
 def generate(n:Node):T
}

Si recordamos, Node es el supertipo del modelo  que estamos usando. El parámetro de tipo, T, es necesario porque para las posibles salidas pueden usarse diferentes tipos; de hecho esto es resultado de refactorizar mi código para permitir el generador alternativo en forma elegante. Con el método abstracto generate, requerimos que todos los generadores implementen la "transformación" de un Node cualquiera (de todos los posibles según el modelo), en algo de tipo T. Para la generación HTML ese tipo será String. Agregué una implementación default de generate que, en vez de tomar un Node toma un Option[Node]. Si se trata de un nodo definido, se deriva el control al método abstracto recién mencionado, y si no, a otro que define cómo se representa algo "vacío" para el tipo T. Esto me pareció práctico porque el modelo suele tener relaciones con opcionalidad: así puedo invocar indistintamente la generación de un nodo concreto o de un nodo que puede estar presente o no. Finalmente, el generador que hace todo el trabajo:

trait HTMLGenerator extends Generator[String] {
  def nullResult = ""
  def generate(n:Node):String = n match {
    case Stmt(s) => s.replace("<","&lt;") + "<br>\n"
    
    case WhenExcep(e,body) => "<p>when " + e + " then<br>\n" + 
      (body map generate).mkString + "</p>\n"
    
    case ExcepSection(s) => openCell("class=\"excsec\"") + 
      "<strong>exception</strong><br>\n" + 
      (for(w <- s) yield generate(w)).mkString +
      closeCell
    
    case Block(stmts, exceps) => "<table>" +
      openCell +
      (stmts map generate).mkString + 
      closeCell +
      (if(exceps.isDefined) generate(exceps.get) else "") +
      "</table><br>\n"
    
    case Loop(h,body) => "<table><tr><td class=\"loop\">" +
      h +
      "</td>\n<td>" +
      (body map generate).mkString +
      "</td></tr></table>\n"     
      
    case Cond(c,ct,ce) => "<table>" +
        openCell("colspan=\"2\" class=\"ifhead\"") + 
        c + 
        "<big><b> ?</b></big>" +
        closeCell + 
        openCell("class=\"ifthen\"") + 
        (ct map generate).mkString + 
        "</td><td class=\"ifelse\">" + 
        (ce match {
          case Nil => "---"
          case xs => (xs map generate).mkString 
        }) + 
        closeCell +
        "</table><br>\n"
    
    case Module(t,n,p,r,v,b) => startHtml + "<h3>" + t + " " + n + "</h3>\n" +
        (p match { case Some(x) => generate(x); case None => "" }) + "<br>\n" +
        (if(v != Nil) "<table>" + openCell + (v map generate).mkString + closeCell + "</table>" else "") +
        "<hr><br>" + generate(b) + endHtml
  }

  private val startHtml = "<html><head><style type=\"text/css\">\n" +
     "table { width: 100%; border: solid thin }\n" +
     ".comment { color: darkgreen; font-style: italic }\n" +
     ".ifhead { background-color: moccasin; text-align: center }\n" +
     ".ifthen { border-top: solid thin; border-right: solid thin; }\n" +
     ".ifelse { border-top: solid thin; border-left: solid thin; }\n" +
     ".excsec { border-top: dashed thin; }\n" +
     ".loop { border-right: solid thin; background-color: MediumAquamarine; }\n" +
     "</style></head><body>\n"
  
  private def openCell(modif:String) = "<tr><td " + modif + ">"
  private def openCell:String = openCell("")
  private val closeCell = "</td></tr>"  
 
  private val endHtml = "</body></html>"
}

Definimos nullResult, para los nodos opcionales que no estén presentes, como un String vacío. Implementamos generate haciendo pattern matching sobre el nodo recibido. Contemplamos todos los tipos de nuestro modelo y generamos la representación HTML deseada para cada uno. Los detalles de la implementación puede que no sean del todo claros a primera vista, pero la idea sí, es bien simple. 

El nodo Module siempre estará en la raiz del AST, por eso es el encargado de especificar el comienzo y el final del documento HTML. Entre una cosa y la otra, se producirán todas las invocaciones recursivas de generate para los nodos contenidos en el árbol. Los estilos CSS acá están hardcodeados, pero idealmente serían customizables. 

Con esto finalizamos la serie, aunque me parece que habrá un bonus.

No hay comentarios:

Publicar un comentario