Initialize a Ruby class from an arbitrary hash, but only keys with matching accessors

后端 未结 6 572
遇见更好的自我
遇见更好的自我 2021-01-01 04:00

Is there a simple way to list the accessors/readers that have been set in a Ruby Class?

class Test
  attr_reader :one, :two

  def initialize
    # Do someth         


        
相关标签:
6条回答
  • 2021-01-01 04:23

    You could override attr_reader, attr_writer and attr_accessor to provide some kind of tracking mechanism for your class so you can have better reflection capability such as this.

    For example:

    class Class
      alias_method :attr_reader_without_tracking, :attr_reader
      def attr_reader(*names)
        attr_readers.concat(names)
        attr_reader_without_tracking(*names)
      end
    
      def attr_readers
        @attr_readers ||= [ ]
      end
    
      alias_method :attr_writer_without_tracking, :attr_writer
      def attr_writer(*names)
        attr_writers.concat(names)
        attr_writer_without_tracking(*names)
      end
    
      def attr_writers
        @attr_writers ||= [ ]
      end
    
      alias_method :attr_accessor_without_tracking, :attr_accessor
      def attr_accessor(*names)
        attr_readers.concat(names)
        attr_writers.concat(names)
        attr_accessor_without_tracking(*names)
      end
    end
    

    These can be demonstrated fairly simply:

    class Foo
      attr_reader :foo, :bar
      attr_writer :baz
      attr_accessor :foobar
    end
    
    puts "Readers: " + Foo.attr_readers.join(', ')
    # => Readers: foo, bar, foobar
    puts "Writers: " + Foo.attr_writers.join(', ')
    # => Writers: baz, foobar
    
    0 讨论(0)
  • 2021-01-01 04:25

    This is what I use (I call this idiom hash-init).

     def initialize(object_attribute_hash = {})
      object_attribute_hash.map { |(k, v)| send("#{k}=", v) }
     end
    

    If you are on Ruby 1.9 you can do it even cleaner (send allows private methods):

     def initialize(object_attribute_hash = {})
      object_attribute_hash.map { |(k, v)| public_send("#{k}=", v) }
     end
    

    This will raise a NoMethodError if you try to assign to foo and method "foo=" does not exist. If you want to do it clean (assign attrs for which writers exist) you should do a check

     def initialize(object_attribute_hash = {})
      object_attribute_hash.map do |(k, v)| 
        writer_m = "#{k}="
        send(writer_m, v) if respond_to?(writer_m) }
      end
     end
    

    however this might lead to situations where you feed your object wrong keys (say from a form) and instead of failing loudly it will just swallow them - painful debugging ahead. So in my book a NoMethodError is a better option (it signifies a contract violation).

    If you just want a list of all writers (there is no way to do that for readers) you do

     some_object.methods.grep(/\w=$/)
    

    which is "get an array of method names and grep it for entries which end with a single equals sign after a word character".

    If you do

      eval("@#{opt} = \"#{val}\"")
    

    and val comes from a web form - congratulations, you just equipped your app with a wide-open exploit.

    0 讨论(0)
  • 2021-01-01 04:26

    There's no built-in way to get such a list. The attr_* functions essentially just add methods, create an instance variable, and nothing else. You could write wrappers for them to do what you want, but that might be overkill. Depending on your particular circumstances, you might be able to make use of Object#instance_variable_defined? and Module#public_method_defined?.

    Also, avoid using eval when possible:

    def initialize(opts)
      opts.delete_if{|opt,val| not the_list_of_readers.include?(opt)}.each do |opt,val|
        instance_variable_set "@#{opt}", val
      end
    end
    
    0 讨论(0)
  • 2021-01-01 04:28

    Try something like this:

    class Test
      attr_accessor :foo, :bar
    
      def initialize(opts = {})
        opts.each do |opt, val|
          send("#{opt}=", val) if respond_to? "#{opt}="
        end
      end
    end
    
    test = Test.new(:foo => "a", :bar => "b", :baz => "c")
    
    p test.foo # => nil
    p test.bar # => nil
    p test.baz # => undefined method `baz' for #<Test:0x1001729f0 @bar="b", @foo="a"> (NoMethodError)
    

    This is basically what Rails does when you pass in a params hash to new. It will ignore all parameters it doesn't know about, and it will allow you to set things that aren't necessarily defined by attr_accessor, but still have an appropriate setter.

    The only downside is that this really requires that you have a setter defined (versus just the accessor) which may not be what you're looking for.

    0 讨论(0)
  • 2021-01-01 04:41

    You can look to see what methods are defined (with Object#methods), and from those identify the setters (the last character of those is =), but there's no 100% sure way to know that those methods weren't implemented in a non-obvious way that involves different instance variables.

    Nevertheless Foo.new.methods.grep(/=$/) will give you a printable list of property setters. Or, since you have a hash already, you can try:

    def initialize(opts)
      opts.each do |opt,val|
        instance_variable_set("@#{opt}", val.to_s) if respond_to? "#{opt}="
      end
    end
    
    0 讨论(0)
  • 2021-01-01 04:44

    Accessors are just ordinary methods that happen to access some piece of data. Here's code that will do roughly what you want. It checks if there's a method named for the hash key and sets an accompanying instance variable if so:

    def initialize(opts)
      opts.each do |opt,val|
        instance_variable_set("@#{opt}", val.to_s) if respond_to? opt
      end
    end
    

    Note that this will get tripped up if a key has the same name as a method but that method isn't a simple instance variable access (e.g., {:object_id => 42}). But not all accessors will necessarily be defined by attr_accessor either, so there's not really a better way to tell. I also changed it to use instance_variable_set, which is so much more efficient and secure it's ridiculous.

    0 讨论(0)
提交回复
热议问题