ActiveModel dynamic attribute type

Viewed 508

Is it possible to decide which type casts an attribute in ActiveModel::Attributes on runtime? I have the following code

class DynamicType < ActiveModel::Type::Value
  def cast(value)
    value # here I don't have access to the underlying instance of MyModel
  end
end

class MyModel
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :value, DynamicType.new
  attribute :type, :string
end

MyModel.new(type: "Integer", value: "12.23").value

I'd like to decide based on the assigned value of type how to cast the value attribute. I tried it with a custom type, but it turns out, inside the #cast method you don't have access to the underlying instance of MyModel (which also could be a good thing, thinking of separation of concerns)

I also tried to use a lambda block, assuming that maybe ActiveModel see's an object that responds to #call and calls this block on runtime (it doesn't):

class MyModel
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :value, ->(my_model) {
    if my_model.type == "Integer"
      # some casting logic
    end
  }
  attribute :type, :string
end

# => throws error
# /usr/local/bundle/gems/activemodel-5.2.5/lib/active_model/attribute.rb:71:in `with_value_from_user': undefined method `assert_valid_value' for #<Proc:0x000056202c03cff8 foo.rb:15 (lambda)> (NoMethodError)

Background: type comes from a DB field and can have various classes in it that do far more than just casting an Integer.

I could just do a def value and build the custom logic there, but I need this multiple times and I also use other ActiveModel features like validations, nested attributes... so I would have to take care about the integration myself.

So maybe there is a way to do this with ActiveModel itself.

1 Answers

Your are storing the value as a string in the DB, so ActiveRecord will retreive it as a string. You will need to convert it manually. Rails provides these methods, that I am sure you are familiar with:

"1".to_i => to integer => 1
"foo.to_sym => to symbol => :foo
"1".to_f => to float => "1.0"
123.to_s => to string => "123"
(1..10).to_a => to array => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Then in your model you could do:

def value
  case type
  when "Integer"
   value.to_i
  when "Float"
   value.to_f
  ...
end

This could work but I see a problem with this approach. You decalre an attribute with the name type, you could get some erros of Rails complaining that type is a reserve word for STI, or get weird bugs, I suggest you to rename it.

Related