Reputation: 6723
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!
Upvotes: 0
Views: 305
Reputation: 12346
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
Upvotes: 0
Reputation: 237110
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").
Upvotes: 2