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>
Véanse:
rexml/document:
#!/usr/bin/env ruby
require 'rexml/document'
filename = ARGV.shift || 'fellowship.xml'
File.open(filename) do |f|
  doc = REXML::Document.new(f)
  author = REXML::XPath.first(doc, '/document/author')
  print "Author: "
  puts author.text       # J. R. R. Tolken
  puts "Chapters:"
  REXML::XPath.each(doc, '/document/chapter/title') do |title|
    puts "  "+title.text # A Long Expected Party
  end                    # A Shadow Of The Past
end
Ejecución:
$ ruby find_author.rb Author: J. R. R. Tolken Chapters: A Long Expected Party A Shadow Of The Past
#!/usr/bin/env ruby
require 'rexml/document'
filename = ARGV.shift || 'fellowship.xml'
File.open(filename) do |f|
  doc = REXML::Document.new(f)
  REXML::XPath.each(doc, '/document/author') do |author|
    author.text = 'J.R.R. Tolkien'
  end
  puts doc
end
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
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
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
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
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
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
Rakefile:
[~/DSL/xmlripper]$ cat Rakefile task :default => :run desc "run xripper " task :run do sh "ruby -I. xripper sample.xr fellowship.xml" endLa 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