ruby-rego 0.1.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/.reek.yml +80 -0
- data/.vscode/extensions.json +19 -0
- data/.vscode/launch.json +35 -0
- data/.vscode/settings.json +25 -0
- data/.vscode/tasks.json +117 -0
- data/.yardopts +12 -0
- data/ARCHITECTURE.md +39 -0
- data/CHANGELOG.md +25 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +183 -0
- data/RELEASING.md +37 -0
- data/Rakefile +38 -0
- data/SECURITY.md +26 -0
- data/Steepfile +10 -0
- data/TODO.md +35 -0
- data/benchmark/builtin_calls.rb +29 -0
- data/benchmark/complex_policy.rb +19 -0
- data/benchmark/comprehensions.rb +19 -0
- data/benchmark/simple_rules.rb +20 -0
- data/examples/README.md +27 -0
- data/examples/sample_config.yaml +2 -0
- data/examples/simple_policy.rego +7 -0
- data/examples/validation_policy.rego +11 -0
- data/exe/rego-validate +6 -0
- data/lib/ruby/rego/ast/base.rb +95 -0
- data/lib/ruby/rego/ast/binary_op.rb +64 -0
- data/lib/ruby/rego/ast/call.rb +27 -0
- data/lib/ruby/rego/ast/composite.rb +48 -0
- data/lib/ruby/rego/ast/comprehension.rb +63 -0
- data/lib/ruby/rego/ast/every.rb +37 -0
- data/lib/ruby/rego/ast/import.rb +32 -0
- data/lib/ruby/rego/ast/literal.rb +70 -0
- data/lib/ruby/rego/ast/module.rb +32 -0
- data/lib/ruby/rego/ast/package.rb +22 -0
- data/lib/ruby/rego/ast/query.rb +63 -0
- data/lib/ruby/rego/ast/reference.rb +58 -0
- data/lib/ruby/rego/ast/rule.rb +114 -0
- data/lib/ruby/rego/ast/unary_op.rb +42 -0
- data/lib/ruby/rego/ast/variable.rb +22 -0
- data/lib/ruby/rego/ast.rb +17 -0
- data/lib/ruby/rego/builtins/aggregates.rb +124 -0
- data/lib/ruby/rego/builtins/base.rb +95 -0
- data/lib/ruby/rego/builtins/collections/array_ops.rb +103 -0
- data/lib/ruby/rego/builtins/collections/object_ops.rb +120 -0
- data/lib/ruby/rego/builtins/collections/set_ops.rb +51 -0
- data/lib/ruby/rego/builtins/collections.rb +137 -0
- data/lib/ruby/rego/builtins/comparisons/casts.rb +139 -0
- data/lib/ruby/rego/builtins/comparisons.rb +84 -0
- data/lib/ruby/rego/builtins/numeric_helpers.rb +56 -0
- data/lib/ruby/rego/builtins/registry.rb +199 -0
- data/lib/ruby/rego/builtins/registry_helpers.rb +27 -0
- data/lib/ruby/rego/builtins/strings/case_ops.rb +22 -0
- data/lib/ruby/rego/builtins/strings/concat.rb +19 -0
- data/lib/ruby/rego/builtins/strings/formatting.rb +35 -0
- data/lib/ruby/rego/builtins/strings/helpers.rb +62 -0
- data/lib/ruby/rego/builtins/strings/number_helpers.rb +48 -0
- data/lib/ruby/rego/builtins/strings/search.rb +63 -0
- data/lib/ruby/rego/builtins/strings/split.rb +19 -0
- data/lib/ruby/rego/builtins/strings/substring.rb +22 -0
- data/lib/ruby/rego/builtins/strings/trim.rb +42 -0
- data/lib/ruby/rego/builtins/strings/trim_helpers.rb +62 -0
- data/lib/ruby/rego/builtins/strings.rb +58 -0
- data/lib/ruby/rego/builtins/types.rb +89 -0
- data/lib/ruby/rego/call_name.rb +55 -0
- data/lib/ruby/rego/cli.rb +1122 -0
- data/lib/ruby/rego/compiled_module.rb +114 -0
- data/lib/ruby/rego/compiler.rb +1097 -0
- data/lib/ruby/rego/environment/overrides.rb +33 -0
- data/lib/ruby/rego/environment/reference_resolution.rb +86 -0
- data/lib/ruby/rego/environment.rb +230 -0
- data/lib/ruby/rego/environment_pool.rb +71 -0
- data/lib/ruby/rego/error_handling.rb +58 -0
- data/lib/ruby/rego/error_payload.rb +34 -0
- data/lib/ruby/rego/errors.rb +196 -0
- data/lib/ruby/rego/evaluator/assignment_support.rb +126 -0
- data/lib/ruby/rego/evaluator/binding_helpers.rb +60 -0
- data/lib/ruby/rego/evaluator/comprehension_evaluator.rb +182 -0
- data/lib/ruby/rego/evaluator/expression_dispatch.rb +45 -0
- data/lib/ruby/rego/evaluator/expression_evaluator.rb +492 -0
- data/lib/ruby/rego/evaluator/object_literal_evaluator.rb +52 -0
- data/lib/ruby/rego/evaluator/operator_evaluator.rb +163 -0
- data/lib/ruby/rego/evaluator/query_node_builder.rb +38 -0
- data/lib/ruby/rego/evaluator/reference_key_resolver.rb +50 -0
- data/lib/ruby/rego/evaluator/reference_resolver.rb +352 -0
- data/lib/ruby/rego/evaluator/rule_evaluator/bindings.rb +70 -0
- data/lib/ruby/rego/evaluator/rule_evaluator.rb +550 -0
- data/lib/ruby/rego/evaluator/rule_value_provider.rb +56 -0
- data/lib/ruby/rego/evaluator/variable_collector.rb +221 -0
- data/lib/ruby/rego/evaluator.rb +174 -0
- data/lib/ruby/rego/lexer/number_reader.rb +68 -0
- data/lib/ruby/rego/lexer/stream.rb +137 -0
- data/lib/ruby/rego/lexer/string_reader.rb +90 -0
- data/lib/ruby/rego/lexer/template_string_reader.rb +62 -0
- data/lib/ruby/rego/lexer.rb +206 -0
- data/lib/ruby/rego/location.rb +73 -0
- data/lib/ruby/rego/memoization.rb +67 -0
- data/lib/ruby/rego/parser/collections.rb +173 -0
- data/lib/ruby/rego/parser/expressions.rb +216 -0
- data/lib/ruby/rego/parser/precedence.rb +42 -0
- data/lib/ruby/rego/parser/query.rb +139 -0
- data/lib/ruby/rego/parser/references.rb +115 -0
- data/lib/ruby/rego/parser/rules.rb +310 -0
- data/lib/ruby/rego/parser.rb +210 -0
- data/lib/ruby/rego/policy.rb +50 -0
- data/lib/ruby/rego/result.rb +91 -0
- data/lib/ruby/rego/token.rb +206 -0
- data/lib/ruby/rego/unifier.rb +451 -0
- data/lib/ruby/rego/value.rb +379 -0
- data/lib/ruby/rego/version.rb +7 -0
- data/lib/ruby/rego/with_modifiers/with_modifier.rb +37 -0
- data/lib/ruby/rego/with_modifiers/with_modifier_applier.rb +48 -0
- data/lib/ruby/rego/with_modifiers/with_modifier_builtin_override.rb +128 -0
- data/lib/ruby/rego/with_modifiers/with_modifier_context.rb +120 -0
- data/lib/ruby/rego/with_modifiers/with_modifier_path_key_resolver.rb +42 -0
- data/lib/ruby/rego/with_modifiers/with_modifier_path_override.rb +99 -0
- data/lib/ruby/rego/with_modifiers/with_modifier_root_scope.rb +58 -0
- data/lib/ruby/rego.rb +72 -0
- data/sig/objspace.rbs +4 -0
- data/sig/psych.rbs +7 -0
- data/sig/rego_validate.rbs +382 -0
- data/sig/ruby/rego.rbs +2150 -0
- metadata +172 -0
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "../call_name"
|
|
5
|
+
|
|
6
|
+
module Ruby
|
|
7
|
+
module Rego
|
|
8
|
+
class Evaluator
|
|
9
|
+
# Evaluates expressions to Rego values.
|
|
10
|
+
# :reek:TooManyInstanceVariables
|
|
11
|
+
# :reek:DataClump
|
|
12
|
+
# :reek:TooManyMethods
|
|
13
|
+
# rubocop:disable Metrics/ClassLength
|
|
14
|
+
class ExpressionEvaluator
|
|
15
|
+
PRIMITIVE_TYPES = [String, Numeric, TrueClass, FalseClass, Array, Hash, Set, NilClass].freeze
|
|
16
|
+
NODE_EVALUATORS = [
|
|
17
|
+
[AST::Literal, ->(literal, _evaluator) { Value.from_ruby(literal.value) }],
|
|
18
|
+
[AST::Variable, ->(variable, evaluator) { evaluator.send(:evaluate_variable, variable) }],
|
|
19
|
+
[AST::Reference, ->(reference, evaluator) { evaluator.send(:evaluate_reference, reference) }],
|
|
20
|
+
[AST::BinaryOp, ->(binary_op, evaluator) { evaluator.send(:evaluate_binary_op, binary_op) }],
|
|
21
|
+
[AST::UnaryOp, ->(unary_op, evaluator) { evaluator.send(:evaluate_unary_op, unary_op) }],
|
|
22
|
+
[AST::ArrayLiteral, ->(node, evaluator) { evaluator.send(:evaluate_array_literal, node) }],
|
|
23
|
+
[AST::ObjectLiteral, ->(node, evaluator) { evaluator.send(:evaluate_object_literal, node) }],
|
|
24
|
+
[AST::SetLiteral, ->(node, evaluator) { evaluator.send(:evaluate_set_literal, node) }],
|
|
25
|
+
[AST::ArrayComprehension, ->(node, evaluator) { evaluator.send(:eval_array_comprehension, node) }],
|
|
26
|
+
[AST::ObjectComprehension, ->(node, evaluator) { evaluator.send(:eval_object_comprehension, node) }],
|
|
27
|
+
[AST::SetComprehension, ->(node, evaluator) { evaluator.send(:eval_set_comprehension, node) }],
|
|
28
|
+
[AST::Every, ->(node, evaluator) { evaluator.send(:evaluate_every, node) }],
|
|
29
|
+
[AST::Call, ->(call, evaluator) { evaluator.send(:evaluate_call, call) }],
|
|
30
|
+
[AST::TemplateString, ->(node, evaluator) { evaluator.send(:evaluate_template_string, node) }]
|
|
31
|
+
].freeze
|
|
32
|
+
TRUE_VALUE = BooleanValue.new(true)
|
|
33
|
+
FALSE_VALUE = BooleanValue.new(false)
|
|
34
|
+
|
|
35
|
+
include AssignmentSupport
|
|
36
|
+
include BindingHelpers
|
|
37
|
+
|
|
38
|
+
# @param environment [Environment]
|
|
39
|
+
# @param reference_resolver [ReferenceResolver]
|
|
40
|
+
# :reek:TooManyStatements
|
|
41
|
+
def initialize(environment:, reference_resolver:)
|
|
42
|
+
@environment = environment
|
|
43
|
+
@reference_resolver = reference_resolver
|
|
44
|
+
@dispatch = build_dispatch
|
|
45
|
+
@unifier = Unifier.new(variable_resolver: method(:resolve_reference_variable_key))
|
|
46
|
+
@object_literal_evaluator = ObjectLiteralEvaluator.new(expression_evaluator: self)
|
|
47
|
+
@comprehension_evaluator = ComprehensionEvaluator.new(
|
|
48
|
+
expression_evaluator: self,
|
|
49
|
+
environment: environment
|
|
50
|
+
)
|
|
51
|
+
@query_evaluator = nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @param query_evaluator [RuleEvaluator]
|
|
55
|
+
# @return [void]
|
|
56
|
+
def attach_query_evaluator(query_evaluator)
|
|
57
|
+
@query_evaluator = query_evaluator
|
|
58
|
+
comprehension_evaluator.attach_query_evaluator(query_evaluator)
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @param node [Object]
|
|
63
|
+
# @return [Value]
|
|
64
|
+
def evaluate(node)
|
|
65
|
+
return node if node.is_a?(Value)
|
|
66
|
+
|
|
67
|
+
dispatch.primitive_value(node) || dispatch.dispatch_node(node, self) || raise_unknown_node(node)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# @param node [Object]
|
|
71
|
+
# @param env [Environment]
|
|
72
|
+
# @return [Enumerator]
|
|
73
|
+
def eval_with_unification(node, env = environment)
|
|
74
|
+
Enumerator.new do |yielder|
|
|
75
|
+
case node
|
|
76
|
+
when AST::BinaryOp
|
|
77
|
+
handle_unification_operator(node, env, yielder)
|
|
78
|
+
when AST::Reference
|
|
79
|
+
yield_reference_bindings(node, env, yielder)
|
|
80
|
+
else
|
|
81
|
+
yield_truthy_bindings(node, yielder)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# :reek:UtilityFunction
|
|
87
|
+
def self.call_name(node)
|
|
88
|
+
CallName.call_name(node)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def self.reference_call_name(reference)
|
|
92
|
+
CallName.reference_call_name(reference)
|
|
93
|
+
end
|
|
94
|
+
private_class_method :reference_call_name
|
|
95
|
+
|
|
96
|
+
def self.reference_base_name(reference)
|
|
97
|
+
CallName.reference_base_name(reference)
|
|
98
|
+
end
|
|
99
|
+
private_class_method :reference_base_name
|
|
100
|
+
|
|
101
|
+
def self.reference_call_segments(path)
|
|
102
|
+
CallName.reference_call_segments(path)
|
|
103
|
+
end
|
|
104
|
+
private_class_method :reference_call_segments
|
|
105
|
+
|
|
106
|
+
def self.dot_ref_segment_value(segment)
|
|
107
|
+
CallName.dot_ref_segment_value(segment)
|
|
108
|
+
end
|
|
109
|
+
private_class_method :dot_ref_segment_value
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
attr_reader :environment, :reference_resolver, :object_literal_evaluator,
|
|
114
|
+
:dispatch, :unifier, :comprehension_evaluator
|
|
115
|
+
|
|
116
|
+
# :reek:UtilityFunction
|
|
117
|
+
def build_dispatch
|
|
118
|
+
ExpressionDispatch.new(
|
|
119
|
+
primitive_types: PRIMITIVE_TYPES,
|
|
120
|
+
node_evaluators: NODE_EVALUATORS
|
|
121
|
+
)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def evaluate_variable(node)
|
|
125
|
+
name = node.name
|
|
126
|
+
return UndefinedValue.new if name == "_"
|
|
127
|
+
|
|
128
|
+
resolve_variable_name(name)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def resolve_variable_name(name)
|
|
132
|
+
resolve_reference_variable_key(name)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def resolve_reference_variable_key(name)
|
|
136
|
+
resolved = environment.lookup(name)
|
|
137
|
+
return resolved unless resolved.is_a?(UndefinedValue)
|
|
138
|
+
return resolved if environment.local_bound?(name)
|
|
139
|
+
|
|
140
|
+
resolve_import_or_rule(name, resolved)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def resolve_import_or_rule(name, fallback)
|
|
144
|
+
reference_resolver.resolve_import_variable(name) ||
|
|
145
|
+
reference_resolver.resolve_rule_variable(name) ||
|
|
146
|
+
fallback
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def evaluate_reference(node)
|
|
150
|
+
reference_resolver.resolve(node)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def evaluate_array_literal(node)
|
|
154
|
+
elements = node.elements.map { |element| evaluate(element) }
|
|
155
|
+
ArrayValue.new(elements)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def evaluate_object_literal(node)
|
|
159
|
+
object_literal_evaluator.evaluate(node)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def evaluate_set_literal(node)
|
|
163
|
+
elements = node.elements.map { |element| evaluate(element) }
|
|
164
|
+
SetValue.new(elements)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# :reek:TooManyStatements
|
|
168
|
+
def evaluate_call(node)
|
|
169
|
+
name_node = node.name
|
|
170
|
+
name = self.class.call_name(name_node)
|
|
171
|
+
return UndefinedValue.new unless name
|
|
172
|
+
|
|
173
|
+
args = node.args.map { |arg| evaluate(arg) }
|
|
174
|
+
return UndefinedValue.new if args.any?(&:undefined?)
|
|
175
|
+
|
|
176
|
+
call_named_function(name, name_node, args)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def evaluate_user_function(name, args)
|
|
180
|
+
return UndefinedValue.new unless query_evaluator
|
|
181
|
+
|
|
182
|
+
query_evaluator.evaluate_function_call(name, args)
|
|
183
|
+
end
|
|
184
|
+
public :evaluate_user_function
|
|
185
|
+
|
|
186
|
+
def evaluate_template_string(node)
|
|
187
|
+
rendered = node.parts.map do |part|
|
|
188
|
+
next part.value if part.is_a?(AST::StringLiteral)
|
|
189
|
+
|
|
190
|
+
format_template_value(evaluate(part))
|
|
191
|
+
end.join
|
|
192
|
+
StringValue.new(rendered)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def call_named_function(name, name_node, args)
|
|
196
|
+
registry = environment.builtin_registry
|
|
197
|
+
return registry.call(name, args) if registry.registered?(name)
|
|
198
|
+
|
|
199
|
+
function_name = function_name_for_call(name_node, name)
|
|
200
|
+
evaluate_user_function(function_name, args)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def function_name_for_call(name_node, fallback_name)
|
|
204
|
+
return fallback_name unless name_node.is_a?(AST::Reference)
|
|
205
|
+
|
|
206
|
+
reference_resolver.function_reference_name(name_node) || fallback_name
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def eval_array_comprehension(node)
|
|
210
|
+
comprehension_evaluator.eval_array(node)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def eval_object_comprehension(node)
|
|
214
|
+
comprehension_evaluator.eval_object(node)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def eval_set_comprehension(node)
|
|
218
|
+
comprehension_evaluator.eval_set(node)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# :reek:TooManyStatements
|
|
222
|
+
def evaluate_every(node)
|
|
223
|
+
collection_value = environment.with_bindings({}) { evaluate(node.domain) }
|
|
224
|
+
return BooleanValue.new(false) if collection_value.is_a?(UndefinedValue)
|
|
225
|
+
|
|
226
|
+
variables = [node.key_var, node.value_var].compact
|
|
227
|
+
bindings_enum = every_bindings(variables, collection_value)
|
|
228
|
+
return BooleanValue.new(false) unless bindings_enum
|
|
229
|
+
|
|
230
|
+
evaluate_every_bindings(node, bindings_enum)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def evaluate_every_bindings(node, bindings_enum)
|
|
234
|
+
with_every_scope(node) do
|
|
235
|
+
bindings_enum.each do |bindings|
|
|
236
|
+
return BooleanValue.new(false) unless every_body_succeeds?(node.body, bindings)
|
|
237
|
+
end
|
|
238
|
+
BooleanValue.new(true)
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# :reek:TooManyStatements
|
|
243
|
+
def evaluate_binary_op(node)
|
|
244
|
+
operator = node.operator
|
|
245
|
+
return evaluate_assignment(node) if operator == :assign
|
|
246
|
+
return evaluate_unification(node) if operator == :unify
|
|
247
|
+
return evaluate_logical_operator(node) if %i[and or].include?(operator)
|
|
248
|
+
|
|
249
|
+
left = evaluate(node.left)
|
|
250
|
+
right = evaluate(node.right)
|
|
251
|
+
OperatorEvaluator.apply(operator, left, right)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def raise_unknown_node(node)
|
|
255
|
+
node_class = node.class
|
|
256
|
+
raise EvaluationError.new("Unsupported AST node: #{node_class}", rule: nil, location: nil)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def evaluate_unary_op(node)
|
|
260
|
+
case node
|
|
261
|
+
in AST::UnaryOp[operator:, operand:]
|
|
262
|
+
OperatorEvaluator.apply_unary(operator, evaluate(operand))
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def handle_unification_operator(node, env, yielder)
|
|
267
|
+
case node.operator
|
|
268
|
+
when :assign
|
|
269
|
+
yield_assignment_bindings(node, env, yielder)
|
|
270
|
+
when :unify
|
|
271
|
+
yield_unification_bindings(node, env, yielder)
|
|
272
|
+
else
|
|
273
|
+
yield_truthy_bindings(node, yielder)
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def evaluate_logical_operator(node)
|
|
278
|
+
case node.operator
|
|
279
|
+
when :and then evaluate_and_operator(node)
|
|
280
|
+
when :or then evaluate_or_operator(node)
|
|
281
|
+
else UndefinedValue.new
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# :reek:TooManyStatements
|
|
286
|
+
def evaluate_and_operator(node)
|
|
287
|
+
left_state = logical_state(evaluate(node.left))
|
|
288
|
+
return FALSE_VALUE if left_state == :falsy
|
|
289
|
+
|
|
290
|
+
right_state = logical_state(evaluate(node.right))
|
|
291
|
+
right_truthy = right_state == :truthy
|
|
292
|
+
right_falsy = right_state == :falsy
|
|
293
|
+
return TRUE_VALUE if left_state == :truthy && right_truthy
|
|
294
|
+
return FALSE_VALUE if right_falsy
|
|
295
|
+
|
|
296
|
+
UndefinedValue.new
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# :reek:TooManyStatements
|
|
300
|
+
def evaluate_or_operator(node)
|
|
301
|
+
left_state = logical_state(evaluate(node.left))
|
|
302
|
+
return TRUE_VALUE if left_state == :truthy
|
|
303
|
+
|
|
304
|
+
right_state = logical_state(evaluate(node.right))
|
|
305
|
+
right_truthy = right_state == :truthy
|
|
306
|
+
right_falsy = right_state == :falsy
|
|
307
|
+
return TRUE_VALUE if right_truthy
|
|
308
|
+
return FALSE_VALUE if left_state == :falsy && right_falsy
|
|
309
|
+
|
|
310
|
+
UndefinedValue.new
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# :reek:UtilityFunction
|
|
314
|
+
def logical_state(value)
|
|
315
|
+
return :undefined if value.undefined?
|
|
316
|
+
|
|
317
|
+
value.truthy? ? :truthy : :falsy
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def every_body_succeeds?(body, bindings)
|
|
321
|
+
environment.with_bindings(bindings) do
|
|
322
|
+
query_evaluator.query_solutions(body, environment).any?
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def every_bindings(variables, collection_value)
|
|
327
|
+
bindings_for_collection(variables, collection_value)
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def bindings_for_collection(variables, collection_value)
|
|
331
|
+
case collection_value
|
|
332
|
+
when ArrayValue then array_bindings_for(variables, collection_value)
|
|
333
|
+
when SetValue then set_bindings_for(variables, collection_value)
|
|
334
|
+
when ObjectValue then object_bindings_for(variables, collection_value)
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def array_bindings_for(variables, collection_value)
|
|
339
|
+
return nil unless [1, 2].include?(variables.length)
|
|
340
|
+
|
|
341
|
+
each_array_binding(variables, collection_value)
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def set_bindings_for(variables, collection_value)
|
|
345
|
+
return nil unless variables.length == 1
|
|
346
|
+
|
|
347
|
+
each_set_binding(variables, collection_value)
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def object_bindings_for(variables, collection_value)
|
|
351
|
+
return nil unless [1, 2].include?(variables.length)
|
|
352
|
+
|
|
353
|
+
each_object_binding(variables, collection_value)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def with_every_scope(node)
|
|
357
|
+
environment.push_scope
|
|
358
|
+
shadow_every_locals(node)
|
|
359
|
+
yield
|
|
360
|
+
ensure
|
|
361
|
+
environment.pop_scope
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# :reek:TooManyStatements
|
|
365
|
+
def shadow_every_locals(node)
|
|
366
|
+
details = BoundVariableCollector.new.collect_details(node.body)
|
|
367
|
+
explicit = details[:explicit].dup
|
|
368
|
+
explicit.concat(every_variable_names(node))
|
|
369
|
+
explicit.uniq!
|
|
370
|
+
shadow_explicit_locals(explicit)
|
|
371
|
+
shadow_unification_locals(details[:unification], explicit)
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# :reek:UtilityFunction
|
|
375
|
+
def every_variable_names(node)
|
|
376
|
+
[node.key_var, node.value_var].compact.map(&:name)
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def shadow_explicit_locals(names)
|
|
380
|
+
names.each { |name| bind_undefined(name) }
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def shadow_unification_locals(names, explicit_names)
|
|
384
|
+
names.each do |name|
|
|
385
|
+
next if explicit_names.include?(name)
|
|
386
|
+
next unless environment.lookup(name).is_a?(UndefinedValue)
|
|
387
|
+
|
|
388
|
+
bind_undefined(name)
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def bind_undefined(name)
|
|
393
|
+
return if Environment::RESERVED_NAMES.include?(name) || name == "_"
|
|
394
|
+
|
|
395
|
+
environment.bind(name, UndefinedValue.new)
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def query_evaluator
|
|
399
|
+
return @query_evaluator if @query_evaluator
|
|
400
|
+
|
|
401
|
+
raise EvaluationError.new("Query evaluator not configured", rule: nil, location: nil)
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def yield_assignment_bindings(node, env, yielder)
|
|
405
|
+
value = evaluate(node.right)
|
|
406
|
+
binding_sets = unifier.unify(node.left, value, env)
|
|
407
|
+
yielder << binding_sets.first if binding_sets.size == 1
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def yield_unification_bindings(node, env, yielder)
|
|
411
|
+
unification_binding_sets(node, env).each { |bindings| yielder << bindings }
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def yield_truthy_bindings(node, yielder)
|
|
415
|
+
value = evaluate(node)
|
|
416
|
+
empty_bindings = {} # @type var empty_bindings: Hash[String, Value]
|
|
417
|
+
yielder << empty_bindings if logical_state(value) == :truthy
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def yield_reference_bindings(node, env, yielder)
|
|
421
|
+
reference_bindings_for(node, env).each do |(bindings, value)|
|
|
422
|
+
next unless logical_state(value) == :truthy
|
|
423
|
+
|
|
424
|
+
yielder << bindings
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
# :reek:TooManyStatements
|
|
429
|
+
# :reek:UtilityFunction
|
|
430
|
+
# :reek:FeatureEnvy
|
|
431
|
+
def format_template_value(value)
|
|
432
|
+
return "<undefined>" if logical_state(value) == :undefined
|
|
433
|
+
|
|
434
|
+
ruby = value.is_a?(Value) ? value.to_ruby : value
|
|
435
|
+
TemplateValueFormatter.new(ruby).render
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
# rubocop:enable Metrics/ClassLength
|
|
439
|
+
|
|
440
|
+
# Formats template string values using a stable JSON representation.
|
|
441
|
+
class TemplateValueFormatter
|
|
442
|
+
# @param value [Object]
|
|
443
|
+
def initialize(value)
|
|
444
|
+
@value = value
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
# @return [String]
|
|
448
|
+
def render
|
|
449
|
+
case value
|
|
450
|
+
when NilClass then "null"
|
|
451
|
+
when String then value
|
|
452
|
+
when Array, Hash, Set then JSON.generate(canonical_value)
|
|
453
|
+
else value.to_s
|
|
454
|
+
end
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
# @return [Object]
|
|
458
|
+
def canonical_value
|
|
459
|
+
case value
|
|
460
|
+
when Hash then canonicalize_hash
|
|
461
|
+
when Array then canonicalize_array
|
|
462
|
+
when Set then canonicalize_set
|
|
463
|
+
else value
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
private
|
|
468
|
+
|
|
469
|
+
attr_reader :value
|
|
470
|
+
|
|
471
|
+
def canonicalize_hash
|
|
472
|
+
result = {} # @type var result: Hash[untyped, untyped]
|
|
473
|
+
value.keys.sort_by(&:to_s).each do |key|
|
|
474
|
+
result[key] = self.class.new(value[key]).canonical_value
|
|
475
|
+
end
|
|
476
|
+
result
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def canonicalize_array
|
|
480
|
+
value.map { |element| self.class.new(element).canonical_value }
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
def canonicalize_set
|
|
484
|
+
value
|
|
485
|
+
.to_a
|
|
486
|
+
.map { |element| self.class.new(element).canonical_value }
|
|
487
|
+
.sort_by { |element| JSON.generate(element) }
|
|
488
|
+
end
|
|
489
|
+
end
|
|
490
|
+
end
|
|
491
|
+
end
|
|
492
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ruby
|
|
4
|
+
module Rego
|
|
5
|
+
class Evaluator
|
|
6
|
+
# Evaluates object literal nodes.
|
|
7
|
+
class ObjectLiteralEvaluator
|
|
8
|
+
# @param expression_evaluator [ExpressionEvaluator]
|
|
9
|
+
def initialize(expression_evaluator:)
|
|
10
|
+
@expression_evaluator = expression_evaluator
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# @param node [AST::ObjectLiteral]
|
|
14
|
+
# @return [Value]
|
|
15
|
+
def evaluate(node)
|
|
16
|
+
pairs = build_pairs(node)
|
|
17
|
+
return pairs if pairs.is_a?(UndefinedValue)
|
|
18
|
+
|
|
19
|
+
ObjectValue.new(pairs)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
attr_reader :expression_evaluator
|
|
25
|
+
|
|
26
|
+
def build_pairs(node)
|
|
27
|
+
pairs = node.pairs.map do |(key_node, value_node)|
|
|
28
|
+
pair = resolve_pair(key_node, value_node)
|
|
29
|
+
return UndefinedValue.new if pair.is_a?(UndefinedValue)
|
|
30
|
+
|
|
31
|
+
pair
|
|
32
|
+
end
|
|
33
|
+
pairs.to_h
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def evaluate_key(key_node)
|
|
37
|
+
expression_evaluator.evaluate(key_node).object_key
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def resolve_pair(key_node, value_node)
|
|
41
|
+
key = evaluate_key(key_node)
|
|
42
|
+
return UndefinedValue.new if key.is_a?(UndefinedValue)
|
|
43
|
+
|
|
44
|
+
value = expression_evaluator.evaluate(value_node)
|
|
45
|
+
return UndefinedValue.new if value.is_a?(UndefinedValue)
|
|
46
|
+
|
|
47
|
+
[key, value]
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ruby
|
|
4
|
+
module Rego
|
|
5
|
+
class Evaluator
|
|
6
|
+
# Shared operator application helpers.
|
|
7
|
+
# rubocop:disable Metrics/ModuleLength
|
|
8
|
+
module OperatorEvaluator
|
|
9
|
+
EQUALITY_OPERATORS = {
|
|
10
|
+
eq: lambda do |lhs, rhs|
|
|
11
|
+
return UndefinedValue.new if lhs.is_a?(UndefinedValue) || rhs.is_a?(UndefinedValue)
|
|
12
|
+
|
|
13
|
+
BooleanValue.new(lhs == rhs)
|
|
14
|
+
end,
|
|
15
|
+
neq: lambda do |lhs, rhs|
|
|
16
|
+
return UndefinedValue.new if lhs.is_a?(UndefinedValue) || rhs.is_a?(UndefinedValue)
|
|
17
|
+
|
|
18
|
+
BooleanValue.new(lhs != rhs)
|
|
19
|
+
end
|
|
20
|
+
}.freeze
|
|
21
|
+
LOGICAL_OPERATORS = {
|
|
22
|
+
and: ->(lhs, rhs) { BooleanValue.new(lhs.truthy? && rhs.truthy?) },
|
|
23
|
+
or: ->(lhs, rhs) { BooleanValue.new(lhs.truthy? || rhs.truthy?) }
|
|
24
|
+
}.freeze
|
|
25
|
+
COMPARISON_OPERATORS = {
|
|
26
|
+
lt: ->(lhs, rhs) { lhs < rhs },
|
|
27
|
+
lte: ->(lhs, rhs) { lhs <= rhs },
|
|
28
|
+
gt: ->(lhs, rhs) { lhs > rhs },
|
|
29
|
+
gte: ->(lhs, rhs) { lhs >= rhs }
|
|
30
|
+
}.freeze
|
|
31
|
+
ARITHMETIC_OPERATORS = {
|
|
32
|
+
plus: ->(lhs, rhs) { lhs + rhs },
|
|
33
|
+
minus: ->(lhs, rhs) { lhs - rhs },
|
|
34
|
+
mult: ->(lhs, rhs) { lhs * rhs },
|
|
35
|
+
div: ->(lhs, rhs) { lhs / rhs },
|
|
36
|
+
mod: ->(lhs, rhs) { lhs % rhs }
|
|
37
|
+
}.freeze
|
|
38
|
+
MEMBERSHIP_OPERATORS = {
|
|
39
|
+
in: ->(lhs, rhs) { membership_value(lhs, rhs) }
|
|
40
|
+
}.freeze
|
|
41
|
+
UNARY_OPERATORS = {
|
|
42
|
+
not: ->(operand) { BooleanValue.new(!operand.truthy?) },
|
|
43
|
+
minus: lambda do |operand|
|
|
44
|
+
number = numeric_value(operand)
|
|
45
|
+
number ? Value.from_ruby(-number) : UndefinedValue.new
|
|
46
|
+
end
|
|
47
|
+
}.freeze
|
|
48
|
+
|
|
49
|
+
# @param operator [Symbol]
|
|
50
|
+
# @param left [Value]
|
|
51
|
+
# @param right [Value]
|
|
52
|
+
# @return [Value]
|
|
53
|
+
# rubocop:disable Metrics/MethodLength
|
|
54
|
+
def self.apply(operator, left, right)
|
|
55
|
+
handlers = [
|
|
56
|
+
-> { apply_logical(operator, left, right) },
|
|
57
|
+
-> { EQUALITY_OPERATORS[operator]&.call(left, right) },
|
|
58
|
+
-> { MEMBERSHIP_OPERATORS[operator]&.call(left, right) },
|
|
59
|
+
-> { apply_comparison(operator, left, right) },
|
|
60
|
+
-> { apply_arithmetic(operator, left, right) }
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
handlers.each do |handler|
|
|
64
|
+
value = handler.call
|
|
65
|
+
return value if value
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
UndefinedValue.new
|
|
69
|
+
end
|
|
70
|
+
# rubocop:enable Metrics/MethodLength
|
|
71
|
+
|
|
72
|
+
# @param operator [Symbol]
|
|
73
|
+
# @param operand [Value]
|
|
74
|
+
# @return [Value]
|
|
75
|
+
def self.apply_unary(operator, operand)
|
|
76
|
+
handler = UNARY_OPERATORS[operator]
|
|
77
|
+
return UndefinedValue.new unless handler
|
|
78
|
+
|
|
79
|
+
handler.call(operand)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def self.apply_logical(operator, left, right)
|
|
83
|
+
handler = LOGICAL_OPERATORS[operator]
|
|
84
|
+
handler&.call(left, right)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def self.apply_comparison(operator, left, right)
|
|
88
|
+
comparison = COMPARISON_OPERATORS[operator]
|
|
89
|
+
return unless comparison
|
|
90
|
+
|
|
91
|
+
compare_values(left, right, &comparison)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def self.apply_arithmetic(operator, left, right)
|
|
95
|
+
arithmetic = ARITHMETIC_OPERATORS[operator]
|
|
96
|
+
return unless arithmetic
|
|
97
|
+
|
|
98
|
+
arithmetic_values(operator, left, right, &arithmetic)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def self.compare_values(left, right)
|
|
102
|
+
left_value = left.to_ruby
|
|
103
|
+
right_value = right.to_ruby
|
|
104
|
+
return UndefinedValue.new unless comparable?(left_value, right_value)
|
|
105
|
+
|
|
106
|
+
BooleanValue.new(yield(left_value, right_value))
|
|
107
|
+
rescue ArgumentError
|
|
108
|
+
UndefinedValue.new
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def self.comparable?(left_value, right_value)
|
|
112
|
+
(left_value.is_a?(Numeric) && right_value.is_a?(Numeric)) ||
|
|
113
|
+
(left_value.is_a?(String) && right_value.is_a?(String))
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def self.arithmetic_values(operator, left, right)
|
|
117
|
+
left_value = numeric_value(left)
|
|
118
|
+
right_value = numeric_value(right)
|
|
119
|
+
return UndefinedValue.new unless left_value && right_value
|
|
120
|
+
return UndefinedValue.new if division_by_zero?(operator, right_value)
|
|
121
|
+
|
|
122
|
+
Value.from_ruby(yield(left_value, right_value))
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def self.division_by_zero?(operator, right_value)
|
|
126
|
+
%i[div mod].include?(operator) && right_value.zero?
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def self.numeric_value(value)
|
|
130
|
+
ruby = value.to_ruby
|
|
131
|
+
return ruby if ruby.is_a?(Numeric)
|
|
132
|
+
|
|
133
|
+
nil
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def self.membership_value(lhs, rhs)
|
|
137
|
+
return UndefinedValue.new if undefined_operand?(lhs, rhs)
|
|
138
|
+
|
|
139
|
+
values = collection_values(rhs)
|
|
140
|
+
return values if values.is_a?(UndefinedValue)
|
|
141
|
+
|
|
142
|
+
BooleanValue.new(values.any? { |element| element == lhs })
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def self.undefined_operand?(lhs, rhs)
|
|
146
|
+
lhs.is_a?(UndefinedValue) || rhs.is_a?(UndefinedValue)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# :reek:DuplicateMethodCall
|
|
150
|
+
def self.collection_values(value)
|
|
151
|
+
return UndefinedValue.new unless value.is_a?(ArrayValue) || value.is_a?(SetValue) || value.is_a?(ObjectValue)
|
|
152
|
+
|
|
153
|
+
collection = value.value
|
|
154
|
+
return collection if value.is_a?(ArrayValue)
|
|
155
|
+
return collection.to_a if value.is_a?(SetValue)
|
|
156
|
+
|
|
157
|
+
collection.keys.map { |key| Value.from_ruby(key) }
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
# rubocop:enable Metrics/ModuleLength
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|