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.
Files changed (124) hide show
  1. checksums.yaml +7 -0
  2. data/.reek.yml +80 -0
  3. data/.vscode/extensions.json +19 -0
  4. data/.vscode/launch.json +35 -0
  5. data/.vscode/settings.json +25 -0
  6. data/.vscode/tasks.json +117 -0
  7. data/.yardopts +12 -0
  8. data/ARCHITECTURE.md +39 -0
  9. data/CHANGELOG.md +25 -0
  10. data/CODE_OF_CONDUCT.md +10 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +183 -0
  13. data/RELEASING.md +37 -0
  14. data/Rakefile +38 -0
  15. data/SECURITY.md +26 -0
  16. data/Steepfile +10 -0
  17. data/TODO.md +35 -0
  18. data/benchmark/builtin_calls.rb +29 -0
  19. data/benchmark/complex_policy.rb +19 -0
  20. data/benchmark/comprehensions.rb +19 -0
  21. data/benchmark/simple_rules.rb +20 -0
  22. data/examples/README.md +27 -0
  23. data/examples/sample_config.yaml +2 -0
  24. data/examples/simple_policy.rego +7 -0
  25. data/examples/validation_policy.rego +11 -0
  26. data/exe/rego-validate +6 -0
  27. data/lib/ruby/rego/ast/base.rb +95 -0
  28. data/lib/ruby/rego/ast/binary_op.rb +64 -0
  29. data/lib/ruby/rego/ast/call.rb +27 -0
  30. data/lib/ruby/rego/ast/composite.rb +48 -0
  31. data/lib/ruby/rego/ast/comprehension.rb +63 -0
  32. data/lib/ruby/rego/ast/every.rb +37 -0
  33. data/lib/ruby/rego/ast/import.rb +32 -0
  34. data/lib/ruby/rego/ast/literal.rb +70 -0
  35. data/lib/ruby/rego/ast/module.rb +32 -0
  36. data/lib/ruby/rego/ast/package.rb +22 -0
  37. data/lib/ruby/rego/ast/query.rb +63 -0
  38. data/lib/ruby/rego/ast/reference.rb +58 -0
  39. data/lib/ruby/rego/ast/rule.rb +114 -0
  40. data/lib/ruby/rego/ast/unary_op.rb +42 -0
  41. data/lib/ruby/rego/ast/variable.rb +22 -0
  42. data/lib/ruby/rego/ast.rb +17 -0
  43. data/lib/ruby/rego/builtins/aggregates.rb +124 -0
  44. data/lib/ruby/rego/builtins/base.rb +95 -0
  45. data/lib/ruby/rego/builtins/collections/array_ops.rb +103 -0
  46. data/lib/ruby/rego/builtins/collections/object_ops.rb +120 -0
  47. data/lib/ruby/rego/builtins/collections/set_ops.rb +51 -0
  48. data/lib/ruby/rego/builtins/collections.rb +137 -0
  49. data/lib/ruby/rego/builtins/comparisons/casts.rb +139 -0
  50. data/lib/ruby/rego/builtins/comparisons.rb +84 -0
  51. data/lib/ruby/rego/builtins/numeric_helpers.rb +56 -0
  52. data/lib/ruby/rego/builtins/registry.rb +199 -0
  53. data/lib/ruby/rego/builtins/registry_helpers.rb +27 -0
  54. data/lib/ruby/rego/builtins/strings/case_ops.rb +22 -0
  55. data/lib/ruby/rego/builtins/strings/concat.rb +19 -0
  56. data/lib/ruby/rego/builtins/strings/formatting.rb +35 -0
  57. data/lib/ruby/rego/builtins/strings/helpers.rb +62 -0
  58. data/lib/ruby/rego/builtins/strings/number_helpers.rb +48 -0
  59. data/lib/ruby/rego/builtins/strings/search.rb +63 -0
  60. data/lib/ruby/rego/builtins/strings/split.rb +19 -0
  61. data/lib/ruby/rego/builtins/strings/substring.rb +22 -0
  62. data/lib/ruby/rego/builtins/strings/trim.rb +42 -0
  63. data/lib/ruby/rego/builtins/strings/trim_helpers.rb +62 -0
  64. data/lib/ruby/rego/builtins/strings.rb +58 -0
  65. data/lib/ruby/rego/builtins/types.rb +89 -0
  66. data/lib/ruby/rego/call_name.rb +55 -0
  67. data/lib/ruby/rego/cli.rb +1122 -0
  68. data/lib/ruby/rego/compiled_module.rb +114 -0
  69. data/lib/ruby/rego/compiler.rb +1097 -0
  70. data/lib/ruby/rego/environment/overrides.rb +33 -0
  71. data/lib/ruby/rego/environment/reference_resolution.rb +86 -0
  72. data/lib/ruby/rego/environment.rb +230 -0
  73. data/lib/ruby/rego/environment_pool.rb +71 -0
  74. data/lib/ruby/rego/error_handling.rb +58 -0
  75. data/lib/ruby/rego/error_payload.rb +34 -0
  76. data/lib/ruby/rego/errors.rb +196 -0
  77. data/lib/ruby/rego/evaluator/assignment_support.rb +126 -0
  78. data/lib/ruby/rego/evaluator/binding_helpers.rb +60 -0
  79. data/lib/ruby/rego/evaluator/comprehension_evaluator.rb +182 -0
  80. data/lib/ruby/rego/evaluator/expression_dispatch.rb +45 -0
  81. data/lib/ruby/rego/evaluator/expression_evaluator.rb +492 -0
  82. data/lib/ruby/rego/evaluator/object_literal_evaluator.rb +52 -0
  83. data/lib/ruby/rego/evaluator/operator_evaluator.rb +163 -0
  84. data/lib/ruby/rego/evaluator/query_node_builder.rb +38 -0
  85. data/lib/ruby/rego/evaluator/reference_key_resolver.rb +50 -0
  86. data/lib/ruby/rego/evaluator/reference_resolver.rb +352 -0
  87. data/lib/ruby/rego/evaluator/rule_evaluator/bindings.rb +70 -0
  88. data/lib/ruby/rego/evaluator/rule_evaluator.rb +550 -0
  89. data/lib/ruby/rego/evaluator/rule_value_provider.rb +56 -0
  90. data/lib/ruby/rego/evaluator/variable_collector.rb +221 -0
  91. data/lib/ruby/rego/evaluator.rb +174 -0
  92. data/lib/ruby/rego/lexer/number_reader.rb +68 -0
  93. data/lib/ruby/rego/lexer/stream.rb +137 -0
  94. data/lib/ruby/rego/lexer/string_reader.rb +90 -0
  95. data/lib/ruby/rego/lexer/template_string_reader.rb +62 -0
  96. data/lib/ruby/rego/lexer.rb +206 -0
  97. data/lib/ruby/rego/location.rb +73 -0
  98. data/lib/ruby/rego/memoization.rb +67 -0
  99. data/lib/ruby/rego/parser/collections.rb +173 -0
  100. data/lib/ruby/rego/parser/expressions.rb +216 -0
  101. data/lib/ruby/rego/parser/precedence.rb +42 -0
  102. data/lib/ruby/rego/parser/query.rb +139 -0
  103. data/lib/ruby/rego/parser/references.rb +115 -0
  104. data/lib/ruby/rego/parser/rules.rb +310 -0
  105. data/lib/ruby/rego/parser.rb +210 -0
  106. data/lib/ruby/rego/policy.rb +50 -0
  107. data/lib/ruby/rego/result.rb +91 -0
  108. data/lib/ruby/rego/token.rb +206 -0
  109. data/lib/ruby/rego/unifier.rb +451 -0
  110. data/lib/ruby/rego/value.rb +379 -0
  111. data/lib/ruby/rego/version.rb +7 -0
  112. data/lib/ruby/rego/with_modifiers/with_modifier.rb +37 -0
  113. data/lib/ruby/rego/with_modifiers/with_modifier_applier.rb +48 -0
  114. data/lib/ruby/rego/with_modifiers/with_modifier_builtin_override.rb +128 -0
  115. data/lib/ruby/rego/with_modifiers/with_modifier_context.rb +120 -0
  116. data/lib/ruby/rego/with_modifiers/with_modifier_path_key_resolver.rb +42 -0
  117. data/lib/ruby/rego/with_modifiers/with_modifier_path_override.rb +99 -0
  118. data/lib/ruby/rego/with_modifiers/with_modifier_root_scope.rb +58 -0
  119. data/lib/ruby/rego.rb +72 -0
  120. data/sig/objspace.rbs +4 -0
  121. data/sig/psych.rbs +7 -0
  122. data/sig/rego_validate.rbs +382 -0
  123. data/sig/ruby/rego.rbs +2150 -0
  124. 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