Search code examples
ruby-on-railsrubygemsruby-on-rails-5active-attr

Rails ActiveAttr Gem, manipulation of attributes within Class?


I have a Rails 5 class which includes ActiveAttr::Model, ActiveAttr:MassAssignment and ActiveAttr::AttributeDefaults.

It defines a couple of attributes using the method attribute and has some instance methods. I have some trouble manipulating the defined attributes. My problem is how to set an attribute value within the initializer. Some code:

class CompanyPresenter
  include ActiveAttr::Model
  include ActiveAttr::MassAssignment
  include ActiveAttr::AttributeDefaults

  attribute :identifier
  # ...
  attribute :street_address
  attribute :postal_code
  attribute :city
  attribute :country
  # ...
  attribute :logo
  attribute :schema_org_identifier
  attribute :productontology
  attribute :website

def initialize(attributes = nil, options = {})
    super
    fetch_po_field
  end

  def fetch_po_field
    productontology = g_i_f_n('ontology') if identifier
  end

  def uri
    @uri ||= URI.parse(website)
  end
  # ...
end

As I have written it, the method fetch_po_field does not work, it thinks that productontology is a local variable (g_i_f_n(...) is defined farther down, it works and its return value is correct). The only way I have found to set this variable is to write self.productontology instead. Moreover, the instance variable @uri is not defined as an attribute, instead it is written down only in this place and visible from outside.

Probably I have simply forgotten the basics of Ruby and Rails, I've done this for so long with ActiveRecord and ActiveModel. Can anybody explain why I need to write self.productontology, using @productontology doesn't work, and why my predecessor who wrote the original code mixed the @ notation in @uri with the attribute-declaration style? I suppose he must have had some reason to do it like this.

I am also happy with any pointers to documentation. I haven't been able to find docs for ActiveAttr showing manipulation of instance variables in methods of an ActiveAttr class.

Thank you :-)


Solution

  • To start you most likely don't need the ActiveAttr gem as it really just replicates APIs that are already available in Rails 5.

    See https://api.rubyonrails.org/classes/ActiveModel.html.

    As I have written it, the method fetch_po_field does not work, it thinks that productontology is a local variable.

    This is really just a Ruby thing and has nothing to do with the Rails Attributes API or the ActiveAttr gem.

    When using assignment you must explicitly set the recipient unless you want to set a local variable. This line:

    self.productontology = g_i_f_n('ontology') if identifier
    

    Is actually calling the setter method productontology= on self using the rval as the argument.

    Can anybody explain why I need to write self.productontology, using @productontology doesn't work

    Consider this plain old ruby example:

    class Thing
      def initialize(**attrs)
        @storage = attrs
      end
    
      def foo
        @storage[:foo]
      end
    
      def foo=(value)
        @storage[:foo] = value
      end
    end
    
    irb(main):020:0> Thing.new(foo: "bar").foo
    => "bar"
    irb(main):021:0> Thing.new(foo: "bar").instance_variable_get("@foo")
    => nil
    

    This looks quite a bit different then the standard accessors you create with attr_accessor. Instead of storing the "attributes" in one instance variable per attribute we use a hash as the internal storage and create accessors to expose the stored values.

    The Rails attributes API does the exact same thing except its not just a simple hash and the accessors are defined with metaprogramming. Why? Because Ruby does not let you track changes to simple instance variables. If you set @foo = "bar" there is no way the model can track the changes to the attribute or do stuff like type casting.

    When you use attribute :identifier you're writing both the setter and getter instance methods as well as some metadata about the attribute like its "type", defaults etc. which are stored in the class.