store_model
store_model copied to clipboard
Array of enums
Hi! We often have the situation where a model has an array of strings that needs to be validated to only include certain values, and we need some predicate methods to check if a model has a certain value inside this array:
class User < ApplicationRecord
after_initialize { self.roles = Array.wrap(roles) }
ROLES = %w[admin user reporter].freeze
validate do
errors.add(:roles, :invalid_roles) if (roles - ROLES).any?
end
ROLES.each do |role|
define_method("#{role}?") do
roles.include?(role)
end
end
end
And IMHO, that looks suspiciously like a StoreModel use case: An array of enums. Only difference would be that a wrong role would raise an exception instead of a validation error (but in my case this would be ok).
As far as I can tell, this isn't possible right now with StoreModel, am I right? Would it be a feature you would consider to include? I'd be happy to try for a PR if you could give me some hints how to implement a feature like this.
Hi, @23tux!
Right now StoreModel focuses on isolating JSON(B) data from the parent model. In your example an array (where it is stored?) is used inside the ApplicationRecord subclass. Probably custom Rails validator will help here:
class User < ApplicationRecord
...
validates :roles, array: { inclusion: { in: ROLES }
end
I guess that enumerize gem could deal with it. I didn't try though :)
@DmitryTsepelev #roles is a JSON column on the users table. I thought because you have support for enum types on single attributes (in the example here https://github.com/DmitryTsepelev/store_model/blob/master/docs/enums.md it is the key status), it would be a good fit to extend this to support an array of enums.
Consider this example, maybe it's clearer this way:
class Configuration
include StoreModel::Model
enum :role, %i[admin user reporter], default: :user
end
class User < ApplicationRecord
attribute :configuration, Configuration.to_type
end
My user has a configuration with an enum of role that defaults to :user. What if I want multiple roles for a user? I thought an array of enums would be a good solution for this. What do you think?
Aha, in this case having the built-in inclusion validator for arrays makes sense to me!
@FunkyloverOne you are right, enumerize has built in support for Arrays when you serialize them or use mongodb. I still have to find out if it works with json array columns though. But because we already have the store_model gem in our Gemfile, adding another dependency for this simple use case isn't something I'm very happy about
@DmitryTsepelev How would you implement this? If you can give me a push in the right direction, I'm happy to try for a PR.
@23tux This is what should be done to make it work:
- Take a look at docs about custom validators
- Add a new custom validator (we already have one) called
ArrayValidator - Implement the validator (there is a good example)
- Add specs
- Bonus point: add docs 🙂
As a result, it will be possible to add a validation to the StoreModel in a following way:
class Configuration
include StoreModel::Model
ROLES = %i[admin user reporter]
enum :role, ROLES, default: :user
validates :roles, array: { inclusion: { in: ROLES }
end
class User < ApplicationRecord
attribute :configuration, Configuration.to_type
end
Bonus point: it might be helpful to have a shortcut for the enum method to use this validation by default enum :role, ROLES, default: :user, validate: true
@DmitryTsepelev thanks for the kick off!
I'm not sure if I understand the need of a validator when using enums. The current implementation raises an error when an invalid value is assigned, e.g. from your examples
class Configuration
include StoreModel::Model
enum :status, %i[active archived], default: :active
end
config = Configuration.new
config.status = "foo"
# ArgumentError: invalid value 'foo' is assigned
# from /usr/local/bundle/gems/store_model-0.7.0/lib/store_model/types/enum_type.rb:56:in `raise_invalid_value!'
I would suggest to keep the same behaviour for an array of enums: When you push an invalid value into the array, it should raise an error. Do you know what I mean?
@23tux Sorry for the long response, that's a valid point! We don't need to use a validator in the current implementation (however, there is a chance that someone would need to make this behaviour optional in the future)