DmitryTsepelev / store_model

Work with JSON-backed attributes as ActiveRecord-ish models
MIT License
1.08k stars 87 forks source link

Dynamic model types #92

Closed 23tux closed 3 years ago

23tux commented 3 years ago

Is it possible to define the type of a store model based on another field? For example something like this:

class Product < ApplicationRecord
  attribute :configs, ->(product) { product.ticket? ? TicketConfig.to_array_type : BaseConfig.to_type }
end

I'm not sure if this is even a StoreModel related question, maybe it has more to do with how ActiveModel includes it's attributes.

So far, I have overwritten the getter of config to achieve this, but maybe there is a way to do it with build in functionality:

class Product < ApplicationRecord
  def configs
    type = ticket? ? TicketConfig.to_array_type : BaseConfig.to_type
    type.cast(super)
  end

  validate do
    config_errors = Array.wrap(config).reject(&:valid?)
    errors.add(:config, config_errors.join(", ")) if config_errors.any?
  end
end
DmitryTsepelev commented 3 years ago

Hi @23tux! Yeah, we have a OneOf for that.

23tux commented 3 years ago

Thanks for your answer, didn't know that!

But as far as I can see, you only have access to the json column itself, and not other columns of the record, am I right?

I also wonder how to decide if you want to use to_type or to_array_type? In the OneOf Example, only the base class is set inside the block, not the type itself.

DmitryTsepelev commented 3 years ago

and not other columns of the record

Yeah, this is how it works now, and there is a chance that we do not have access to the "parent" model at the moment of deserialization (so we won't be able to refer to other fields).

how to decide if you want to use to_type or to_array_type?

to_type is used when there is a single JSON (e.g., { color: "red" }), while to_array_type wraps array of JSONs (e.g., [{ color: "red" }, { color: "green" }])

23tux commented 3 years ago

@DmitryTsepelev thanks, I've managed to inject the type into the json column, so this works.

One last question: Is it possible to use the ActiveRecord::Type::Json type in StoreModel.one_of?

I tried to use

OneOfConfigurations = StoreModel.one_of do |json|
  json["_model"]&.constantize || ActiveRecord::Type::Json
end

but this throws an error

StoreModel::Types::ExpandWrapperError: ActiveRecord::Type::Json is an invalid model klass

For some models I have to provide a fallback if the underlying data doesn't match the StoreModel, and otherwise ActiveSupport throws a

ActiveModel::UnknownAttributeError: unknown attribute 'foo' for MyModel.
DmitryTsepelev commented 3 years ago

StoreModel::Types::ExpandWrapperError is thrown when the type that returned from the block does not include a StoreModel::Model module (code is here). I guess it might be possible to define a custom class including that module that behaves like ActiveRecord::Type::Json.

unknown attribute 'foo'

Interesting, sounds like JSON stored in database does not have foo field, while it's defined in the StoreModel class, am I right? I'm asking because we do handle the opposite scenario using unknown attributes.

23tux commented 3 years ago

@DmitryTsepelev thanks for the hint to the unknown attributes! That solves indeed my problem, I just provide a generic fallback model.