guise
A typical, quick-and-easy role management system involves users and roles
tables with a join table between them to determine membership to a role:
class User < ActiveRecord::Base
has_many :user_roles
has_many :roles, through: :user_roles
end
class UserRole < ActiveRecord::Bae
belongs_to :user
belongs_to :role
end
class Role < ActiveRecord::Base
has_many :user_roles
has_many :users, through: :user_roles
end
A problem with this is that in simple setups, application behavior tends to be
hard-coded to rely on the existence of a database record representing the role
object in the roles table.
guise de-normalizes the above setup by storing the name of the role as a
column in what would be the join table between users and roles. The allowed
values are limited to the values defined in a declaration on the model that is
meant to have different roles.
Given User and Role models where the Role model has a value column.
class User < ActiveRecord::Base
end
class Role < ActiveRecord::Base
end
By adding the following method call to has_guises to User and guise_for to
Role:
class User < ActiveRecord::Base
has_guises :DeskWorker, :MailForwarder, association: :roles, attribute: :value
end
class Role < ActiveRecord::Base
guise_for :User
end
The equivalent associations, model scopes and validations are configured:
class User < ActiveRecord::Base
has_many :roles
scope :desk_workers, -> { joins(:roles).where(roles: { value: "DeskWorker" }) }
scope :mail_forwarders, -> { joins(:roles).where(roles: { value: "MailForwarder" }) }
def has_role?(value)
roles.detect { |role| role.value == value }
end
def has_roles?(*values)
values.all? { |value| has_role?(value)
end
def has_any_roles?(*values)
values.any? { |value| has_role?(value)
end
def desk_worker?
has_role?("DeskWorker")
end
def mail_forwarder?
has_role?("MailForwarder")
end
end
class Role < ActiveRecord::Base
belongs_to :user
scope :desk_workers, -> { where(value: "DeskWorker") }
scope :mail_forwarders, -> { where(value: "MailForwarder") }
validates(
:value,
presence: true,
uniqueness: { scope: :user_id },
inclusion: { in: %w( DeskWorker MailForwarder ) }
)
end
This allows filtering users by role / type and assigning records a role without requiring an existing record in the database. The predicate methods can be used for permissions / authorization.
Installation
Add this line to your application's Gemfile:
gem 'guise'
Then execute:
$ bundle
Or install it yourself as:
$ gem install guise
Usage
Create a table to store your type information:
rails generate model role user:references value:string:uniq
rake db:migrate
It is recommended to add an index on the foreign key and guise attribute. In
this case the columns are user_id and value.
Then add has_guises to your model. This will setup the has_many association
for you. It requires the name of the association and name of the column that
the subclass name will be stored in.
class User < ActiveRecord::Base
has_guises :DeskWorker, :MailForwarder, association: :roles, attribute: :value
end
This adds the following methods to the User class:
:desk_workersand:mail_forwardersmodel scopes.:has_guise?that checks if a user is a particular type.:desk_worker?,:mail_forwarderthat proxy to:has_guise?.:has_guises?that checks if a user has records for all the types supplied.:has_any_guises?that checks if a user has records for any of the types supplied.
To configure the other end of the association, add guise_for:
class UserRole < ActiveRecord::Base
guise_for :User
end
This method does the following:
- Sets up
belongs_toassociation and accepts the standard options. - Validates the column storing the name of the guise in the list supplied is unique to the resource it belongs to and is one of the provided names.
Role Subclasses
If using User.<guise_scope> is too tedious, it is possible to setup
subclasses to represent each value referenced in has_guises using the
guise_of method:
class DeskWorker < User
guise_of :User
end
This is equivalent to the following:
class DeskWorker < User
default_scope -> { joins(:roles).where(roles: { value: 'DeskWorker'}) }
after_initialize do
self.guises.build(value: 'DeskWorker')
end
end
To scope the association class to a guise, use scoped_guise_for. The name of
the class must be <guise_value><association_class_name> (i.e. the guise it
represents combined with the name of the parent class.
class DeskWorkerUserRole < UserRole
scoped_guise_for :User
end
This sets up the class as follows:
class DeskWorkerUserRole < UserRole
default_scope -> { where(value: "DeskWorker") }
after_initialize do
self.value = "DeskWorker"
end
end
Customization
If the association doesn't standard association assumptions made by
activerecord, you can pass in the options for has_many into has_guises.
The same applies to guise_for with the addition that you can specify not to
validate attributes.
class Person < ActiveRecord::Base
has_guises :Admin, :Engineer,
association: :positions,
attribute: :rank,
foreign_key: :employee_id,
class_name: :JobTitle
end
class JobTitle < ActiveRecord::Base
guise_for :Person,
foreign_key: :employee_id,
validate: false # skip setting up validations
end

