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,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../value"
|
|
4
|
+
|
|
5
|
+
module Ruby
|
|
6
|
+
module Rego
|
|
7
|
+
# Provides temporary input/data overrides for the environment.
|
|
8
|
+
module EnvironmentOverrides
|
|
9
|
+
UNSET = Object.new.freeze
|
|
10
|
+
|
|
11
|
+
# @param input [Object]
|
|
12
|
+
# @param data [Object]
|
|
13
|
+
# @yieldparam environment [Environment]
|
|
14
|
+
# @return [Object]
|
|
15
|
+
def with_overrides(input: UNSET, data: UNSET)
|
|
16
|
+
original = [@input, @data]
|
|
17
|
+
memoization.with_context do
|
|
18
|
+
apply_overrides(input, data)
|
|
19
|
+
yield self
|
|
20
|
+
end
|
|
21
|
+
ensure
|
|
22
|
+
@input, @data = original
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def apply_overrides(input, data)
|
|
28
|
+
@input = Value.from_ruby(input) unless input.equal?(UNSET)
|
|
29
|
+
@data = Value.from_ruby(data) unless data.equal?(UNSET)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../ast"
|
|
4
|
+
require_relative "../value"
|
|
5
|
+
|
|
6
|
+
module Ruby
|
|
7
|
+
module Rego
|
|
8
|
+
# Reference resolution helpers for Environment.
|
|
9
|
+
module EnvironmentReferenceResolution
|
|
10
|
+
# @param ref [Object]
|
|
11
|
+
# @return [Value]
|
|
12
|
+
# :reek:FeatureEnvy
|
|
13
|
+
def resolve_reference(ref)
|
|
14
|
+
base, path = if ref.is_a?(AST::Reference)
|
|
15
|
+
[ref.base, ref.path]
|
|
16
|
+
else
|
|
17
|
+
path = [] # @type var path: Array[AST::RefArg]
|
|
18
|
+
[ref, path]
|
|
19
|
+
end
|
|
20
|
+
resolve_reference_path(resolve_base(base), path)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @param variable [AST::Variable]
|
|
24
|
+
# @return [Object]
|
|
25
|
+
def reference_key_for(variable)
|
|
26
|
+
resolve_reference_variable(variable)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# :reek:TooManyStatements
|
|
32
|
+
# :reek:FeatureEnvy
|
|
33
|
+
def resolve_base(base)
|
|
34
|
+
return lookup(base.name) if base.is_a?(AST::Variable)
|
|
35
|
+
return base if base.is_a?(Value)
|
|
36
|
+
return lookup(base.to_s) if base.is_a?(String) || base.is_a?(Symbol)
|
|
37
|
+
|
|
38
|
+
value = base.is_a?(AST::Literal) ? base.value : base
|
|
39
|
+
Value.from_ruby(value)
|
|
40
|
+
rescue ArgumentError
|
|
41
|
+
UndefinedValue.new
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# :reek:FeatureEnvy
|
|
45
|
+
def resolve_path_segment(current, segment)
|
|
46
|
+
key = extract_reference_key(segment)
|
|
47
|
+
return UndefinedValue.new if key.is_a?(UndefinedValue)
|
|
48
|
+
|
|
49
|
+
return current.fetch(key) if current.is_a?(ObjectValue)
|
|
50
|
+
return current.fetch_index(key) if current.is_a?(ArrayValue)
|
|
51
|
+
|
|
52
|
+
UndefinedValue.new
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# :reek:FeatureEnvy
|
|
56
|
+
def resolve_reference_path(current, path)
|
|
57
|
+
path.each do |segment|
|
|
58
|
+
current = resolve_path_segment(current, segment)
|
|
59
|
+
return current if current.is_a?(UndefinedValue)
|
|
60
|
+
end
|
|
61
|
+
current
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# :reek:TooManyStatements
|
|
65
|
+
# :reek:FeatureEnvy
|
|
66
|
+
def extract_reference_key(segment)
|
|
67
|
+
value = segment.is_a?(AST::RefArg) ? segment.value : segment
|
|
68
|
+
return value.value if value.is_a?(AST::Literal)
|
|
69
|
+
return resolve_reference_variable(value) if value.is_a?(AST::Variable)
|
|
70
|
+
return value.to_ruby if value.is_a?(Value)
|
|
71
|
+
|
|
72
|
+
Value.from_ruby(value).to_ruby
|
|
73
|
+
rescue ArgumentError
|
|
74
|
+
UndefinedValue.new
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# :reek:FeatureEnvy
|
|
78
|
+
def resolve_reference_variable(value)
|
|
79
|
+
resolved = lookup(value.name)
|
|
80
|
+
return resolved if resolved.is_a?(UndefinedValue)
|
|
81
|
+
|
|
82
|
+
resolved.to_ruby
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "ast"
|
|
4
|
+
require_relative "errors"
|
|
5
|
+
require_relative "value"
|
|
6
|
+
require_relative "builtins/registry"
|
|
7
|
+
require_relative "memoization"
|
|
8
|
+
require_relative "environment/overrides"
|
|
9
|
+
require_relative "environment/reference_resolution"
|
|
10
|
+
|
|
11
|
+
module Ruby
|
|
12
|
+
module Rego
|
|
13
|
+
# Execution environment for evaluating Rego policies.
|
|
14
|
+
# :reek:TooManyInstanceVariables
|
|
15
|
+
# rubocop:disable Metrics/ClassLength
|
|
16
|
+
class Environment
|
|
17
|
+
# Encapsulates environment state for pooling.
|
|
18
|
+
State = Struct.new(:input, :data, :rules, :builtin_registry, keyword_init: true)
|
|
19
|
+
|
|
20
|
+
RESERVED_BINDINGS = {
|
|
21
|
+
"input" => :input,
|
|
22
|
+
"data" => :data
|
|
23
|
+
}.freeze
|
|
24
|
+
RESERVED_NAMES = RESERVED_BINDINGS.keys.freeze
|
|
25
|
+
|
|
26
|
+
# Input document as a Rego value.
|
|
27
|
+
#
|
|
28
|
+
# @return [Value]
|
|
29
|
+
attr_reader :input
|
|
30
|
+
|
|
31
|
+
# Data document as a Rego value.
|
|
32
|
+
#
|
|
33
|
+
# @return [Value]
|
|
34
|
+
attr_reader :data
|
|
35
|
+
|
|
36
|
+
# Rule index by name.
|
|
37
|
+
#
|
|
38
|
+
# @return [Hash]
|
|
39
|
+
attr_reader :rules
|
|
40
|
+
|
|
41
|
+
# Builtin registry in use.
|
|
42
|
+
#
|
|
43
|
+
# @return [Builtins::BuiltinRegistry, Builtins::BuiltinRegistryOverlay]
|
|
44
|
+
attr_reader :builtin_registry
|
|
45
|
+
|
|
46
|
+
# Memoization store for evaluation caches.
|
|
47
|
+
#
|
|
48
|
+
# @return [Memoization::Store]
|
|
49
|
+
attr_reader :memoization
|
|
50
|
+
|
|
51
|
+
# Create an evaluation environment.
|
|
52
|
+
#
|
|
53
|
+
# @param input [Object] input document
|
|
54
|
+
# @param data [Object] data document
|
|
55
|
+
# @param rules [Hash] rule index
|
|
56
|
+
# @param builtin_registry [Builtins::BuiltinRegistry, Builtins::BuiltinRegistryOverlay] registry
|
|
57
|
+
def initialize(input: {}, data: {}, rules: {}, builtin_registry: Builtins::BuiltinRegistry.instance)
|
|
58
|
+
@memoization = Memoization::Store.new
|
|
59
|
+
@builtin_registry = builtin_registry
|
|
60
|
+
@locals = [fresh_scope] # @type var locals: Array[Hash[String, Value]]
|
|
61
|
+
@scope_pool = [] # @type var @scope_pool: Array[Hash[String, Value]]
|
|
62
|
+
apply_state(State.new(input: input, data: data, rules: rules, builtin_registry: builtin_registry))
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Build an environment from a state struct.
|
|
66
|
+
#
|
|
67
|
+
# @param state [State]
|
|
68
|
+
# @return [Environment]
|
|
69
|
+
def self.from_state(state)
|
|
70
|
+
new(
|
|
71
|
+
input: state.input,
|
|
72
|
+
data: state.data,
|
|
73
|
+
rules: state.rules,
|
|
74
|
+
builtin_registry: state.builtin_registry
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
include EnvironmentOverrides
|
|
79
|
+
include EnvironmentReferenceResolution
|
|
80
|
+
|
|
81
|
+
# Push a new scope for local bindings.
|
|
82
|
+
#
|
|
83
|
+
# @return [Environment] self
|
|
84
|
+
def push_scope
|
|
85
|
+
scope = scope_pool.pop
|
|
86
|
+
scope ||= fresh_scope
|
|
87
|
+
scope.clear
|
|
88
|
+
locals << scope
|
|
89
|
+
self
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Pop the latest local scope.
|
|
93
|
+
#
|
|
94
|
+
# @return [void]
|
|
95
|
+
def pop_scope
|
|
96
|
+
return nil if locals.length <= 1
|
|
97
|
+
|
|
98
|
+
scope = locals.pop # @type var scope: Hash[String, Value]
|
|
99
|
+
scope.clear
|
|
100
|
+
scope_pool << scope
|
|
101
|
+
nil
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Reset environment state for reuse.
|
|
105
|
+
#
|
|
106
|
+
# @param state [State] reset state
|
|
107
|
+
# @return [Environment] self
|
|
108
|
+
def reset!(state)
|
|
109
|
+
apply_state(state)
|
|
110
|
+
reset_scopes
|
|
111
|
+
memoization.reset!
|
|
112
|
+
self
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Reset environment state for reuse without mutation semantics.
|
|
116
|
+
#
|
|
117
|
+
# @param state [State] reset state
|
|
118
|
+
# @return [Environment]
|
|
119
|
+
def reset(state)
|
|
120
|
+
reset!(state)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Reset the environment for pool reuse.
|
|
124
|
+
#
|
|
125
|
+
# @return [Environment]
|
|
126
|
+
def prepare_for_pool
|
|
127
|
+
empty_hash = {} # @type var empty_hash: Hash[untyped, untyped]
|
|
128
|
+
reset(State.new(input: empty_hash, data: empty_hash, rules: rules, builtin_registry: builtin_registry))
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Bind a local name to a value.
|
|
132
|
+
#
|
|
133
|
+
# @param name [String, Symbol] binding name
|
|
134
|
+
# @param value [Object] value to bind
|
|
135
|
+
# @return [Value] bound value
|
|
136
|
+
def bind(name, value)
|
|
137
|
+
name = name.to_s
|
|
138
|
+
raise Error, "Cannot bind reserved name: #{name}" if RESERVED_NAMES.include?(name)
|
|
139
|
+
|
|
140
|
+
value = Value.from_ruby(value)
|
|
141
|
+
locals.last[name] = value
|
|
142
|
+
value
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Lookup a binding from the current scope chain.
|
|
146
|
+
#
|
|
147
|
+
# @param name [String, Symbol] binding name
|
|
148
|
+
# @return [Value] resolved value or undefined
|
|
149
|
+
# :reek:TooManyStatements
|
|
150
|
+
def lookup(name)
|
|
151
|
+
name = name.to_s
|
|
152
|
+
reserved = RESERVED_BINDINGS[name]
|
|
153
|
+
return public_send(reserved) if reserved
|
|
154
|
+
|
|
155
|
+
locals.reverse_each do |scope|
|
|
156
|
+
return scope[name] if scope.key?(name)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
UndefinedValue.new
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Check whether a name is bound in any local scope.
|
|
163
|
+
#
|
|
164
|
+
# @param name [String, Symbol] binding name
|
|
165
|
+
# @return [Boolean]
|
|
166
|
+
def local_bound?(name)
|
|
167
|
+
name = name.to_s
|
|
168
|
+
return false if RESERVED_NAMES.include?(name)
|
|
169
|
+
|
|
170
|
+
locals.reverse_each do |scope|
|
|
171
|
+
return true if scope.key?(name)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
false
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Execute a block with additional temporary bindings.
|
|
178
|
+
#
|
|
179
|
+
# @param bindings [Hash{String, Symbol => Object}] bindings to apply
|
|
180
|
+
# @yieldreturn [Object]
|
|
181
|
+
# @return [Object] block result
|
|
182
|
+
def with_bindings(bindings)
|
|
183
|
+
push_scope
|
|
184
|
+
bindings.each { |name, value| bind(name, value) }
|
|
185
|
+
yield
|
|
186
|
+
ensure
|
|
187
|
+
pop_scope
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Execute a block with an overridden builtin registry.
|
|
191
|
+
#
|
|
192
|
+
# @param registry [Builtins::BuiltinRegistry, Builtins::BuiltinRegistryOverlay] registry to use
|
|
193
|
+
# @yieldparam environment [Environment]
|
|
194
|
+
# @return [Object] block result
|
|
195
|
+
def with_builtin_registry(registry)
|
|
196
|
+
original = @builtin_registry
|
|
197
|
+
memoization.with_context do
|
|
198
|
+
@builtin_registry = registry
|
|
199
|
+
yield self
|
|
200
|
+
end
|
|
201
|
+
ensure
|
|
202
|
+
@builtin_registry = original
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
private
|
|
206
|
+
|
|
207
|
+
attr_reader :locals, :scope_pool
|
|
208
|
+
|
|
209
|
+
def fresh_scope
|
|
210
|
+
{} # @type var scope: Hash[String, Value]
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def reset_scopes
|
|
214
|
+
locals.each(&:clear)
|
|
215
|
+
scope_pool.clear
|
|
216
|
+
base_scope = locals.first || fresh_scope
|
|
217
|
+
locals.replace([base_scope])
|
|
218
|
+
base_scope.clear
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def apply_state(state)
|
|
222
|
+
@input = Value.from_ruby(state.input)
|
|
223
|
+
@data = Value.from_ruby(state.data)
|
|
224
|
+
@rules = state.rules.dup
|
|
225
|
+
@builtin_registry = state.builtin_registry
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
# rubocop:enable Metrics/ClassLength
|
|
229
|
+
end
|
|
230
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "environment"
|
|
4
|
+
|
|
5
|
+
module Ruby
|
|
6
|
+
module Rego
|
|
7
|
+
# Pools Environment instances for reuse across evaluations.
|
|
8
|
+
# Thread-safe for concurrent checkouts/checkins.
|
|
9
|
+
class EnvironmentPool
|
|
10
|
+
# @param max_size [Integer, nil] maximum pool size for reuse (nil means unbounded)
|
|
11
|
+
# :reek:ControlParameter
|
|
12
|
+
def initialize(max_size: nil)
|
|
13
|
+
pool = [] # @type var pool: Array[Environment]
|
|
14
|
+
@pool = pool
|
|
15
|
+
@max_size = normalized_max_size(max_size)
|
|
16
|
+
@mutex = Mutex.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @param state [Environment::State]
|
|
20
|
+
# @return [Environment]
|
|
21
|
+
def checkout(state)
|
|
22
|
+
@mutex.synchronize do
|
|
23
|
+
environment = @pool.pop # @type var environment: Environment?
|
|
24
|
+
return Environment.from_state(state) unless environment
|
|
25
|
+
|
|
26
|
+
environment.reset!(state)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @param environment [Environment]
|
|
31
|
+
# @return [void]
|
|
32
|
+
def checkin(environment)
|
|
33
|
+
@mutex.synchronize do
|
|
34
|
+
return nil if max_size_full?(@pool.length)
|
|
35
|
+
|
|
36
|
+
environment.prepare_for_pool
|
|
37
|
+
@pool << environment
|
|
38
|
+
end
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
attr_reader :max_size
|
|
45
|
+
|
|
46
|
+
def max_size_full?(current_size)
|
|
47
|
+
return false if max_size.equal?(unbounded)
|
|
48
|
+
|
|
49
|
+
current_size >= max_size
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def unbounded
|
|
53
|
+
@unbounded ||= Object.new.freeze
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# :reek:NilCheck
|
|
57
|
+
def normalized_max_size(value)
|
|
58
|
+
case value
|
|
59
|
+
when nil
|
|
60
|
+
unbounded
|
|
61
|
+
when Integer
|
|
62
|
+
raise ArgumentError, "max_size must be non-negative" if value.negative?
|
|
63
|
+
|
|
64
|
+
value
|
|
65
|
+
else
|
|
66
|
+
raise ArgumentError, "max_size must be an Integer"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "errors"
|
|
4
|
+
require_relative "location"
|
|
5
|
+
|
|
6
|
+
module Ruby
|
|
7
|
+
# Ruby Rego implementation namespace.
|
|
8
|
+
module Rego
|
|
9
|
+
# Internal helper for wrapping unexpected errors in public API calls.
|
|
10
|
+
module ErrorHandling
|
|
11
|
+
# Wrap a public API call with standardized error handling.
|
|
12
|
+
#
|
|
13
|
+
# @param context [String] error context label
|
|
14
|
+
# @yieldreturn [Object]
|
|
15
|
+
# @return [Object]
|
|
16
|
+
# :reek:TooManyStatements
|
|
17
|
+
# :reek:UncommunicativeVariableName
|
|
18
|
+
def self.wrap(context)
|
|
19
|
+
yield
|
|
20
|
+
rescue Error => e
|
|
21
|
+
raise e
|
|
22
|
+
rescue StandardError => e
|
|
23
|
+
raise build_error(context, e), cause: e
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Extract a location from an error-like object.
|
|
27
|
+
#
|
|
28
|
+
# @param error [Object] error to inspect
|
|
29
|
+
# @return [Location, nil]
|
|
30
|
+
# :reek:ManualDispatch
|
|
31
|
+
# :reek:TooManyStatements
|
|
32
|
+
def self.location_from(error)
|
|
33
|
+
location = error.respond_to?(:location) ? error.location : nil
|
|
34
|
+
return location if location
|
|
35
|
+
|
|
36
|
+
line = error.respond_to?(:line) ? error.line : nil
|
|
37
|
+
column = error.respond_to?(:column) ? error.column : nil
|
|
38
|
+
|
|
39
|
+
return Location.new(line: line, column: column) if line.is_a?(Integer) && column.is_a?(Integer)
|
|
40
|
+
|
|
41
|
+
nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Build a wrapped error from an exception.
|
|
45
|
+
#
|
|
46
|
+
# @param context [String]
|
|
47
|
+
# @param error [StandardError]
|
|
48
|
+
# @return [Error]
|
|
49
|
+
def self.build_error(context, error)
|
|
50
|
+
Error.new("Rego #{context} failed: #{error.message}", location: location_from(error))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private_class_method :build_error
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private_constant :ErrorHandling
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "errors"
|
|
4
|
+
|
|
5
|
+
module Ruby
|
|
6
|
+
module Rego
|
|
7
|
+
# Normalizes error objects for structured result payloads.
|
|
8
|
+
module ErrorPayload
|
|
9
|
+
# @param error [Object]
|
|
10
|
+
# @return [Object]
|
|
11
|
+
def self.from(error)
|
|
12
|
+
return error if error.is_a?(Hash) || error.is_a?(String)
|
|
13
|
+
return error.to_h if error.is_a?(Error)
|
|
14
|
+
return standard_error_payload(error) if error.is_a?(StandardError)
|
|
15
|
+
|
|
16
|
+
error.to_s
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @param error [StandardError]
|
|
20
|
+
# @return [Hash{Symbol => Object}]
|
|
21
|
+
# :reek:ManualDispatch
|
|
22
|
+
def self.standard_error_payload(error)
|
|
23
|
+
payload = { message: error.message }
|
|
24
|
+
return payload unless error.respond_to?(:location)
|
|
25
|
+
|
|
26
|
+
location = error.location
|
|
27
|
+
payload[:location] = location.to_s if location
|
|
28
|
+
payload
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private_class_method :standard_error_payload
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "location"
|
|
4
|
+
|
|
5
|
+
module Ruby
|
|
6
|
+
# Ruby Rego implementation namespace.
|
|
7
|
+
module Rego
|
|
8
|
+
# Shared formatting helpers for error message details.
|
|
9
|
+
module ErrorFormatting
|
|
10
|
+
# Format detail key/value pairs for error messages.
|
|
11
|
+
#
|
|
12
|
+
# @param details [Hash{Symbol => Object}]
|
|
13
|
+
# @return [String]
|
|
14
|
+
def self.format_details(details)
|
|
15
|
+
details.compact.map { |key, value| "#{key}: #{value}" }.join(", ")
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private_constant :ErrorFormatting
|
|
20
|
+
|
|
21
|
+
# Base error for all Ruby::Rego exceptions.
|
|
22
|
+
class Error < StandardError
|
|
23
|
+
# @return [Location, nil]
|
|
24
|
+
attr_reader :location
|
|
25
|
+
|
|
26
|
+
# Create a new error with optional location details.
|
|
27
|
+
#
|
|
28
|
+
# @param message [String, nil] error message
|
|
29
|
+
# @param location [Location, nil] source location
|
|
30
|
+
def initialize(message = nil, location: nil)
|
|
31
|
+
@raw_message = message
|
|
32
|
+
@location = location
|
|
33
|
+
super(compose_message(message))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Serialize the error to a hash.
|
|
37
|
+
#
|
|
38
|
+
# @return [Hash{Symbol => Object}]
|
|
39
|
+
def to_h
|
|
40
|
+
{
|
|
41
|
+
message: raw_message,
|
|
42
|
+
type: self.class.name,
|
|
43
|
+
location: location&.to_s
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
attr_reader :raw_message
|
|
50
|
+
|
|
51
|
+
def compose_message(message)
|
|
52
|
+
[message, location&.then { |loc| "(#{loc})" }].compact.join(" ")
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Error raised during tokenization.
|
|
57
|
+
class LexerError < Error
|
|
58
|
+
# @return [Integer]
|
|
59
|
+
attr_reader :line
|
|
60
|
+
|
|
61
|
+
# @return [Integer]
|
|
62
|
+
attr_reader :column
|
|
63
|
+
|
|
64
|
+
# Create a lexer error.
|
|
65
|
+
#
|
|
66
|
+
# @param message [String] error message
|
|
67
|
+
# @param line [Integer] line number
|
|
68
|
+
# @param column [Integer] column number
|
|
69
|
+
# @param offset [Integer, nil] character offset
|
|
70
|
+
# @param length [Integer, nil] token length
|
|
71
|
+
def initialize(message, line:, column:, offset: nil, length: nil)
|
|
72
|
+
@line = line
|
|
73
|
+
@column = column
|
|
74
|
+
location = Location.new(line: line, column: column, offset: offset, length: length)
|
|
75
|
+
super(message, location: location)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Error raised during parsing.
|
|
80
|
+
class ParserError < Error
|
|
81
|
+
# @return [Integer]
|
|
82
|
+
attr_reader :line
|
|
83
|
+
|
|
84
|
+
# @return [Integer]
|
|
85
|
+
attr_reader :column
|
|
86
|
+
|
|
87
|
+
# @return [String, nil]
|
|
88
|
+
attr_reader :context
|
|
89
|
+
|
|
90
|
+
# Create a parser error.
|
|
91
|
+
#
|
|
92
|
+
# @param message [String] error message
|
|
93
|
+
# @param context [String, nil] token context
|
|
94
|
+
# @param location [Location] error location
|
|
95
|
+
def initialize(message, location:, context: nil)
|
|
96
|
+
@line = location.line
|
|
97
|
+
@column = location.column
|
|
98
|
+
@context = context
|
|
99
|
+
composed = context ? "#{message} (context: #{context})" : message
|
|
100
|
+
super(composed, location: location)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Build an error from a position hash or location.
|
|
104
|
+
#
|
|
105
|
+
# @param message [String] error message
|
|
106
|
+
# @param position [Hash, Location] source position
|
|
107
|
+
# @param context [String, nil] token context
|
|
108
|
+
# @return [ParserError]
|
|
109
|
+
def self.from_position(message, position:, context: nil)
|
|
110
|
+
location = Location.from(position)
|
|
111
|
+
new(message, location: location, context: context)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Error raised during module compilation.
|
|
116
|
+
class CompilationError < Error
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Error raised during evaluation.
|
|
120
|
+
class EvaluationError < Error
|
|
121
|
+
# @return [Object, nil]
|
|
122
|
+
attr_reader :rule
|
|
123
|
+
|
|
124
|
+
# Create an evaluation error.
|
|
125
|
+
#
|
|
126
|
+
# @param message [String] error message
|
|
127
|
+
# @param rule [Object, nil] rule context
|
|
128
|
+
# @param location [Location, nil] source location
|
|
129
|
+
def initialize(message, rule: nil, location: nil)
|
|
130
|
+
@rule = rule
|
|
131
|
+
details = ErrorFormatting.format_details(rule: rule)
|
|
132
|
+
composed = details.empty? ? message : "#{message} (#{details})"
|
|
133
|
+
super(composed, location: location)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Error raised for type-checking issues.
|
|
138
|
+
class TypeError < Error
|
|
139
|
+
# @return [Object, nil]
|
|
140
|
+
attr_reader :expected
|
|
141
|
+
|
|
142
|
+
# @return [Object, nil]
|
|
143
|
+
attr_reader :actual
|
|
144
|
+
|
|
145
|
+
# @return [String, nil]
|
|
146
|
+
attr_reader :context
|
|
147
|
+
|
|
148
|
+
# Create a type error.
|
|
149
|
+
#
|
|
150
|
+
# @param message [String] error message
|
|
151
|
+
# @param expected [Object, nil] expected type or value
|
|
152
|
+
# @param actual [Object, nil] actual type or value
|
|
153
|
+
# @param context [String, nil] error context
|
|
154
|
+
# @param location [Location, nil] source location
|
|
155
|
+
def initialize(message, expected: nil, actual: nil, context: nil, location: nil)
|
|
156
|
+
@expected = expected
|
|
157
|
+
@actual = actual
|
|
158
|
+
@context = context
|
|
159
|
+
details = ErrorFormatting.format_details(expected: expected, actual: actual, context: context)
|
|
160
|
+
composed = details.empty? ? message : "#{message} (#{details})"
|
|
161
|
+
super(composed, location: location)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Error raised for invalid builtin arguments.
|
|
166
|
+
class BuiltinArgumentError < TypeError
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Error raised when object keys normalize to the same value.
|
|
170
|
+
class ObjectKeyConflictError < Error
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Error raised during unification/pattern matching.
|
|
174
|
+
class UnificationError < Error
|
|
175
|
+
# @return [Object, nil]
|
|
176
|
+
attr_reader :pattern
|
|
177
|
+
|
|
178
|
+
# @return [Object, nil]
|
|
179
|
+
attr_reader :value
|
|
180
|
+
|
|
181
|
+
# Create a unification error.
|
|
182
|
+
#
|
|
183
|
+
# @param message [String] error message
|
|
184
|
+
# @param pattern [Object, nil] pattern being matched
|
|
185
|
+
# @param value [Object, nil] value being matched
|
|
186
|
+
# @param location [Location, nil] source location
|
|
187
|
+
def initialize(message, pattern: nil, value: nil, location: nil)
|
|
188
|
+
@pattern = pattern
|
|
189
|
+
@value = value
|
|
190
|
+
details = ErrorFormatting.format_details(pattern: pattern, value: value)
|
|
191
|
+
composed = details.empty? ? message : "#{message} (#{details})"
|
|
192
|
+
super(composed, location: location)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|