38

What's the shortest, one-liner way to list all methods defined with attr_accessor? I would like to make it so, if I have a class MyBaseClass, anything that extends that, I can get the attr_accessor's defined in the subclasses. Something like this:

class MyBaseClass < Hash
  def attributes
    # ??
  end
end

class SubClass < MyBaseClass
  attr_accessor :id, :title, :body
end

puts SubClass.new.attributes.inspect #=> [id, title, body]

What about to display just the attr_reader and attr_writer definitions?

Lance
  • 75,200
  • 93
  • 289
  • 503

6 Answers6

56

Extract the attributes in to an array, assign them to a constant, then splat them in to attr_accessor.

class SubClass < MyBaseClass
  ATTRS = [:id, :title, :body]
  attr_accessor(*ATTRS)
end

Now you can access them via the constant:

puts SubClass.ATTRS #=> [:id, :title, :body]
davetapley
  • 17,000
  • 12
  • 60
  • 86
  • 3
    Wouldn't it be `SubClass::ATTRS` ? Your syntax suggests sending the `ATTRS` message to an instance of type `SubClass` – eggie5 Jun 20 '17 at 16:48
43

There is no way (one-liner or otherwise) to list all methods defined by attr_accessor and only methods defined by attr_accessor without defining your own attr_accessor.

Here's a solution that overrides attr_accessor in MyBaseClass to remember which methods have been created using attr_accessor:

class MyBaseClass
  def self.attr_accessor(*vars)
    @attributes ||= []
    @attributes.concat vars
    super(*vars)
  end

  def self.attributes
    @attributes
  end

  def attributes
    self.class.attributes
  end
end

class SubClass < MyBaseClass
  attr_accessor :id, :title, :body
end

SubClass.new.attributes.inspect #=> [:id, :title, :body]
sepp2k
  • 363,768
  • 54
  • 674
  • 675
  • 5
    Minor nitpick, but on line 5, you don't need to call `super(*vars)` since you're passing it the same arguments that the method received; you can just call `super` without any arguments or parentheses, which will automatically pass `*vars`. – Mark Rushakoff Mar 21 '10 at 15:21
  • 3
    @MarkRushakoff: I figured this way it is more obvious what's going on. Many people assume `super` is the same thing as `super()` (and to be honest I'm not sure that I wouldn't like it better if it was). – sepp2k Mar 21 '10 at 15:23
  • 4
    The rationale behind super passing the arguments (and block if any) by default is that usually the interface from doesn't change and it's thus what you want. I'd say embrace Ruby and let others embrace it too, even if that means they have to learn that super is not the same as super()... – Marc-André Lafortune Mar 23 '10 at 04:16
  • Is there a way to do this where instead of using inheritance (subclassing) you could use a Module for the overriding of attr_accessor and then use the Module as a mixin to other classes? I tried taking what is in MyBaseClass and turn that into a Module AttributeAccessors. Then in my class that I wanted to use it, i said `extend AttributeAccessors` but when I would try to access attributes in the instance of the class, it would fail with: `NoMethodError: undefined method 'attributes' for Class:Class` – Robert J Berger Jan 30 '14 at 22:23
7

Heres an alternative using a mixin rather than inheritance:

module TrackAttributes
  def attr_readers
    self.class.instance_variable_get('@attr_readers')
  end

  def attr_writers
    self.class.instance_variable_get('@attr_writers')
  end

  def attr_accessors
    self.class.instance_variable_get('@attr_accessors')
  end

  def self.included(klass)
    klass.send :define_singleton_method, :attr_reader, ->(*params) do
      @attr_readers ||= []
      @attr_readers.concat params
      super(*params)
    end

    klass.send :define_singleton_method, :attr_writer, ->(*params) do
      @attr_writers ||= []
      @attr_writers.concat params
      super(*params)
    end

    klass.send :define_singleton_method, :attr_accessor, ->(*params) do
      @attr_accessors ||= []
      @attr_accessors.concat params
      super(*params)
    end
  end
end

class MyClass
  include TrackAttributes

  attr_accessor :id, :title, :body
end

MyClass.new.attr_accessors #=> [:id, :title, :body]
  • Is there a way to do `MyClass.attr_accessors` to get the same result using mixins? The inheritance approach from @sepp2k allows both. I can think of doing an extend and an include, but that seems redundant. Is there a better way? – EricC Jan 13 '15 at 16:48
2

Following up on Christian's response, but modifying to use ActiveSupport::Concern...

module TrackAttributes
  extend ActiveSupport::Concern

  included do
    define_singleton_method(:attr_reader) do |*params|
      @attr_readers ||= []
      @attr_readers.concat params
      super(*params)
    end

    define_singleton_method(:attr_writer) do |*params|
      @attr_writers ||= []
      @attr_writers.concat params
      super(*params)
    end

    define_singleton_method(:attr_accessor) do |*params|
      @attr_accessors ||= []
      @attr_accessors.concat params
      super(*params)
    end
  end

  def attr_readers
    self.class.instance_variable_get('@attr_readers')
  end

  def attr_writers
    self.class.instance_variable_get('@attr_writers')
  end

  def attr_accessors
    self.class.instance_variable_get('@attr_accessors')
  end

end
smooshy
  • 66
  • 1
  • 4
2

Class definitions

class MyBaseClass
  attr_writer :an_attr_writer
  attr_reader :an_attr_reader

  def instance_m
  end

  def self.class_m
  end
end

class SubClass < MyBaseClass
  attr_accessor :id

  def sub_instance_m
  end

  def self.class_sub_m
  end
end

Call as class methods

p SubClass.instance_methods - Object.methods    
p MyBaseClass.instance_methods - Object.methods

Call as instance methods

a = SubClass.new
b = MyBaseClass.new

p a.methods - Object.methods
p b.methods - Object.methods

Both will output the same

#=> [:id, :sub_instance_m, :id=, :an_attr_reader, :instance_m, :an_attr_writer=]
#=> [:an_attr_reader, :instance_m, :an_attr_writer=]

How to tell which are writer reader and accessor?

attr_accessor is both attr_writer and attr_reader

attr_reader outputs no = after the method name

attr_writer outputs an = after the method name

You can always use a regular expression to filter that output.

jasonleonhard
  • 12,047
  • 89
  • 66
0

It could be filtered with the equal sign matching logic:

methods = (SubClass.instance_methods - SubClass.superclass.instance_methods).map(&:to_s)
methods.select { |method| !method.end_with?('=') && methods.include?(method + '=') }
#> ["id", "title", "body"]
pocheptsov
  • 1,936
  • 20
  • 24