Module: RolePlays::Mixin::ClassMethods

Defined in:
lib/role_plays/mixin.rb

Overview

The DSL a policy class answers, and the lookups it resolves a declaration with

Instance Method Summary collapse

Instance Method Details

#action_handler(role, action) ⇒ Object



236
237
238
# File 'lib/role_plays/mixin.rb', line 236

def action_handler(role, action)
  policy_roles[role]&.action_for(action) || policy_roles[ANY_ROLE]&.action_for(action)
end

#action_predicatesObject

Module holding the generated predicates of this class, included on first use



268
269
270
# File 'lib/role_plays/mixin.rb', line 268

def action_predicates
  @action_predicates ||= Module.new.tap { |generated| include generated }
end

#context(*names) ⇒ Object

Declares the keywords the policy is built with, giving each one a reader that answers nil when the caller left it out, so a handler can treat it as optional:

context :user, :order, :order_relation

action(:create) { %i[title] + (order ? [:user_id] : []) }

Declaring is optional — any keyword new is given is readable by name anyway. What the declaration adds is the nil for a keyword that was not passed: an undeclared name always raises, so a typo in a handler cannot quietly answer nil.



282
283
284
285
286
287
288
289
290
# File 'lib/role_plays/mixin.rb', line 282

def context(*names)
  names.flatten.each do |name|
    name = name.to_sym
    policy_context_keys << name unless policy_context_keys.include?(name)
    next if method_defined?(name) || private_method_defined?(name)

    context_readers.define_method(name) { context[name] }
  end
end

#context_readersObject

Module holding the generated context readers of this class, included on first use



298
299
300
301
302
303
# File 'lib/role_plays/mixin.rb', line 298

def context_readers
  @context_readers ||= Module.new.tap do |generated|
    generated.extend(ContextReaders)
    include generated
  end
end

#declare_role(role) ⇒ Object

Files a Role struct under its name, merging it into an already declared role of the same name so a later declaration extends or overrides the earlier one



223
224
225
226
227
228
# File 'lib/role_plays/mixin.rb', line 223

def declare_role(role)
  declared = policy_roles[role.role]

  policy_roles[role.role] = declared ? declared.merge(role) : role
  define_action_predicates(role.actions.keys)
end

#define_action_predicates(*actions) ⇒ Object

Defines a can_<action>? predicate per declared action, so a policy answers the same can_create? / can_destroy? messages a hand written one does:

action :create   # => can_create?  == can?(:create)
action :destroy  # => can_destroy? == can?(:destroy)

Called for every role declaration, so the predicates cover the actions of all roles — each one still resolves the handler for the current role at call time. The methods live on a module included into the policy, so a predicate written by hand on the class always wins.



257
258
259
260
261
262
263
264
265
# File 'lib/role_plays/mixin.rb', line 257

def define_action_predicates(*actions)
  actions.flatten.each do |action|
    action = action.to_sym
    predicate = :"can_#{action}?"
    next if action_predicates.method_defined?(predicate)

    action_predicates.define_method(predicate) { can?(action) }
  end
end

#permitted_attributes_handler(role, label) ⇒ Object



240
241
242
# File 'lib/role_plays/mixin.rb', line 240

def permitted_attributes_handler(role, label)
  policy_roles[role]&.permitted_attributes_for(label) || policy_roles[ANY_ROLE]&.permitted_attributes_for(label)
end

#policy_context_keysObject

The names this policy declares with context



293
294
295
# File 'lib/role_plays/mixin.rb', line 293

def policy_context_keys
  @policy_context_keys ||= []
end

#policy_rolesObject

The roles this policy declares — nothing is inherited, so a role shared with another policy is composed in as a Role struct rather than picked up from a parent class



232
233
234
# File 'lib/role_plays/mixin.rb', line 232

def policy_roles
  @policy_roles ||= {}
end

#reserved_context_name?(name) ⇒ Boolean

Whether a keyword of this name would be unreachable because the policy answers it already. A reader generated by context does not count — answering it is what it is there for.

Returns:

  • (Boolean)


307
308
309
310
311
# File 'lib/role_plays/mixin.rb', line 307

def reserved_context_name?(name)
  return false unless method_defined?(name) || private_method_defined?(name)

  !instance_method(name).owner.is_a?(ContextReaders)
end

#role(*roles, &block) ⇒ Object

Declares one or more roles, either with a block of actions or with a prebuilt Role struct. Names given as a list or as an array share the block, which is evaluated once and filed under each of them, so roles with identical permissions are declared together:

role :user do ... end
role %i[user access_code] do ... end

Raises:

  • (ArgumentError)


208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/role_plays/mixin.rb', line 208

def role(*roles, &block)
  roles = roles.flatten
  raise ArgumentError, "role requires at least one role name" if roles.empty?

  shared = nil
  roles.each do |role_or_name|
    next declare_role(role_or_name) if role_or_name.is_a?(Role)

    shared ||= RoleBuilder.build(role_or_name, &block)
    declare_role(shared.new(role: role_or_name.to_sym))
  end
end

#scope_handler(role, label) ⇒ Object



244
245
246
# File 'lib/role_plays/mixin.rb', line 244

def scope_handler(role, label)
  policy_roles[role]&.scope_for(label) || policy_roles[ANY_ROLE]&.scope_for(label)
end