declarative_policy 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.gitlab-ci.yml +48 -0
- data/.rspec +4 -0
- data/.rubocop.yml +10 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Dangerfile +16 -0
- data/Gemfile +24 -0
- data/Gemfile.lock +197 -0
- data/LICENSE.txt +21 -0
- data/README.md +144 -0
- data/Rakefile +8 -0
- data/danger/plugins/project_helper.rb +58 -0
- data/danger/roulette/Dangerfile +97 -0
- data/declarative_policy.gemspec +36 -0
- data/doc/caching.md +4 -0
- data/doc/configuration.md +78 -0
- data/doc/defining-policies.md +185 -0
- data/lib/declarative_policy.rb +128 -0
- data/lib/declarative_policy/base.rb +351 -0
- data/lib/declarative_policy/cache.rb +39 -0
- data/lib/declarative_policy/condition.rb +104 -0
- data/lib/declarative_policy/configuration.rb +37 -0
- data/lib/declarative_policy/delegate_dsl.rb +22 -0
- data/lib/declarative_policy/nil_policy.rb +8 -0
- data/lib/declarative_policy/policy_dsl.rb +46 -0
- data/lib/declarative_policy/preferred_scope.rb +31 -0
- data/lib/declarative_policy/rule.rb +316 -0
- data/lib/declarative_policy/rule_dsl.rb +51 -0
- data/lib/declarative_policy/runner.rb +203 -0
- data/lib/declarative_policy/step.rb +89 -0
- data/lib/declarative_policy/version.rb +5 -0
- metadata +84 -0
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeclarativePolicy
|
4
|
+
module Cache
|
5
|
+
class << self
|
6
|
+
def user_key(user)
|
7
|
+
return '<anonymous>' if user.nil?
|
8
|
+
|
9
|
+
id_for(user)
|
10
|
+
end
|
11
|
+
|
12
|
+
def policy_key(user, subject)
|
13
|
+
u = user_key(user)
|
14
|
+
s = subject_key(subject)
|
15
|
+
"/dp/policy/#{u}/#{s}"
|
16
|
+
end
|
17
|
+
|
18
|
+
def subject_key(subject)
|
19
|
+
return '<nil>' if subject.nil?
|
20
|
+
return subject.inspect if subject.is_a?(Symbol)
|
21
|
+
|
22
|
+
"#{subject.class.name}:#{id_for(subject)}"
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def id_for(obj)
|
28
|
+
id =
|
29
|
+
begin
|
30
|
+
obj.id
|
31
|
+
rescue NoMethodError
|
32
|
+
nil
|
33
|
+
end
|
34
|
+
|
35
|
+
id || "##{obj.object_id}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeclarativePolicy
|
4
|
+
# A Condition is the data structure that is created by the
|
5
|
+
# `condition` declaration on DeclarativePolicy::Base. It is
|
6
|
+
# more or less just a struct of the data passed to that
|
7
|
+
# declaration. It holds on to the block to be instance_eval'd
|
8
|
+
# on a context (instance of Base) later, via #compute.
|
9
|
+
class Condition
|
10
|
+
attr_reader :name, :description, :scope, :manual_score, :context_key
|
11
|
+
|
12
|
+
def initialize(name, opts = {}, &compute)
|
13
|
+
@name = name
|
14
|
+
@compute = compute
|
15
|
+
@scope = opts.fetch(:scope, :normal)
|
16
|
+
@description = opts.delete(:description)
|
17
|
+
@context_key = opts[:context_key]
|
18
|
+
@manual_score = opts.fetch(:score, nil)
|
19
|
+
end
|
20
|
+
|
21
|
+
def compute(context)
|
22
|
+
!!context.instance_eval(&@compute)
|
23
|
+
end
|
24
|
+
|
25
|
+
def key
|
26
|
+
"#{@context_key}/#{@name}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# In contrast to a Condition, a ManifestCondition contains
|
31
|
+
# a Condition and a context object, and is capable of calculating
|
32
|
+
# a result itself. This is the return value of Base#condition.
|
33
|
+
class ManifestCondition
|
34
|
+
def initialize(condition, context)
|
35
|
+
@condition = condition
|
36
|
+
@context = context
|
37
|
+
end
|
38
|
+
|
39
|
+
# The main entry point - does this condition pass? We reach into
|
40
|
+
# the context's cache here so that we can share in the global
|
41
|
+
# cache (often RequestStore or similar).
|
42
|
+
def pass?
|
43
|
+
@context.cache(cache_key) { @condition.compute(@context) }
|
44
|
+
end
|
45
|
+
|
46
|
+
# Whether we've already computed this condition.
|
47
|
+
def cached?
|
48
|
+
@context.cached?(cache_key)
|
49
|
+
end
|
50
|
+
|
51
|
+
# This is used to score Rule::Condition. See Rule::Condition#score
|
52
|
+
# and Runner#steps_by_score for how scores are used.
|
53
|
+
#
|
54
|
+
# The number here is intended to represent, abstractly, how
|
55
|
+
# expensive it would be to calculate this condition.
|
56
|
+
#
|
57
|
+
# See #cache_key for info about @condition.scope.
|
58
|
+
def score
|
59
|
+
# If we've been cached, no computation is necessary.
|
60
|
+
return 0 if cached?
|
61
|
+
|
62
|
+
# Use the override from condition(score: ...) if present
|
63
|
+
return @condition.manual_score if @condition.manual_score
|
64
|
+
|
65
|
+
# Global scope rules are cheap due to max cache sharing
|
66
|
+
return 2 if @condition.scope == :global
|
67
|
+
|
68
|
+
# "Normal" rules can't share caches with any other policies
|
69
|
+
return 16 if @condition.scope == :normal
|
70
|
+
|
71
|
+
# otherwise, we're :user or :subject scope, so it's 4 if
|
72
|
+
# the caller has declared a preference
|
73
|
+
return 4 if @condition.scope == DeclarativePolicy.preferred_scope
|
74
|
+
|
75
|
+
# and 8 for all other :user or :subject scope conditions.
|
76
|
+
8
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
# This method controls the caching for the condition. This is where
|
82
|
+
# the condition(scope: ...) option comes into play. Notice that
|
83
|
+
# depending on the scope, we may cache only by the user or only by
|
84
|
+
# the subject, resulting in sharing across different policy objects.
|
85
|
+
def cache_key
|
86
|
+
@cache_key ||=
|
87
|
+
case @condition.scope
|
88
|
+
when :normal then "/dp/condition/#{@condition.key}/#{user_key},#{subject_key}"
|
89
|
+
when :user then "/dp/condition/#{@condition.key}/#{user_key}"
|
90
|
+
when :subject then "/dp/condition/#{@condition.key}/#{subject_key}"
|
91
|
+
when :global then "/dp/condition/#{@condition.key}"
|
92
|
+
else raise 'invalid scope'
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def user_key
|
97
|
+
Cache.user_key(@context.user)
|
98
|
+
end
|
99
|
+
|
100
|
+
def subject_key
|
101
|
+
Cache.subject_key(@context.subject)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeclarativePolicy
|
4
|
+
class Configuration
|
5
|
+
ConfigurationError = Class.new(StandardError)
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@named_policies = {}
|
9
|
+
@name_transformation = ->(name) { "#{name}Policy" }
|
10
|
+
end
|
11
|
+
|
12
|
+
def named_policy(name, policy = nil)
|
13
|
+
@named_policies[name] = policy if policy
|
14
|
+
|
15
|
+
@named_policies[name] || raise(ConfigurationError, "No #{name} policy configured")
|
16
|
+
end
|
17
|
+
|
18
|
+
def nil_policy(policy = nil)
|
19
|
+
@nil_policy = policy if policy
|
20
|
+
|
21
|
+
@nil_policy || ::DeclarativePolicy::NilPolicy
|
22
|
+
end
|
23
|
+
|
24
|
+
def name_transformation(&block)
|
25
|
+
@name_transformation = block
|
26
|
+
nil
|
27
|
+
end
|
28
|
+
|
29
|
+
def policy_class(domain_class_name)
|
30
|
+
return unless domain_class_name
|
31
|
+
|
32
|
+
@name_transformation.call(domain_class_name).constantize
|
33
|
+
rescue NameError
|
34
|
+
nil
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeclarativePolicy
|
4
|
+
# Used when the name of a delegate is mentioned in
|
5
|
+
# the rule DSL.
|
6
|
+
class DelegateDsl
|
7
|
+
def initialize(rule_dsl, delegate_name)
|
8
|
+
@rule_dsl = rule_dsl
|
9
|
+
@delegate_name = delegate_name
|
10
|
+
end
|
11
|
+
|
12
|
+
def method_missing(msg, *args)
|
13
|
+
return super unless args.empty? && !block_given?
|
14
|
+
|
15
|
+
@rule_dsl.delegate(@delegate_name, msg)
|
16
|
+
end
|
17
|
+
|
18
|
+
def respond_to_missing?(msg, include_all)
|
19
|
+
true
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeclarativePolicy
|
4
|
+
# The return value of a rule { ... } declaration.
|
5
|
+
# Can call back to register rules with the containing
|
6
|
+
# Policy class (context_class here). See Base.rule
|
7
|
+
#
|
8
|
+
# Note that the #policy method just performs an #instance_eval,
|
9
|
+
# which is useful for multiple #enable or #prevent calls.
|
10
|
+
#
|
11
|
+
# Also provides a #method_missing proxy to the context
|
12
|
+
# class's class methods, so that helper methods can be
|
13
|
+
# defined and used in a #policy { ... } block.
|
14
|
+
class PolicyDsl
|
15
|
+
def initialize(context_class, rule)
|
16
|
+
@context_class = context_class
|
17
|
+
@rule = rule
|
18
|
+
end
|
19
|
+
|
20
|
+
def policy(&block)
|
21
|
+
instance_eval(&block)
|
22
|
+
end
|
23
|
+
|
24
|
+
def enable(*abilities)
|
25
|
+
@context_class.enable_when(abilities, @rule)
|
26
|
+
end
|
27
|
+
|
28
|
+
def prevent(*abilities)
|
29
|
+
@context_class.prevent_when(abilities, @rule)
|
30
|
+
end
|
31
|
+
|
32
|
+
def prevent_all
|
33
|
+
@context_class.prevent_all_when(@rule)
|
34
|
+
end
|
35
|
+
|
36
|
+
def method_missing(msg, *args, &block)
|
37
|
+
return super unless @context_class.respond_to?(msg)
|
38
|
+
|
39
|
+
@context_class.__send__(msg, *args, &block) # rubocop: disable GitlabSecurity/PublicSend
|
40
|
+
end
|
41
|
+
|
42
|
+
def respond_to_missing?(msg)
|
43
|
+
@context_class.respond_to?(msg) || super
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeclarativePolicy
|
4
|
+
module PreferredScope
|
5
|
+
PREFERRED_SCOPE_KEY = :"DeclarativePolicy.preferred_scope"
|
6
|
+
|
7
|
+
def with_preferred_scope(scope)
|
8
|
+
old_scope = Thread.current[PREFERRED_SCOPE_KEY]
|
9
|
+
Thread.current[PREFERRED_SCOPE_KEY] = scope
|
10
|
+
yield
|
11
|
+
ensure
|
12
|
+
Thread.current[PREFERRED_SCOPE_KEY] = old_scope
|
13
|
+
end
|
14
|
+
|
15
|
+
def preferred_scope
|
16
|
+
Thread.current[PREFERRED_SCOPE_KEY]
|
17
|
+
end
|
18
|
+
|
19
|
+
def user_scope(&block)
|
20
|
+
with_preferred_scope(:user, &block)
|
21
|
+
end
|
22
|
+
|
23
|
+
def subject_scope(&block)
|
24
|
+
with_preferred_scope(:subject, &block)
|
25
|
+
end
|
26
|
+
|
27
|
+
def preferred_scope=(scope)
|
28
|
+
Thread.current[PREFERRED_SCOPE_KEY] = scope
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,316 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeclarativePolicy
|
4
|
+
module Rule
|
5
|
+
# A Rule is the object that results from the `rule` declaration,
|
6
|
+
# usually built using the DSL in `RuleDsl`. It is a basic logical
|
7
|
+
# combination of building blocks, and is capable of deciding,
|
8
|
+
# given a context (instance of DeclarativePolicy::Base) whether it
|
9
|
+
# passes or not. Note that this decision doesn't by itself know
|
10
|
+
# how that affects the actual ability decision - for that, a
|
11
|
+
# `Step` is used.
|
12
|
+
class Base
|
13
|
+
def self.make(*args)
|
14
|
+
new(*args).simplify
|
15
|
+
end
|
16
|
+
|
17
|
+
# true or false whether this rule passes.
|
18
|
+
# `context` is a policy - an instance of
|
19
|
+
# DeclarativePolicy::Base.
|
20
|
+
def pass?(_context)
|
21
|
+
raise 'abstract'
|
22
|
+
end
|
23
|
+
|
24
|
+
# same as #pass? except refuses to do any I/O,
|
25
|
+
# returning nil if the result is not yet cached.
|
26
|
+
# used for accurately scoring And/Or
|
27
|
+
def cached_pass?(_context)
|
28
|
+
raise 'abstract'
|
29
|
+
end
|
30
|
+
|
31
|
+
# abstractly, how long would it take to compute
|
32
|
+
# this rule? lower-scored rules are tried first.
|
33
|
+
def score(_context)
|
34
|
+
raise 'abstract'
|
35
|
+
end
|
36
|
+
|
37
|
+
# unwrap double negatives and nested and/or
|
38
|
+
def simplify
|
39
|
+
self
|
40
|
+
end
|
41
|
+
|
42
|
+
# convenience combination methods
|
43
|
+
def or(other)
|
44
|
+
Or.make([self, other])
|
45
|
+
end
|
46
|
+
|
47
|
+
def and(other)
|
48
|
+
And.make([self, other])
|
49
|
+
end
|
50
|
+
|
51
|
+
def negate
|
52
|
+
Not.make(self)
|
53
|
+
end
|
54
|
+
|
55
|
+
alias_method :|, :or
|
56
|
+
alias_method :&, :and
|
57
|
+
alias_method :~, :negate
|
58
|
+
|
59
|
+
def inspect
|
60
|
+
"#<Rule #{repr}>"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# A rule that checks a condition. This is the
|
65
|
+
# type of rule that results from a basic bareword
|
66
|
+
# in the rule dsl (see RuleDsl#method_missing).
|
67
|
+
class Condition < Base
|
68
|
+
def initialize(name)
|
69
|
+
@name = name
|
70
|
+
end
|
71
|
+
|
72
|
+
# we delegate scoring to the condition. See
|
73
|
+
# ManifestCondition#score.
|
74
|
+
def score(context)
|
75
|
+
context.condition(@name).score
|
76
|
+
end
|
77
|
+
|
78
|
+
# Let the ManifestCondition from the context
|
79
|
+
# decide whether we pass.
|
80
|
+
def pass?(context)
|
81
|
+
context.condition(@name).pass?
|
82
|
+
end
|
83
|
+
|
84
|
+
# returns nil unless it's already cached
|
85
|
+
def cached_pass?(context)
|
86
|
+
condition = context.condition(@name)
|
87
|
+
return unless condition.cached?
|
88
|
+
|
89
|
+
condition.pass?
|
90
|
+
end
|
91
|
+
|
92
|
+
def description(context)
|
93
|
+
context.class.conditions[@name].description
|
94
|
+
end
|
95
|
+
|
96
|
+
def repr
|
97
|
+
@name.to_s
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# A rule constructed from DelegateDsl - using a condition from a
|
102
|
+
# delegated policy.
|
103
|
+
class DelegatedCondition < Base
|
104
|
+
# Internal use only - this is rescued each time it's raised.
|
105
|
+
MissingDelegate = Class.new(StandardError)
|
106
|
+
|
107
|
+
def initialize(delegate_name, name)
|
108
|
+
@delegate_name = delegate_name
|
109
|
+
@name = name
|
110
|
+
end
|
111
|
+
|
112
|
+
def delegated_context(context)
|
113
|
+
policy = context.delegated_policies[@delegate_name]
|
114
|
+
raise MissingDelegate if policy.nil?
|
115
|
+
|
116
|
+
policy
|
117
|
+
end
|
118
|
+
|
119
|
+
def score(context)
|
120
|
+
delegated_context(context).condition(@name).score
|
121
|
+
rescue MissingDelegate
|
122
|
+
0
|
123
|
+
end
|
124
|
+
|
125
|
+
def cached_pass?(context)
|
126
|
+
condition = delegated_context(context).condition(@name)
|
127
|
+
return unless condition.cached?
|
128
|
+
|
129
|
+
condition.pass?
|
130
|
+
rescue MissingDelegate
|
131
|
+
false
|
132
|
+
end
|
133
|
+
|
134
|
+
def pass?(context)
|
135
|
+
delegated_context(context).condition(@name).pass?
|
136
|
+
rescue MissingDelegate
|
137
|
+
false
|
138
|
+
end
|
139
|
+
|
140
|
+
def repr
|
141
|
+
"#{@delegate_name}.#{@name}"
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# A rule constructed from RuleDsl#can?. Computes a different ability
|
146
|
+
# on the same subject.
|
147
|
+
class Ability < Base
|
148
|
+
attr_reader :ability
|
149
|
+
|
150
|
+
def initialize(ability)
|
151
|
+
@ability = ability
|
152
|
+
end
|
153
|
+
|
154
|
+
# We ask the ability's runner for a score
|
155
|
+
def score(context)
|
156
|
+
context.runner(@ability).score
|
157
|
+
end
|
158
|
+
|
159
|
+
def pass?(context)
|
160
|
+
context.allowed?(@ability)
|
161
|
+
end
|
162
|
+
|
163
|
+
def cached_pass?(context)
|
164
|
+
runner = context.runner(@ability)
|
165
|
+
return unless runner.cached?
|
166
|
+
|
167
|
+
runner.pass?
|
168
|
+
end
|
169
|
+
|
170
|
+
def description(_context)
|
171
|
+
"User can #{@ability.inspect}"
|
172
|
+
end
|
173
|
+
|
174
|
+
def repr
|
175
|
+
"can?(#{@ability.inspect})"
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
# Logical `and`, containing a list of rules. Only passes
|
180
|
+
# if all of them do.
|
181
|
+
class And < Base
|
182
|
+
attr_reader :rules
|
183
|
+
|
184
|
+
def initialize(rules)
|
185
|
+
@rules = rules
|
186
|
+
end
|
187
|
+
|
188
|
+
def simplify
|
189
|
+
simplified_rules = @rules.flat_map do |rule|
|
190
|
+
simplified = rule.simplify
|
191
|
+
case simplified
|
192
|
+
when And then simplified.rules
|
193
|
+
else [simplified]
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
And.new(simplified_rules)
|
198
|
+
end
|
199
|
+
|
200
|
+
def score(context)
|
201
|
+
return 0 unless cached_pass?(context).nil?
|
202
|
+
|
203
|
+
# note that cached rules will have score 0 anyways.
|
204
|
+
@rules.sum { |r| r.score(context) }
|
205
|
+
end
|
206
|
+
|
207
|
+
def pass?(context)
|
208
|
+
# try to find a cached answer before
|
209
|
+
# checking in order
|
210
|
+
cached = cached_pass?(context)
|
211
|
+
return cached unless cached.nil?
|
212
|
+
|
213
|
+
@rules.all? { |r| r.pass?(context) }
|
214
|
+
end
|
215
|
+
|
216
|
+
def cached_pass?(context)
|
217
|
+
@rules.each do |rule|
|
218
|
+
pass = rule.cached_pass?(context)
|
219
|
+
|
220
|
+
return pass if pass.nil? || pass == false
|
221
|
+
end
|
222
|
+
|
223
|
+
true
|
224
|
+
end
|
225
|
+
|
226
|
+
def repr
|
227
|
+
"all?(#{rules.map(&:repr).join(', ')})"
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
# Logical `or`. Mirrors And.
|
232
|
+
class Or < Base
|
233
|
+
attr_reader :rules
|
234
|
+
|
235
|
+
def initialize(rules)
|
236
|
+
@rules = rules
|
237
|
+
end
|
238
|
+
|
239
|
+
def pass?(context)
|
240
|
+
cached = cached_pass?(context)
|
241
|
+
return cached unless cached.nil?
|
242
|
+
|
243
|
+
@rules.any? { |r| r.pass?(context) }
|
244
|
+
end
|
245
|
+
|
246
|
+
def simplify
|
247
|
+
simplified_rules = @rules.flat_map do |rule|
|
248
|
+
simplified = rule.simplify
|
249
|
+
case simplified
|
250
|
+
when Or then simplified.rules
|
251
|
+
else [simplified]
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
Or.new(simplified_rules)
|
256
|
+
end
|
257
|
+
|
258
|
+
def cached_pass?(context)
|
259
|
+
@rules.each do |rule|
|
260
|
+
pass = rule.cached_pass?(context)
|
261
|
+
|
262
|
+
return pass if pass.nil? || pass == true
|
263
|
+
end
|
264
|
+
|
265
|
+
false
|
266
|
+
end
|
267
|
+
|
268
|
+
def score(context)
|
269
|
+
return 0 unless cached_pass?(context).nil?
|
270
|
+
|
271
|
+
@rules.sum { |r| r.score(context) }
|
272
|
+
end
|
273
|
+
|
274
|
+
def repr
|
275
|
+
"any?(#{@rules.map(&:repr).join(', ')})"
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
class Not < Base
|
280
|
+
attr_reader :rule
|
281
|
+
|
282
|
+
def initialize(rule)
|
283
|
+
@rule = rule
|
284
|
+
end
|
285
|
+
|
286
|
+
def simplify
|
287
|
+
case @rule
|
288
|
+
when And then Or.new(@rule.rules.map(&:negate)).simplify
|
289
|
+
when Or then And.new(@rule.rules.map(&:negate)).simplify
|
290
|
+
when Not then @rule.rule.simplify
|
291
|
+
else Not.new(@rule.simplify)
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
def pass?(context)
|
296
|
+
!@rule.pass?(context)
|
297
|
+
end
|
298
|
+
|
299
|
+
def cached_pass?(context)
|
300
|
+
case @rule.cached_pass?(context)
|
301
|
+
when nil then nil
|
302
|
+
when true then false
|
303
|
+
when false then true
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
def score(context)
|
308
|
+
@rule.score(context)
|
309
|
+
end
|
310
|
+
|
311
|
+
def repr
|
312
|
+
"~#{@rule.repr}"
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|