Skip to content

Instantly share code, notes, and snippets.

@mediafinger
Last active November 18, 2022 13:43
Show Gist options
  • Save mediafinger/101d23e2cea98b0697dd698e928ab387 to your computer and use it in GitHub Desktop.
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`
# 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