3288e1a874
This style change enforces `return if ...` instead of `return nil if ...` to save maintainers a few minor review points
312 lines
7.1 KiB
Ruby
312 lines
7.1 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module DeclarativePolicy
|
|
module Rule
|
|
# A Rule is the object that results from the `rule` declaration,
|
|
# usually built using the DSL in `RuleDsl`. It is a basic logical
|
|
# combination of building blocks, and is capable of deciding,
|
|
# given a context (instance of DeclarativePolicy::Base) whether it
|
|
# passes or not. Note that this decision doesn't by itself know
|
|
# how that affects the actual ability decision - for that, a
|
|
# `Step` is used.
|
|
class Base
|
|
def self.make(*args)
|
|
new(*args).simplify
|
|
end
|
|
|
|
# true or false whether this rule passes.
|
|
# `context` is a policy - an instance of
|
|
# DeclarativePolicy::Base.
|
|
def pass?(context)
|
|
raise 'abstract'
|
|
end
|
|
|
|
# same as #pass? except refuses to do any I/O,
|
|
# returning nil if the result is not yet cached.
|
|
# used for accurately scoring And/Or
|
|
def cached_pass?(context)
|
|
raise 'abstract'
|
|
end
|
|
|
|
# abstractly, how long would it take to compute
|
|
# this rule? lower-scored rules are tried first.
|
|
def score(context)
|
|
raise 'abstract'
|
|
end
|
|
|
|
# unwrap double negatives and nested and/or
|
|
def simplify
|
|
self
|
|
end
|
|
|
|
# convenience combination methods
|
|
def or(other)
|
|
Or.make([self, other])
|
|
end
|
|
|
|
def and(other)
|
|
And.make([self, other])
|
|
end
|
|
|
|
def negate
|
|
Not.make(self)
|
|
end
|
|
|
|
alias_method :|, :or
|
|
alias_method :&, :and
|
|
alias_method :~@, :negate
|
|
|
|
def inspect
|
|
"#<Rule #{repr}>"
|
|
end
|
|
end
|
|
|
|
# A rule that checks a condition. This is the
|
|
# type of rule that results from a basic bareword
|
|
# in the rule dsl (see RuleDsl#method_missing).
|
|
class Condition < Base
|
|
def initialize(name)
|
|
@name = name
|
|
end
|
|
|
|
# we delegate scoring to the condition. See
|
|
# ManifestCondition#score.
|
|
def score(context)
|
|
context.condition(@name).score
|
|
end
|
|
|
|
# Let the ManifestCondition from the context
|
|
# decide whether we pass.
|
|
def pass?(context)
|
|
context.condition(@name).pass?
|
|
end
|
|
|
|
# returns nil unless it's already cached
|
|
def cached_pass?(context)
|
|
condition = context.condition(@name)
|
|
return unless condition.cached?
|
|
|
|
condition.pass?
|
|
end
|
|
|
|
def description(context)
|
|
context.class.conditions[@name].description
|
|
end
|
|
|
|
def repr
|
|
@name.to_s
|
|
end
|
|
end
|
|
|
|
# A rule constructed from DelegateDsl - using a condition from a
|
|
# delegated policy.
|
|
class DelegatedCondition < Base
|
|
# Internal use only - this is rescued each time it's raised.
|
|
MissingDelegate = Class.new(StandardError)
|
|
|
|
def initialize(delegate_name, name)
|
|
@delegate_name = delegate_name
|
|
@name = name
|
|
end
|
|
|
|
def delegated_context(context)
|
|
policy = context.delegated_policies[@delegate_name]
|
|
raise MissingDelegate if policy.nil?
|
|
|
|
policy
|
|
end
|
|
|
|
def score(context)
|
|
delegated_context(context).condition(@name).score
|
|
rescue MissingDelegate
|
|
0
|
|
end
|
|
|
|
def cached_pass?(context)
|
|
condition = delegated_context(context).condition(@name)
|
|
return unless condition.cached?
|
|
|
|
condition.pass?
|
|
rescue MissingDelegate
|
|
false
|
|
end
|
|
|
|
def pass?(context)
|
|
delegated_context(context).condition(@name).pass?
|
|
rescue MissingDelegate
|
|
false
|
|
end
|
|
|
|
def repr
|
|
"#{@delegate_name}.#{@name}"
|
|
end
|
|
end
|
|
|
|
# A rule constructed from RuleDsl#can?. Computes a different ability
|
|
# on the same subject.
|
|
class Ability < Base
|
|
attr_reader :ability
|
|
def initialize(ability)
|
|
@ability = ability
|
|
end
|
|
|
|
# We ask the ability's runner for a score
|
|
def score(context)
|
|
context.runner(@ability).score
|
|
end
|
|
|
|
def pass?(context)
|
|
context.allowed?(@ability)
|
|
end
|
|
|
|
def cached_pass?(context)
|
|
runner = context.runner(@ability)
|
|
return unless runner.cached?
|
|
|
|
runner.pass?
|
|
end
|
|
|
|
def description(context)
|
|
"User can #{@ability.inspect}"
|
|
end
|
|
|
|
def repr
|
|
"can?(#{@ability.inspect})"
|
|
end
|
|
end
|
|
|
|
# Logical `and`, containing a list of rules. Only passes
|
|
# if all of them do.
|
|
class And < Base
|
|
attr_reader :rules
|
|
def initialize(rules)
|
|
@rules = rules
|
|
end
|
|
|
|
def simplify
|
|
simplified_rules = @rules.flat_map do |rule|
|
|
simplified = rule.simplify
|
|
case simplified
|
|
when And then simplified.rules
|
|
else [simplified]
|
|
end
|
|
end
|
|
|
|
And.new(simplified_rules)
|
|
end
|
|
|
|
def score(context)
|
|
return 0 unless cached_pass?(context).nil?
|
|
|
|
# note that cached rules will have score 0 anyways.
|
|
@rules.map { |r| r.score(context) }.inject(0, :+)
|
|
end
|
|
|
|
def pass?(context)
|
|
# try to find a cached answer before
|
|
# checking in order
|
|
cached = cached_pass?(context)
|
|
return cached unless cached.nil?
|
|
|
|
@rules.all? { |r| r.pass?(context) }
|
|
end
|
|
|
|
def cached_pass?(context)
|
|
@rules.each do |rule|
|
|
pass = rule.cached_pass?(context)
|
|
|
|
return pass if pass.nil? || pass == false
|
|
end
|
|
|
|
true
|
|
end
|
|
|
|
def repr
|
|
"all?(#{rules.map(&:repr).join(', ')})"
|
|
end
|
|
end
|
|
|
|
# Logical `or`. Mirrors And.
|
|
class Or < Base
|
|
attr_reader :rules
|
|
def initialize(rules)
|
|
@rules = rules
|
|
end
|
|
|
|
def pass?(context)
|
|
cached = cached_pass?(context)
|
|
return cached unless cached.nil?
|
|
|
|
@rules.any? { |r| r.pass?(context) }
|
|
end
|
|
|
|
def simplify
|
|
simplified_rules = @rules.flat_map do |rule|
|
|
simplified = rule.simplify
|
|
case simplified
|
|
when Or then simplified.rules
|
|
else [simplified]
|
|
end
|
|
end
|
|
|
|
Or.new(simplified_rules)
|
|
end
|
|
|
|
def cached_pass?(context)
|
|
@rules.each do |rule|
|
|
pass = rule.cached_pass?(context)
|
|
|
|
return pass if pass.nil? || pass == true
|
|
end
|
|
|
|
false
|
|
end
|
|
|
|
def score(context)
|
|
return 0 unless cached_pass?(context).nil?
|
|
|
|
@rules.map { |r| r.score(context) }.inject(0, :+)
|
|
end
|
|
|
|
def repr
|
|
"any?(#{@rules.map(&:repr).join(', ')})"
|
|
end
|
|
end
|
|
|
|
class Not < Base
|
|
attr_reader :rule
|
|
def initialize(rule)
|
|
@rule = rule
|
|
end
|
|
|
|
def simplify
|
|
case @rule
|
|
when And then Or.new(@rule.rules.map(&:negate)).simplify
|
|
when Or then And.new(@rule.rules.map(&:negate)).simplify
|
|
when Not then @rule.rule.simplify
|
|
else Not.new(@rule.simplify)
|
|
end
|
|
end
|
|
|
|
def pass?(context)
|
|
!@rule.pass?(context)
|
|
end
|
|
|
|
def cached_pass?(context)
|
|
case @rule.cached_pass?(context)
|
|
when nil then nil
|
|
when true then false
|
|
when false then true
|
|
end
|
|
end
|
|
|
|
def score(context)
|
|
@rule.score(context)
|
|
end
|
|
|
|
def repr
|
|
"~#{@rule.repr}"
|
|
end
|
|
end
|
|
end
|
|
end
|