4

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.

23tux
  • 14,104
  • 15
  • 88
  • 187
  • As you said ActiveModel::Type::Value has no access to the instance itself. https://github.com/rails/rails/blob/main/activemodel/lib/active_model/type/value.rb, it indeed seems to be on purpose. But why can't you put the value method as some shared module/concern and share it that way? – Joel Blum Apr 18 '21 at 08:24
  • Sharing it via a module is of course possible, but I thought it would be more declarative to do it via the `attribute` method. And yes I agree, not having access to the underlying instance seems on purpose. Maybe there is just no way of achieving this without monkey patching / enhancing ActiveSupport. – 23tux Apr 18 '21 at 09:56

1 Answers1

-2

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.

Juan Artau
  • 357
  • 2
  • 13