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,379 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rubocop:disable Lint/RedundantRequireStatement
|
|
4
|
+
require "set"
|
|
5
|
+
# rubocop:enable Lint/RedundantRequireStatement
|
|
6
|
+
require_relative "errors"
|
|
7
|
+
|
|
8
|
+
module Ruby
|
|
9
|
+
module Rego
|
|
10
|
+
# Base class for Rego values.
|
|
11
|
+
class Value
|
|
12
|
+
TYPE_NAME = "value"
|
|
13
|
+
|
|
14
|
+
# Create a value wrapper.
|
|
15
|
+
#
|
|
16
|
+
# @param value [Object] underlying value
|
|
17
|
+
def initialize(value = nil)
|
|
18
|
+
@value = value
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# The wrapped Ruby value.
|
|
22
|
+
#
|
|
23
|
+
# @return [Object]
|
|
24
|
+
attr_reader :value
|
|
25
|
+
|
|
26
|
+
# Determine truthiness for Rego evaluation.
|
|
27
|
+
#
|
|
28
|
+
# @return [Boolean]
|
|
29
|
+
def truthy?
|
|
30
|
+
true
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Convert the value back to Ruby.
|
|
34
|
+
#
|
|
35
|
+
# @return [Object]
|
|
36
|
+
def to_ruby
|
|
37
|
+
value
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Return the Rego type name.
|
|
41
|
+
#
|
|
42
|
+
# @return [String]
|
|
43
|
+
def type_name
|
|
44
|
+
self.class::TYPE_NAME
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Resolve a reference on the value.
|
|
48
|
+
#
|
|
49
|
+
# @param _key [Object] reference key
|
|
50
|
+
# @return [Value] resolved value or undefined
|
|
51
|
+
def fetch_reference(_key)
|
|
52
|
+
return self if undefined?
|
|
53
|
+
|
|
54
|
+
UndefinedValue.new
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Check if the value is undefined.
|
|
58
|
+
#
|
|
59
|
+
# @return [Boolean]
|
|
60
|
+
def undefined?
|
|
61
|
+
is_a?(UndefinedValue)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Return a normalized object key representation.
|
|
65
|
+
#
|
|
66
|
+
# @return [Object]
|
|
67
|
+
def object_key
|
|
68
|
+
to_ruby
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Compare values by class and underlying Ruby value.
|
|
72
|
+
#
|
|
73
|
+
# @param other [Object]
|
|
74
|
+
# @return [Boolean]
|
|
75
|
+
def ==(other)
|
|
76
|
+
other.is_a?(self.class) && other.to_ruby == to_ruby
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
alias eql? ==
|
|
80
|
+
|
|
81
|
+
# Hash for use in Ruby collections.
|
|
82
|
+
#
|
|
83
|
+
# @return [Integer]
|
|
84
|
+
def hash
|
|
85
|
+
[self.class.name, to_ruby].hash
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Coerce Ruby values into Rego values.
|
|
89
|
+
#
|
|
90
|
+
# @param value [Object] value to convert
|
|
91
|
+
# @return [Value] converted value
|
|
92
|
+
def self.from_ruby(value)
|
|
93
|
+
return value if value.is_a?(Value)
|
|
94
|
+
return UndefinedValue.new if value.equal?(UndefinedValue::UNDEFINED)
|
|
95
|
+
|
|
96
|
+
built_value = build_value(value)
|
|
97
|
+
return built_value if built_value
|
|
98
|
+
|
|
99
|
+
raise ArgumentError, "Unsupported value type: #{value.class}"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def self.build_value(value)
|
|
103
|
+
build_simple_value(value) || build_composite_value(value)
|
|
104
|
+
end
|
|
105
|
+
private_class_method :build_value
|
|
106
|
+
|
|
107
|
+
def self.build_simple_value(value)
|
|
108
|
+
case value
|
|
109
|
+
when NilClass
|
|
110
|
+
NullValue.new
|
|
111
|
+
when TrueClass, FalseClass
|
|
112
|
+
BooleanValue.new(value)
|
|
113
|
+
when String
|
|
114
|
+
StringValue.new(value)
|
|
115
|
+
when Numeric
|
|
116
|
+
NumberValue.new(value)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
private_class_method :build_simple_value
|
|
120
|
+
|
|
121
|
+
def self.build_composite_value(value)
|
|
122
|
+
case value
|
|
123
|
+
when Array
|
|
124
|
+
ArrayValue.new(value)
|
|
125
|
+
when Hash
|
|
126
|
+
ObjectValue.new(value)
|
|
127
|
+
when Set
|
|
128
|
+
SetValue.new(value)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
private_class_method :build_composite_value
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Represents a string value.
|
|
135
|
+
class StringValue < Value
|
|
136
|
+
TYPE_NAME = "string"
|
|
137
|
+
|
|
138
|
+
# Create a string value.
|
|
139
|
+
#
|
|
140
|
+
# @param value [String] string value
|
|
141
|
+
def initialize(value)
|
|
142
|
+
super(String(value))
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Represents a numeric value.
|
|
147
|
+
class NumberValue < Value
|
|
148
|
+
TYPE_NAME = "number"
|
|
149
|
+
|
|
150
|
+
# Create a numeric value.
|
|
151
|
+
#
|
|
152
|
+
# @param value [Numeric] numeric value
|
|
153
|
+
def initialize(value)
|
|
154
|
+
raise ArgumentError, "Expected Numeric, got #{value.class}" unless value.is_a?(Numeric)
|
|
155
|
+
|
|
156
|
+
super
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Represents a boolean value.
|
|
161
|
+
class BooleanValue < Value
|
|
162
|
+
TYPE_NAME = "boolean"
|
|
163
|
+
|
|
164
|
+
# Create a boolean value.
|
|
165
|
+
#
|
|
166
|
+
# @param value [Boolean] boolean value
|
|
167
|
+
def initialize(value)
|
|
168
|
+
klass = value.class
|
|
169
|
+
raise ArgumentError, "Expected Boolean, got #{klass}" unless [TrueClass, FalseClass].include?(klass)
|
|
170
|
+
|
|
171
|
+
super
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Determine truthiness.
|
|
175
|
+
#
|
|
176
|
+
# @return [Boolean]
|
|
177
|
+
def truthy?
|
|
178
|
+
value
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Represents a null value.
|
|
183
|
+
class NullValue < Value
|
|
184
|
+
TYPE_NAME = "null"
|
|
185
|
+
|
|
186
|
+
# Create a null value.
|
|
187
|
+
def initialize
|
|
188
|
+
super(nil)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Null is falsy.
|
|
192
|
+
#
|
|
193
|
+
# @return [Boolean]
|
|
194
|
+
def truthy?
|
|
195
|
+
false
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Represents an undefined value.
|
|
200
|
+
class UndefinedValue < Value
|
|
201
|
+
TYPE_NAME = "undefined"
|
|
202
|
+
UNDEFINED = Object.new.freeze
|
|
203
|
+
|
|
204
|
+
# Create an undefined value marker.
|
|
205
|
+
def initialize
|
|
206
|
+
super(UNDEFINED)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Undefined is falsy.
|
|
210
|
+
#
|
|
211
|
+
# @return [Boolean]
|
|
212
|
+
def truthy?
|
|
213
|
+
false
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Return the singleton undefined marker.
|
|
217
|
+
#
|
|
218
|
+
# @return [Object]
|
|
219
|
+
def to_ruby
|
|
220
|
+
UNDEFINED
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Use the instance itself as an object key.
|
|
224
|
+
#
|
|
225
|
+
# @return [UndefinedValue]
|
|
226
|
+
def object_key
|
|
227
|
+
self
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Represents an array value.
|
|
232
|
+
class ArrayValue < Value
|
|
233
|
+
TYPE_NAME = "array"
|
|
234
|
+
|
|
235
|
+
# Create an array value.
|
|
236
|
+
#
|
|
237
|
+
# @param elements [Array<Object>] elements to wrap
|
|
238
|
+
def initialize(elements)
|
|
239
|
+
@elements = elements.map { |element| Value.from_ruby(element) }
|
|
240
|
+
super(@elements)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Fetch an element by index.
|
|
244
|
+
#
|
|
245
|
+
# @param index [Integer] array index
|
|
246
|
+
# @return [Value] element or undefined
|
|
247
|
+
def fetch_index(index)
|
|
248
|
+
return UndefinedValue.new unless index.is_a?(Integer)
|
|
249
|
+
|
|
250
|
+
@elements[index] || UndefinedValue.new
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Resolve a reference for an array.
|
|
254
|
+
#
|
|
255
|
+
# @param key [Object] index value
|
|
256
|
+
# @return [Value] element or undefined
|
|
257
|
+
def fetch_reference(key)
|
|
258
|
+
fetch_index(key)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Convert the array back to Ruby.
|
|
262
|
+
#
|
|
263
|
+
# @return [Array<Object>]
|
|
264
|
+
def to_ruby
|
|
265
|
+
@elements.map(&:to_ruby)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
private
|
|
269
|
+
|
|
270
|
+
attr_reader :elements
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Represents an object value.
|
|
274
|
+
class ObjectValue < Value
|
|
275
|
+
TYPE_NAME = "object"
|
|
276
|
+
|
|
277
|
+
# Create an object value.
|
|
278
|
+
#
|
|
279
|
+
# @param pairs [Hash<Object, Object>] object pairs
|
|
280
|
+
def initialize(pairs)
|
|
281
|
+
@values = normalize_pairs(pairs)
|
|
282
|
+
super(@values)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# :reek:TooManyStatements
|
|
286
|
+
# :reek:FeatureEnvy
|
|
287
|
+
def normalize_pairs(pairs)
|
|
288
|
+
values = {} # @type var values: Hash[Object, Value]
|
|
289
|
+
key_sources = {} # @type var key_sources: Hash[Object, Object]
|
|
290
|
+
pairs.each_with_object(values) do |(key, val), acc|
|
|
291
|
+
normalized_key = key.is_a?(Symbol) ? key.to_s : key
|
|
292
|
+
ensure_unique_key(normalized_key, key_sources, key)
|
|
293
|
+
acc[normalized_key] = Value.from_ruby(val)
|
|
294
|
+
end
|
|
295
|
+
values
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# :reek:FeatureEnvy
|
|
299
|
+
def ensure_unique_key(normalized_key, key_sources, key)
|
|
300
|
+
return unless key_sources.key?(normalized_key)
|
|
301
|
+
|
|
302
|
+
existing_key = key_sources[normalized_key]
|
|
303
|
+
raise ObjectKeyConflictError, "Conflicting object keys: #{existing_key.inspect} and #{key.inspect}"
|
|
304
|
+
ensure
|
|
305
|
+
key_sources[normalized_key] = key
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Fetch a value by key.
|
|
309
|
+
#
|
|
310
|
+
# @param key [Object] object key
|
|
311
|
+
# @return [Value] value or undefined
|
|
312
|
+
def fetch(key)
|
|
313
|
+
return @values[key] if @values.key?(key)
|
|
314
|
+
|
|
315
|
+
return fetch_by_symbol_key(key) if key.is_a?(Symbol)
|
|
316
|
+
|
|
317
|
+
UndefinedValue.new
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Resolve a reference for an object.
|
|
321
|
+
#
|
|
322
|
+
# @param key [Object] object key
|
|
323
|
+
# @return [Value] value or undefined
|
|
324
|
+
def fetch_reference(key)
|
|
325
|
+
fetch(key)
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def fetch_by_symbol_key(key)
|
|
329
|
+
string_key = key.to_s
|
|
330
|
+
@values[string_key] || UndefinedValue.new
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# Convert the object back to Ruby.
|
|
334
|
+
#
|
|
335
|
+
# @return [Hash<Object, Object>]
|
|
336
|
+
def to_ruby
|
|
337
|
+
@values.transform_values(&:to_ruby)
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
private
|
|
341
|
+
|
|
342
|
+
attr_reader :values
|
|
343
|
+
private :fetch_by_symbol_key, :normalize_pairs, :ensure_unique_key
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# Represents a set value.
|
|
347
|
+
class SetValue < Value
|
|
348
|
+
TYPE_NAME = "set"
|
|
349
|
+
|
|
350
|
+
# Create a set value.
|
|
351
|
+
#
|
|
352
|
+
# @param elements [Set<Object>, Array<Object>] set elements
|
|
353
|
+
def initialize(elements)
|
|
354
|
+
collection = elements.is_a?(Set) ? elements.to_a : Array(elements)
|
|
355
|
+
@elements = Set.new(collection.map { |element| Value.from_ruby(element) })
|
|
356
|
+
super(@elements)
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# Check whether the set includes a value.
|
|
360
|
+
#
|
|
361
|
+
# @param value [Object] value to check
|
|
362
|
+
# @return [Boolean]
|
|
363
|
+
def include?(value)
|
|
364
|
+
@elements.include?(Value.from_ruby(value))
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# Convert the set back to Ruby.
|
|
368
|
+
#
|
|
369
|
+
# @return [Set<Object>]
|
|
370
|
+
def to_ruby
|
|
371
|
+
Set.new(@elements.map(&:to_ruby))
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
private
|
|
375
|
+
|
|
376
|
+
attr_reader :elements
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "with_modifier_context"
|
|
4
|
+
|
|
5
|
+
module Ruby
|
|
6
|
+
module Rego
|
|
7
|
+
module WithModifiers
|
|
8
|
+
# Applies `with` modifiers to a temporary environment.
|
|
9
|
+
class WithModifier
|
|
10
|
+
# @param target [Object]
|
|
11
|
+
# @param value [Object]
|
|
12
|
+
def initialize(target:, value:)
|
|
13
|
+
@target = target
|
|
14
|
+
@value = value
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @return [Object]
|
|
18
|
+
attr_reader :target
|
|
19
|
+
|
|
20
|
+
# @return [Object]
|
|
21
|
+
attr_reader :value
|
|
22
|
+
|
|
23
|
+
# @param environment [Environment]
|
|
24
|
+
# @param expression_evaluator [Evaluator::ExpressionEvaluator]
|
|
25
|
+
# @yieldparam environment [Environment]
|
|
26
|
+
# @return [Object]
|
|
27
|
+
def with_environment(environment, expression_evaluator, &)
|
|
28
|
+
WithModifierContext.new(
|
|
29
|
+
modifier: self,
|
|
30
|
+
environment: environment,
|
|
31
|
+
expression_evaluator: expression_evaluator
|
|
32
|
+
).apply(&)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "with_modifier"
|
|
4
|
+
|
|
5
|
+
module Ruby
|
|
6
|
+
module Rego
|
|
7
|
+
module WithModifiers
|
|
8
|
+
# Applies a sequence of with modifiers around a block.
|
|
9
|
+
class WithModifierApplier
|
|
10
|
+
# @param modifiers [Array<AST::WithModifier>]
|
|
11
|
+
# @param environment [Environment]
|
|
12
|
+
# @param expression_evaluator [Evaluator::ExpressionEvaluator]
|
|
13
|
+
# @yieldparam environment [Environment]
|
|
14
|
+
# @return [Object]
|
|
15
|
+
def self.apply(modifiers, environment, expression_evaluator, &block)
|
|
16
|
+
block ||= ->(_env) {}
|
|
17
|
+
return block.call(environment) if modifiers.empty?
|
|
18
|
+
|
|
19
|
+
build_chain(modifiers, expression_evaluator, block).call(environment)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.build_chain(modifiers, expression_evaluator, block)
|
|
23
|
+
index = modifiers.length - 1
|
|
24
|
+
chain = block
|
|
25
|
+
|
|
26
|
+
while index >= 0
|
|
27
|
+
chain = wrap_modifier(modifiers[index], expression_evaluator, chain)
|
|
28
|
+
index -= 1
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
chain
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.wrap_modifier(modifier, expression_evaluator, block)
|
|
35
|
+
lambda do |env|
|
|
36
|
+
WithModifier.new(target: modifier.target, value: modifier.value).with_environment(
|
|
37
|
+
env,
|
|
38
|
+
expression_evaluator,
|
|
39
|
+
&block
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private_class_method :build_chain, :wrap_modifier
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../ast"
|
|
4
|
+
require_relative "../builtins/registry"
|
|
5
|
+
require_relative "../errors"
|
|
6
|
+
require_relative "../value"
|
|
7
|
+
|
|
8
|
+
module Ruby
|
|
9
|
+
module Rego
|
|
10
|
+
module WithModifiers
|
|
11
|
+
# Temporarily replaces a builtin with another builtin implementation.
|
|
12
|
+
class WithModifierBuiltinOverride
|
|
13
|
+
# @param name [String]
|
|
14
|
+
# @param value [Object]
|
|
15
|
+
# @param expression_evaluator [Evaluator::ExpressionEvaluator]
|
|
16
|
+
# @param location [Location, nil]
|
|
17
|
+
def initialize(name:, value:, expression_evaluator:, location: nil)
|
|
18
|
+
@name = name.to_s
|
|
19
|
+
@value = value
|
|
20
|
+
@expression_evaluator = expression_evaluator
|
|
21
|
+
@location = location
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @yieldparam environment [Environment]
|
|
25
|
+
# @return [Object]
|
|
26
|
+
def apply(environment, &block)
|
|
27
|
+
block ||= ->(_env) {}
|
|
28
|
+
registry = environment.builtin_registry
|
|
29
|
+
registry.entry_for(name)
|
|
30
|
+
override_registry = registry.with_override(name, override_entry(registry, environment))
|
|
31
|
+
environment.with_builtin_registry(override_registry, &block)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
attr_reader :name, :value, :expression_evaluator, :location
|
|
37
|
+
|
|
38
|
+
def override_entry(registry, environment)
|
|
39
|
+
original = registry.entry_for(name)
|
|
40
|
+
replacement = replacement_entry(registry, environment)
|
|
41
|
+
ensure_matching_arity(original.arity, replacement.arity)
|
|
42
|
+
Builtins::BuiltinRegistry::Entry.new(name: name, arity: replacement.arity, handler: replacement.handler)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def replacement_entry(registry, environment)
|
|
46
|
+
replacement = replacement_name
|
|
47
|
+
return registry.entry_for(replacement) if registry.registered?(replacement)
|
|
48
|
+
|
|
49
|
+
function_entry(environment, replacement)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# rubocop:disable Metrics/MethodLength
|
|
53
|
+
def function_entry(environment, function_name)
|
|
54
|
+
# @type var empty_rules: Array[AST::Rule]
|
|
55
|
+
empty_rules = []
|
|
56
|
+
rules = environment.rules.fetch(function_name.to_s) { empty_rules }
|
|
57
|
+
function_rule = rules.find(&:function?)
|
|
58
|
+
unless function_rule
|
|
59
|
+
raise EvaluationError.new(
|
|
60
|
+
"With modifier expects a builtin function name",
|
|
61
|
+
rule: nil,
|
|
62
|
+
location: location
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
arity = Array(function_rule.head[:args]).length
|
|
67
|
+
handler = lambda do |*args|
|
|
68
|
+
expression_evaluator.evaluate_user_function(function_name, args)
|
|
69
|
+
end
|
|
70
|
+
Builtins::BuiltinRegistry::Entry.new(name: function_name, arity: arity, handler: handler)
|
|
71
|
+
end
|
|
72
|
+
# rubocop:enable Metrics/MethodLength
|
|
73
|
+
|
|
74
|
+
def ensure_matching_arity(expected, actual)
|
|
75
|
+
expected_list = normalize_arity_list(expected)
|
|
76
|
+
actual_list = normalize_arity_list(actual)
|
|
77
|
+
return if expected_list.sort == actual_list.sort
|
|
78
|
+
|
|
79
|
+
raise EvaluationError.new(
|
|
80
|
+
"With modifier function arity mismatch",
|
|
81
|
+
rule: nil,
|
|
82
|
+
location: location
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# :reek:UtilityFunction
|
|
87
|
+
def normalize_arity_list(arity)
|
|
88
|
+
arity.is_a?(Array) ? arity : [arity]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def replacement_name
|
|
92
|
+
direct_name = direct_name_from_value
|
|
93
|
+
return direct_name if direct_name
|
|
94
|
+
|
|
95
|
+
resolved_name = resolved_name_from_value
|
|
96
|
+
return resolved_name if resolved_name
|
|
97
|
+
|
|
98
|
+
raise EvaluationError.new(
|
|
99
|
+
"With modifier expects a builtin function name",
|
|
100
|
+
rule: nil,
|
|
101
|
+
location: location
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def direct_name_from_value
|
|
106
|
+
return value.name if value.is_a?(AST::Variable)
|
|
107
|
+
return reference_name_from_value if value.is_a?(AST::Reference)
|
|
108
|
+
return value.value if value.is_a?(AST::StringLiteral)
|
|
109
|
+
|
|
110
|
+
nil
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def reference_name_from_value
|
|
114
|
+
base = value.base
|
|
115
|
+
return base.name if base.is_a?(AST::Variable) && value.path.empty?
|
|
116
|
+
|
|
117
|
+
nil
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def resolved_name_from_value
|
|
121
|
+
resolved = expression_evaluator.evaluate(value)
|
|
122
|
+
resolved_value = resolved.is_a?(Value) ? resolved.to_ruby : resolved
|
|
123
|
+
resolved_value if resolved_value.is_a?(String)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|