Last active
November 18, 2022 13:43
-
-
Save mediafinger/101d23e2cea98b0697dd698e928ab387 to your computer and use it in GitHub Desktop.
Example code to allow instances of the `User` model to have multiple `roles`
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Example code to allow instances of the `User` model to have multiple `roles`: | |
# Migration ------------------- | |
class AddRolesToUsers < ActiveRecord::Migration[7.0] | |
def change | |
add_column :users, :roles, :text, array: true, default: [], null: false | |
end | |
end | |
# Model ---------------------- | |
class User < ApplicationRecord | |
VALID_ROLES = [ | |
"visitor", | |
"registered", | |
"pro", | |
"premium", | |
"admin", | |
].freeze | |
# All the following methods are optional convenience methods! | |
# None of them are necessary to make this work! | |
# But they help to add or remove single roles to a user. | |
after_initialize :define_has_role_methods | |
def add_role!(role) | |
add_role(role).save! | |
end | |
def add_role(role) | |
unless VALID_ROLES.include?(role.to_s) | |
errors.add(:roles, "invalid role, must be in #{VALID_ROLES}") | |
return self | |
end | |
roles << role.to_s | |
roles.uniq! | |
self | |
end | |
def delete_role!(role) | |
delete_role(role).save! | |
end | |
def delete_role(role) | |
self.roles -= Array(role.to_s) | |
self | |
end | |
private | |
# optional convenience methods | |
# defines visitor?, registered?, pro?, premium?, admin? ... query methods | |
def define_has_role_methods | |
VALID_ROLES.each do |role| | |
self.class.send(:define_method, "#{role}?".to_sym) do | |
roles.include?(role.to_s) | |
end | |
end | |
end | |
end | |
# Factory ---------------------- | |
FactoryBot.define do | |
factory :user, class: "User" do | |
roles { [User::VALID_ROLES.sample] } | |
end | |
end | |
# Validator --------------------- | |
# optional to generation better error messages | |
# and let the user know exactly which elements are invalid | |
# | |
class ArrayInclusionValidator < ActiveModel::EachValidator | |
def validate_each(record, attribute, value) | |
values = Array(value) || [] | |
rejected = [] | |
rejected.concat(values - options[:in]) if options[:in] | |
rejected.concat(values.find_all { |v| !options[:proc].call(v) }) if options[:proc] | |
return if rejected.blank? | |
formatted_rejected = rejected.uniq.collect(&:inspect).join(", ") | |
record.errors.add( | |
attribute, :inclusion, **options.except(:in).merge!(rejected_values: formatted_rejected, value:) | |
) | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment