Subsecciones

Dos DSLs: Generando XML con Validación via Generación de Métodos

Primer DSL: Lenguaje para la Generación de XML

HTMLForm.generate(STDOUT) do
  comment "This is a simple HTML form"
  form :name => "registration",
       :action => "http://www.example.com/register.cgi" do
    content "Name:"
    br
    input :name => "name"
    br
    content "Post:"
    br
    textarea :name => "address", :rows=>6, :cols=>40 do
      "Please enter your post here"        # Este bloque retorna contenido
    end
    br
    button { "Submit" }                    # Este bloque retorna contenido
  end
end

XML Generado

[xmlgenerator]$ ruby xml_generator.rb > ejemplo.html

[xmlgenerator]$ cat ejemplo.html 
<!-- This is a simple HTML form -->
<form name='registration' action='http://www.example.com/register.cgi' method='GET' enctype='application/x-www-form-urlencoded'>Name:<br/>
<input name='name' type='text'/>
<br/>
Post:<br/>
<textarea name='address' rows='6' cols='40'>Please enter your post here</textarea>
<br/>
<button type='submit'>Submit</button>
</form>

Visualización del HTML Generado

Segundo DSL: Lenguaje para la Generación del DSL Anterior y su Validación

Este DSL permite describir una gramática XML:

Que tags son permitidos y para cada tag que atributos son legales y de que tipo son. Se usaría así:

class HTMLForm < XMLGrammar
  element :form, :action => REQ,        # atributo requerido
                 :method => "GET",      # cadena: valor por defecto
                 :enctype => "application/x-www-form-urlencoded",
                 :name => OPT           # opcional
  element :input, :type => "text", :name => OPT, :value => OPT,
                  :maxlength => OPT, :size => OPT, :src => OPT,
                  :checked => BOOL, :disabled => BOOL, :readonly => BOOL
  element :textarea, :rows => REQ, :cols => REQ, :name => OPT,
                     :disabled => BOOL, :readonly => BOOL
  element :button, :name => OPT, :value => OPT,
                   :type => "submit", :disabled => OPT
  element :br
end

El método element es proveído por XMLGrammar y construye un método de instancia con el nombre especificado (por ejemplo, :form) como primer argumento en la subclase (HTMLForm en el ejemplo).

Como segundo argumento opcional recibe un hash especificando los atributos legales del elemento y de que tipo son (REQ por requerido, OPT por opcional, una String como en :method => "GET" indica valor por defecto y BOOL para atributos cuyo valor es su propio nombre.

En el código anterior se crean métodos form, input, texarea, button y br en la clase HTMLForm.

La Clase XMLGrammar

class XMLGrammar
  # Create an instance of this class, specifying a stream or object to
  # hold the output. This can be any object that responds to <<(String).
  def initialize(out)
    @out = out  # Remember where to send our output
  end

  # Invoke the block in an instance that outputs to the specified stream.
  def self.generate(out, &block)
    new(out).instance_eval(&block)
  end

  # Define an allowed element (or tag) in the grammar.
  # This class method is the grammar-specification DSL
  # and defines the methods that constitute the XML-output DSL.
  def self.element(tagname, attributes={})
    @allowed_attributes ||= {}
    @allowed_attributes[tagname] = attributes

    class_eval %Q{
      def #{tagname}(attributes={}, &block)
        tag(:#{tagname},attributes,&block)
      end
    }
  end

  # These are constants used when defining attribute values.
  OPT = :opt     # for optional attributes
  REQ = :req     # for required attributes
  BOOL = :bool   # for attributes whose value is their own name

  def self.allowed_attributes
    @allowed_attributes
  end

  # Output the specified object as CDATA, return nil.
  def content(text)
    @out << text.to_s
    nil
  end

  # Output the specified object as a comment, return nil.
  def comment(text)
    @out << "<!-- #{text} -->\n"
    nil
  end

  # Output a tag with the specified name and attribute.
  # If there is a block, invoke it to output or return content.
  # Return nil.
  def tag(tagname, attributes={})
    # Output the tag name
    @out << "<#{tagname}"

    # Get the allowed attributes for this tag.
    allowed = self.class.allowed_attributes[tagname]

    # First, make sure that each of the attributes is allowed.
    # Assuming they are allowed, output all of the specified ones.
    attributes.each_pair do |key,value|
      raise "unknown attribute: #{key}" unless allowed.include?(key)
      @out << " #{key}='#{value}'"
    end

    # Now look through the allowed attributes, checking for 
    # required attributes that were omitted and for attributes with
    # default values that we can output.
    allowed.each_pair do |key,value|
      # If this attribute was already output, do nothing.
      next if attributes.has_key? key
      if (value == REQ)
        raise "required attribute '#{key}' missing in <#{tagname}>"
      elsif value.is_a? String
        @out << " #{key}='#{value}'"
      end
    end

    if block_given?
      # This block has content
      @out << '>'             # End the opening tag
      content = yield         # Invoke the block to output or return content
      if content              # If any content returned
        @out << content.to_s  # Output it as a string
      end
      @out << "</#{tagname}>\n" # Close the tag
    else 
      # Otherwise, this is an empty tag, so just close it.
      @out << "/>\n"
    end
    nil # Tags output themselves, so they don't return any content.
  end
end

Código Completo

[~/chapter8ReflectionandMetaprogramming/DSL/xmlgenerator(master)]$ pwd -P
/Users/casiano/Google Drive/src/ruby/TheRubyProgrammingLanguage/chapter8ReflectionandMetaprogramming/DSL/xmlgenerator

class XMLGrammar
  # Create an instance of this class, specifying a stream or object to
  # hold the output. This can be any object that responds to <<(String).
  def initialize(out)
    @out = out  # Remember where to send our output
  end

  # Invoke the block in an instance that outputs to the specified stream.
  def self.generate(out, &block)
    new(out).instance_eval(&block)
  end

  # Define an allowed element (or tag) in the grammar.
  # This class method is the grammar-specification DSL
  # and defines the methods that constitute the XML-output DSL.
  def self.element(tagname, attributes={})
    @allowed_attributes ||= {}
    @allowed_attributes[tagname] = attributes

    class_eval %Q{
      def #{tagname}(attributes={}, &block)
        tag(:#{tagname},attributes,&block)
      end
    }
  end

  # These are constants used when defining attribute values.
  OPT = :opt     # for optional attributes
  REQ = :req     # for required attributes
  BOOL = :bool   # for attributes whose value is their own name

  def self.allowed_attributes
    @allowed_attributes
  end

  # Output the specified object as CDATA, return nil.
  def content(text)
    @out << text.to_s
    nil
  end

  # Output the specified object as a comment, return nil.
  def comment(text)
    @out << "<!-- #{text} -->\n"
    nil
  end

  # Output a tag with the specified name and attribute.
  # If there is a block, invoke it to output or return content.
  # Return nil.
  def tag(tagname, attributes={})
    # Output the tag name
    @out << "<#{tagname}"

    # Get the allowed attributes for this tag.
    allowed = self.class.allowed_attributes[tagname]

    # First, make sure that each of the attributes is allowed.
    # Assuming they are allowed, output all of the specified ones.
    attributes.each_pair do |key,value|
      raise "unknown attribute: #{key}" unless allowed.include?(key)
      @out << " #{key}='#{value}'"
    end

    # Now look through the allowed attributes, checking for 
    # required attributes that were omitted and for attributes with
    # default values that we can output.
    allowed.each_pair do |key,value|
      # If this attribute was already output, do nothing.
      next if attributes.has_key? key
      if (value == REQ)
        raise "required attribute '#{key}' missing in <#{tagname}>"
      elsif value.is_a? String
        @out << " #{key}='#{value}'"
      end
    end

    if block_given?
      # This block has content
      @out << '>'             # End the opening tag
      content = yield         # Invoke the block to output or return content
      if content              # If any content returned
        @out << content.to_s  # Output it as a string
      end
      @out << "</#{tagname}>\n" # Close the tag
    else 
      # Otherwise, this is an empty tag, so just close it.
      @out << "/>\n"
    end
    nil # Tags output themselves, so they don't return any content.
  end
end

class HTMLForm < XMLGrammar
  element :form, :action => REQ,
                 :method => "GET",
                 :enctype => "application/x-www-form-urlencoded",
                 :name => OPT
  element :input, :type => "text", :name => OPT, :value => OPT,
                  :maxlength => OPT, :size => OPT, :src => OPT,
                  :checked => BOOL, :disabled => BOOL, :readonly => BOOL
  element :textarea, :rows => REQ, :cols => REQ, :name => OPT,
                     :disabled => BOOL, :readonly => BOOL
  element :button, :name => OPT, :value => OPT,
                   :type => "submit", :disabled => OPT
  element :br
end

HTMLForm.generate(STDOUT) do
  comment "This is a simple HTML form"
  form :name => "registration",
       :action => "http://www.example.com/register.cgi" do
    content "Name:"
    br
    input :name => "name"
    br
    content "Post:"
    br
    textarea :name => "address", :rows=>6, :cols=>40 do
      "Please enter your post here"
    end
    br
    button { "Submit" }
  end
end

Casiano Rodriguez León 2015-01-07