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