The best kittens, technology, and video games blog in the world.

Monday, July 31, 2006

XML-oriented programming with Ruby

Photo from Commons by Bfe, GFDL.
XML programming is important. There are even special languages that make it easy. But do we need a new language or can we have the same level of expressiveness in an existing one, like Ruby ? Let's try to program XML - but not starting from implementation and bending our mental model until it fits, but starting from a mental model and bending the language ! And let's care about the real thing first, DTD, entities, validation and whatnot are less common so they can wait. The basic structure should of course be an XML node object:
a_node = XML.new(:a, {:href => "http://www.google.com/"}, "Google")
What do we want the constructor to accept ? Well, the first argument must be a Symbol (or a String) with XML tag name. Then the attributes in a hash table. Then the contents - random collection of strings and XML nodes. Notice that we can easily make attribute hash optional, if the second attribute is not a hash it's simply part of the content.
a = XML.new(:br)
b = XML.new(:br, {:style => "clear: right"})
c = XML.new(:h3, "My new blog")
d = XML.new(:body, c, "Hello, ", XML.new(:a, {:href=>"http://earth.google.com/"}, "world"), "!")
Of course our XML object will have the appropriate to_s method, so we can simply say print xml_node to get the correctly formatted XML. XML should also support XML.parse(xml_as_string), XML.from_file(file_name_or_handler) and XML.from_url(url) methods for fetching existing XML files. We probably also want to be able to easily generate subclasses of XML:
H3 = XML.make_subclass(:h3)
a = H3.new("Hello")
The subclasses factory may even get much complex, but let's stay with the easy one for now. We would like our XML nodes to support the "natural" operations like:
a_node[:href]  = "http://en.wikipedia.org/"
body_node << H3.new("New section")
node.each{|c| print c}
node = node.map{|c| if c.name == :h3 then H2.new(c.attr, c.contents) else c end}
Because Ruby 1.8 doesn't support arbitrary *-expansion, we probably also want to automatically expand Arrays passed as constructor arguments - XML nodes cannot have arrays as elements anyway. The we will be able to say (compare with analogous program in CDuce):
require 'magic/xml'
include XML::XHTML # Import all XHTML subclasses

posts = XML.from_url "http://taw:password@del.icio.us/api/posts/all"

def format_posts(posts)
  posts.contents.map{|post| LI.new(A.new({:href=>post[:href],}, post[:description])) }
end

print HTML.new(
  HEAD.new(
    TITLE.new("del.icio.us summary magically generated by Ruby")
  ),
  BODY.new(
    H3.new("The list"),
    format_posts(posts)
  )
)
Yay, isn't it cute ? Uhm, now it only needs to be implemented :-D

No comments: