views:

168

answers:

2

I am making a framework where objects should be created a according to a predefined XML file. For example, if in the xml file the following occurs:

<type name="man"> 
    <property name="name" type="string">
    <property name="height" type="int">
    <property name="age" type="int">
    <property name="profession" type="string" value="unemployed">
</type>

In Ruby, this should allow you to create an object as following:

man = Man.new('John', 188, 30)

Note: For fields where 'value' is defined in the xml, no value should be accepted in the initialize method, but should rather be set by the class itself as a default value.

Any recommended implementations for this? I am current watching Dave Thomas' screencasts about meta programming, so this looks very suitable, but any suggestions would be appreciated!

+2  A: 

Well, first you'll need to parse the XML. You could use a library like Hpricot or Nokogiri for that. Here's an example that will create a Man class given that type node from Nokogiri:

def define_class_from_xml(node, in_module = Object)
  class_name = node['name'].dup
  class_name[0] = class_name[0].upcase
  new_class = in_module.const_set(class_name, Class.new)

  attributes = node.search('property').map {|child| child['name']}
  attribute_values = node.search('property[@value]').inject({}) do |hash, child|
    hash[child['name']] = child['value']
    hash
  end

  new_class.class_eval do
    attr_accessor *attributes
    define_method(:initialize) do |*args|
      needed_args_count = attributes.size - attribute_values.size
      if args.size < needed_args_count
        raise ArgumentError, "#{args.size} arguments given; #{needed_args_count} needed"
      end
      attributes.zip(args).each {|attr, val| send "#{attr}=", val}
      if args.size < attributes.size
        attributes[args.size..-1].each {|attr| send "#{attr}=", attribute_values[attr]}
      end
    end
  end
end

It's not the most elegant bit of metaprogramming you'll ever see, but I can't think of how to make it any simpler at the moment. The first bit gets the class name and makes an empty class by that name, the second gets the attributes from the XML, and the third is the only real metaprogramming. It's a class definition using that information (with the minor added hassle of needing to check the argument count, since we can't tell Ruby "X number of arguments is required").

Chuck
If I add the parameters in a certain order, can I extract them in this same order? I tried with 'instance_variables' but git them in the opposite order. Is the order guaranteed?
lk
Instance variables aren't an ordered collection, so they don't really have a guaranteed order. The order is actually different between Ruby versions. If you want to store a sequence of instance variable names, the best bet is just to make an array and store that yourself.
Chuck
A: 

Not getting into the xml parsing, but assuming you have got as far as extracting the following array:

name = 'Man'
props = [["name", "string"], 
         ["height", "int"],
         ["age", "int"],
         ["profession", "string", "unemployed"]]

here's code to create the class:

def new_class(class_name, attrs)
  klass = Class.new do
    attrs.each do |attr, type, val|
      if val
        attr_reader attr
      else
        attr_accessor attr
      end
    end
  end

  init = ""
  attrs.each do |attr, type, val|
    if val
      if type == "string"
        init << "@#{attr} = '#{val}'\n"
      else # assuming all other types are numeric
        init << "@#{attr} = #{val}\n"
      end
    else
      init << "@#{attr} = #{attr}\n"
    end
  end

  args = attrs.select {|attr, type, val| val.nil?}.map {|attr, type, val| attr}.join(",")

  klass.class_eval %{
    def initialize(#{args})
      #{init}
    end
  }

  Object.const_set class_name, klass
end

name = 'Man'
props = [["name", "string"], ["height", "int"], ["age", "int"], ["profession", "string", "unemployed"]]
new_class(name, props)

man = Man.new('John', 188, 30)

p man
Martin DeMello