views:

80

answers:

2

I am trying to dynamically define methods based on xml mappings. This works really well. However I want to create an instance variable that is a array of the dynamically defined methods.

My code looks something like this

  def xml_attr_reader(*args)
    xml_list = ""
    args.each do |arg|
      string_val = "def #{arg}; " +
                   "   xml_mapping.#{arg}; " +
                   "end; "
      self.class_eval string_val
      xml_hash = xml_list + "'#{arg}',"
    end

    self.class_eval "@xml_attributes = [] if @xml_attributes.nil?;" +
                    "@xml_attributes = @xml_attributes + [#{xml_list}];" +
                    "puts 'xml_attrs = ' + @xml_attributes.to_s;" +
                    "def xml_attributes;" +
                    "  puts 'xml_attrs = ' + @xml_attributes.to_s;" +
                    "  @xml_attributes;" +
                    "end"
  end

So everything works except when I call xml_attributes on an instance it return null (and prints out 'xml_attrs = ').

While the puts before the definition actually prints out the correct array. (when I instantiate the instance)


Update - kandadaboggu solution below works. I really did not explain my question fully though so here is more information. I want to call xml_attr_reader within a class that extends XmlConfig. Basically I want it to work in the same way as attr_reader of active record.

Taking kandadaboggu solution of

class XmlConfig
  def xml_attr_reader(*args)
    args.each do |arg|
      class_eval(<<-RUBY, __FILE__, __LINE__)
        def #{arg}
          xml_mapping.#{arg}
        end
      RUBY
    end
    unless respond_to?(:xml_attributes)
      class_eval(<<-RUBY, __FILE__, __LINE__)
        attr_accessor :xml_attributes
      RUBY
    end
    (self.xml_attributes ||=[]).concat(args)
  end
end

and

config = XmlConfig.new
config.xml_attr_reader("age", "name")
config.age #=> age
config.name #=> name
config.xml_attributes #=> ["age", "name" ]

config.xml_attr_reader("city")
config.city #=> city
config.xml_attributes #=> ["age", "name", "city"]

What I really want is this (where I had XmlConfig as a module not a Class in my version though)

class Event < ActiveWhatever
  extend XmlConfig
  xml_attr_reader :age, :name
  xml_attr_reader :email, :location
end


class PrivateEvent < Event
  xml_attr_reader :owner, :password
end
+1  A: 

Try this:

class XmlConfig
  def xml_attr_reader(*args)
    args.each do |arg|
      class_eval(<<-RUBY, __FILE__, __LINE__)
        def #{arg}
          xml_mapping.#{arg}
        end
      RUBY
    end
    unless respond_to?(:xml_attributes)
      class_eval(<<-RUBY, __FILE__, __LINE__)
        attr_accessor :xml_attributes
      RUBY
    end
    (self.xml_attributes ||=[]).concat(args)
  end
end

Now you can make the following calls:

config = XmlConfig.new
config.xml_attr_reader("age", "name")
config.age #=> age
config.name #=> name
config.xml_attributes #=> ["age", "name" ]

config.xml_attr_reader("city")
config.city #=> city
config.xml_attributes #=> ["age", "name", "city"]

Note: All the methods are instance methods.

KandadaBoggu
So this almost works. It works for the case of one call to xml_attr_reader in a class. But if I have may attributes I call the method multiple times to keep my lines < 80 characters. The last call to xml_attr_reader is overwrites the xml_attributes.I tried to concatenate the @xml_attributes but it is always nil when accessed in this dynamic generation.I also have the issue of inheritance where xml_attr_reader may get called in a parent also. I was the list of attributes to be the union of the parent and childs. Thanks for the help, any other ideas?
Will
I have made the change to fix the issue. Let me know if it works for you.
KandadaBoggu
Thanks for trying but it does not work. I put in a print statement after the if (self.respond_to?(:xml_attributes)) but it never gets called.
Will
There was a syntax error in my code. I had an extra bracket. If your code has that bracket, I suggest you try with the new code. I will test this later today and let you know.
KandadaBoggu
I have fixed and tested the code. It works for me. Let me know if it works for you.
KandadaBoggu
Look at the updated code.
KandadaBoggu
Ah, so I try what you have and that works. It is a little different from what I am attempting to do though. See my edited question above
Will
I have added a new answer for your amended question. Let me know if it works for you.
KandadaBoggu
+1  A: 

Here is a solution for your amended question.

   module XmlConfig
    def self.included(base)
      base.extend ClassMethods
      base.class_inheritable_array(:xml_attributes)
      base.xml_attributes = []
    end

    module ClassMethods    
      def xml_attr_reader(*args)
        args.each do |arg|
          self.class_eval(<<-RUBY, __FILE__, __LINE__)
            def #{arg}
              xml_mapping.#{arg}
            end
          RUBY
        end
        self.xml_attributes = args
      end
    end
  end

Now you can include the module in any class.

  class Event < ActiveWhatever
    include XmlConfig
    xml_attr_reader :age, :name
    xml_attr_reader :email, :location
  end


  class PrivateEvent < Event
    xml_attr_reader :owner, :password
  end

Let us test the result:

Event.xml_attributes        # ->[:age, :name, :email, :location]
PrivateEvent.xml_attributes # ->[:age, :name, :email, :location, 
                            #                 :owner, :password]

e= Event.new(...)
e.age     # -> 27
pe= PrivateEvent.new(...)
pe.owner  # -> Will
KandadaBoggu
Excellent answer. Works like a charm. Thanks for persisting with this one, it is much appreciated.
Will