Subsecciones


Un DSL para Procesar Documentos XML usando instance_eval

  1. Ejemplo modificado de la sección Dealing with XML del libro [9].
  2. Véase también https://github.com/cocoa/eloquent-ruby.

XML

Un ejemplo de XML. Contenidos del fichero fellowship.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<document>
  <title>The Fellowship of the Ring</title>
  <author>J. R. R. Tolken</author>
  <published>1954</published>
  
  <chapter>
    <title>A Long Expected Party</title>
    <content>When Mr. Bilbo Bagins from Bag End ...</content>
  </chapter>

  <chapter>
    <title>A Shadow Of The Past</title>
    <content>The talk did not die down ...</content>
  </chapter>

  <!-- etc -->
</document>

REXML

Véanse:

Un DSL para XML: Primera Versión. Modo de Uso

Aunque REXML es fácil de usar, nos gustaría disponer de un poco mas de expresividad de manera que podamos expresar un conjunto de acciones que se ejecuten cuando se produce cierto matching. El siguiente fragmento de código muestra una primera versión de nuestro DSL:

if $0 == __FILE__
  ripper = XmlRipper.new do |r|
    puts "Compiling  ..."
    r.on_path '/document/title' do |t| 
      puts "Title: "+t.text 
    end
    r.on_path '/document/author' do |a| 
      puts "Author: "+a.text 
    end
    r.action { puts "Chapters: " }
    r.on_path '/document/chapter/title' do |ct| 
      puts "  "+ct.text 
    end
  end
  filename = ARGV.shift || 'fellowship.xml'
  ripper.run filename
  #  Compiling  ...
  #  Title: The Fellowship of the Ring
  #  Author: J. R. R. Tolken
  #  Chapters: 
  #    A Long Expected Party
  #    A Shadow Of The Past
end

La Clase XmlRipper: Primera Aproximación

require 'rexml/document'

class XmlRipper
  def initialize()
    @before_action = proc { } # do nothing
    @path_action   = []       # Array: to preserve the order
    @after_action  = proc { }
    yield self if block_given?
  end

  def on_path(path, &block)
    @path_action << [ path, block ]
  end

  def action(&block)
    @path_action << [ '', block ]
  end

  def before(&block)
    @before_action = block
  end

  def after(&block)
    @after_action = block
  end

  def run_path_actions(doc)
    @path_action.each do |path_action|
      path, action = path_action
      REXML::XPath.each(doc, path) do |element|
        action[element]
      end
    end
  end

  def run(file_name)
    File.open(file_name) do |f|
      doc = REXML::Document.new(f)
      @before_action[doc]
      run_path_actions(doc)
      @after_action[doc]
    end
  end
end

Un DSL para XML: Segunda Versión

Queremos evitar esas cacofonías de uso del objeto r en la versión anterior (sección 15.12.2) y poder usarlo así:

[~/chapter8ReflectionandMetaprogramming/DSL/xmlripper]$ cat sample.xr 
puts "Compiling  ..."
on_path '/document/title' do |t| 
  puts "Title: "+t.text 
end
on_path '/document/author' do |a| 
  a.text = "J.R.R. Tolkien"
  puts "Author: "+a.text 
end
action { puts "Chapters: " }
on_path '/document/chapter/title' do |ct| 
  puts "  "+ct.text 
end

Nueva versión de initialize

Casi lo único que hay que cambiar en la versión anterior es el método initialize:

  def initialize( path = nil, &block )
    @before_action = proc { } # do nothing
    @path_action   = []       # Array: to preserve the order
    @after_action  = proc { }

    if path then
      instance_eval( File.read( path ), path, 1 )
    else
      instance_eval( &block ) if block_given?
    end
  end

Si queremos que nuestro DSL pueda ser llamado especificando el contexto para que el bloque se ejecute podemos usar esta versión ligeramente mas complicada:

  def initialize( path = nil, &block )
    @before_action = proc { } # do nothing
    @path_action   = []       # Array: to preserve the order
    @after_action  = proc { }

    if path then
      instance_eval( File.read( path ) , path, 1)
    elsif block_given?
      if block.arity < 1
        instance_eval( &block ) 
      else
        block.call(self)
      end
    end
  end

Un Ejecutable para Interpretar Guiones en nuestro DSL XRipper

En la misma forma en que trabajan rake y rspec podemos crear un ejecutable que recibe como entrada un fichero escrito en el DSL XRipper y ejecuta las acciones asociadas:

[~/DSL/xmlripper]$ cat xripper 
#!/usr/bin/env ruby

require 'xml_ripper_v2'

scriptname = ARGV.shift
xmlfile = ARGV.shift

if scriptname and xmlfile and File.exists? scriptname and File.exists? xmlfile
  XmlRipper.new(scriptname).run(xmlfile)
else
  puts "Usage:\n\t#$0 xripper_script file.xml"
end

Un Guión Escrito en Nuestro DSL XRipper para Nuestro Intérprete xripper

Ahora lo único que hay que hacer es ejecutar nuestro intérprete xripper sobre un fichero escrito en el DSL:

[~/chapter8ReflectionandMetaprogramming/DSL/xmlripper]$ cat sample.xr 
puts "Compiling  ..."
on_path '/document/title' do |t| 
  puts "Title: "+t.text 
end
on_path '/document/author' do |a| 
  a.text = "J.R.R. Tolkien"
  puts "Author: "+a.text 
end
action { puts "Chapters: " }
on_path '/document/chapter/title' do |ct| 
  puts "  "+ct.text 
end

Ejecutando el Intérprete XRipper

Para llevar a cabo la ejecución escribimos un Rakefile:
[~/DSL/xmlripper]$ cat Rakefile 
task :default => :run

desc "run xripper "
task :run do
  sh "ruby -I. xripper sample.xr fellowship.xml"
end
La ejecución produce esta salida:
[~/chapter8ReflectionandMetaprogramming/DSL/xmlripper]$ rake
ruby -I. xripper sample.xr fellowship.xml
Compiling  ...
Title: The Fellowship of the Ring
Author: J.R.R. Tolkien
Chapters: 
  A Long Expected Party
  A Shadow Of The Past

Casiano Rodriguez León 2015-01-07