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,310 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ruby
|
|
4
|
+
module Rego
|
|
5
|
+
# Parsing helpers for rules and module declarations.
|
|
6
|
+
# :reek:TooManyMethods
|
|
7
|
+
# :reek:DataClump
|
|
8
|
+
# :reek:RepeatedConditional
|
|
9
|
+
# rubocop:disable Metrics/ClassLength
|
|
10
|
+
class Parser
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
# :reek:TooManyStatements
|
|
14
|
+
def parse_module
|
|
15
|
+
consume_newlines
|
|
16
|
+
package = parse_package
|
|
17
|
+
imports = [] # @type var imports: Array[AST::Import]
|
|
18
|
+
rules = [] # @type var rules: Array[AST::Rule]
|
|
19
|
+
|
|
20
|
+
consume_newlines
|
|
21
|
+
until at_end?
|
|
22
|
+
parse_statement(imports, rules)
|
|
23
|
+
consume_newlines
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
AST::Module.new(package: package, imports: imports, rules: rules, location: package.location)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# :reek:UncommunicativeVariableName
|
|
30
|
+
# :reek:TooManyStatements
|
|
31
|
+
def parse_statement(imports, rules)
|
|
32
|
+
consume_newlines
|
|
33
|
+
return if at_end?
|
|
34
|
+
|
|
35
|
+
if match?(TokenType::IMPORT)
|
|
36
|
+
imports << parse_import
|
|
37
|
+
else
|
|
38
|
+
rules << parse_rule
|
|
39
|
+
end
|
|
40
|
+
rescue ParserError => e
|
|
41
|
+
record_error(e)
|
|
42
|
+
synchronize
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def parse_package
|
|
46
|
+
keyword = consume(TokenType::PACKAGE, "Expected package declaration.")
|
|
47
|
+
path = parse_path(IdentifierContext.new(name: "package", allowed_types: PACKAGE_PATH_TOKEN_TYPES))
|
|
48
|
+
AST::Package.new(path: path, location: keyword.location)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def parse_import
|
|
52
|
+
keyword = consume(TokenType::IMPORT, "Expected import declaration.")
|
|
53
|
+
path = parse_path(IdentifierContext.new(name: "import", allowed_types: IMPORT_PATH_TOKEN_TYPES))
|
|
54
|
+
alias_name = parse_import_alias
|
|
55
|
+
|
|
56
|
+
AST::Import.new(path: path, alias_name: alias_name, location: keyword.location)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def parse_import_alias
|
|
60
|
+
return nil unless match?(TokenType::AS)
|
|
61
|
+
|
|
62
|
+
advance
|
|
63
|
+
parse_identifier(IdentifierContext.new(name: "import alias", allowed_types: PACKAGE_PATH_TOKEN_TYPES))
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# :reek:TooManyStatements
|
|
67
|
+
def parse_rule
|
|
68
|
+
default_token = consume_default_keyword
|
|
69
|
+
name_token = current_token
|
|
70
|
+
name_path = parse_rule_name_path
|
|
71
|
+
name = name_path.first
|
|
72
|
+
head = parse_rule_head(name, name_token)
|
|
73
|
+
head = apply_rule_head_path(head, name_path.drop(1), name_token)
|
|
74
|
+
head = mark_default_head(head) if default_token
|
|
75
|
+
definition = parse_rule_definition(default_token, head)
|
|
76
|
+
validate_rule_definition(default_token, head, definition)
|
|
77
|
+
|
|
78
|
+
build_rule_node(name: name, head: head, name_token: name_token, definition: definition)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def consume_default_keyword
|
|
82
|
+
match?(TokenType::DEFAULT) ? advance : nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# :reek:DuplicateMethodCall
|
|
86
|
+
# rubocop:disable Metrics/MethodLength
|
|
87
|
+
def parse_rule_name_path
|
|
88
|
+
segments = [] # @type var segments: Array[String]
|
|
89
|
+
context = IdentifierContext.new(name: "rule", allowed_types: PACKAGE_PATH_TOKEN_TYPES)
|
|
90
|
+
segments << parse_identifier(context)
|
|
91
|
+
|
|
92
|
+
loop do
|
|
93
|
+
if match?(TokenType::DOT)
|
|
94
|
+
advance
|
|
95
|
+
segments << parse_identifier(context)
|
|
96
|
+
next
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
break unless bracket_string_segment?
|
|
100
|
+
|
|
101
|
+
segment = parse_bracket_path_segment
|
|
102
|
+
break unless segment
|
|
103
|
+
|
|
104
|
+
segments << segment
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
segments
|
|
108
|
+
end
|
|
109
|
+
# rubocop:enable Metrics/MethodLength
|
|
110
|
+
|
|
111
|
+
# :reek:UtilityFunction
|
|
112
|
+
def mark_default_head(head)
|
|
113
|
+
head.merge(default: true)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# :reek:NilCheck
|
|
117
|
+
# :reek:ControlParameter
|
|
118
|
+
def parse_default_value(default_token, head)
|
|
119
|
+
return nil unless default_token
|
|
120
|
+
|
|
121
|
+
default_value = head[:value]
|
|
122
|
+
parse_error("Expected default rule value.") if default_value.nil?
|
|
123
|
+
default_value
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# :reek:ControlParameter
|
|
127
|
+
def parse_non_default_body(default_token)
|
|
128
|
+
return nil if default_token
|
|
129
|
+
return nil unless match?(TokenType::IF, TokenType::LBRACE)
|
|
130
|
+
|
|
131
|
+
parse_rule_body
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def parse_rule_definition(default_token, head)
|
|
135
|
+
default_value = parse_default_value(default_token, head)
|
|
136
|
+
body = parse_non_default_body(default_token)
|
|
137
|
+
else_clause = parse_else_clause_for_definition(default_token)
|
|
138
|
+
|
|
139
|
+
{
|
|
140
|
+
default_value: default_value,
|
|
141
|
+
body: body,
|
|
142
|
+
else_clause: else_clause
|
|
143
|
+
}
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# :reek:ControlParameter
|
|
147
|
+
def parse_else_clause_for_definition(default_token)
|
|
148
|
+
consume_newlines
|
|
149
|
+
parse_error("Default rules cannot have else clauses.") if default_token && match?(TokenType::ELSE)
|
|
150
|
+
parse_else_clause_if_present
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# :reek:FeatureEnvy
|
|
154
|
+
# :reek:ControlParameter
|
|
155
|
+
def validate_rule_definition(default_token, head, definition)
|
|
156
|
+
return if default_token
|
|
157
|
+
return unless head[:type] == :complete
|
|
158
|
+
return if head[:value] || definition[:body]
|
|
159
|
+
|
|
160
|
+
parse_error("Expected rule body or value.")
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def parse_else_clause_if_present
|
|
164
|
+
return nil unless match?(TokenType::ELSE)
|
|
165
|
+
|
|
166
|
+
parse_else_clause
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# :reek:UtilityFunction
|
|
170
|
+
# :reek:LongParameterList
|
|
171
|
+
def build_rule_node(name:, head:, name_token:, definition:)
|
|
172
|
+
AST::Rule.new(
|
|
173
|
+
name: name,
|
|
174
|
+
head: head,
|
|
175
|
+
body: definition[:body],
|
|
176
|
+
default_value: definition[:default_value],
|
|
177
|
+
else_clause: definition[:else_clause],
|
|
178
|
+
location: name_token.location
|
|
179
|
+
)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def parse_rule_head(name, name_token)
|
|
183
|
+
return parse_contains_rule_head(name, name_token) if match?(TokenType::CONTAINS)
|
|
184
|
+
return parse_function_rule_head(name, name_token) if match?(TokenType::LPAREN)
|
|
185
|
+
return parse_bracket_rule_head(name, name_token) if match?(TokenType::LBRACKET)
|
|
186
|
+
|
|
187
|
+
build_rule_head(:complete, name, name_token, value: parse_rule_value)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def parse_contains_rule_head(name, name_token)
|
|
191
|
+
advance
|
|
192
|
+
term = parse_expression
|
|
193
|
+
build_rule_head(:partial_set, name, name_token, term: term)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def parse_function_rule_head(name, name_token)
|
|
197
|
+
args = parse_rule_head_args
|
|
198
|
+
value = parse_rule_value
|
|
199
|
+
build_rule_head(:function, name, name_token, args: args, value: value)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def parse_bracket_rule_head(name, name_token)
|
|
203
|
+
key = parse_rule_head_key
|
|
204
|
+
return parse_partial_object_rule_head(name, name_token, key) if match?(TokenType::ASSIGN, TokenType::UNIFY)
|
|
205
|
+
|
|
206
|
+
build_rule_head(:partial_set, name, name_token, term: key)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def parse_partial_object_rule_head(name, name_token, key)
|
|
210
|
+
advance
|
|
211
|
+
value = parse_expression
|
|
212
|
+
build_rule_head(:partial_object, name, name_token, key: key, value: value)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# :reek:UtilityFunction
|
|
216
|
+
# :reek:LongParameterList
|
|
217
|
+
def build_rule_head(type, name, name_token, **attrs)
|
|
218
|
+
{ type: type, name: name, location: name_token.location }.merge(attrs)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def apply_rule_head_path(head, segments, name_token)
|
|
222
|
+
return head if segments.empty?
|
|
223
|
+
return nested_rule_head(head, segments, name_token) if head[:type] == :complete
|
|
224
|
+
|
|
225
|
+
parse_error("Rule head references require complete rule definitions.")
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# :reek:UtilityFunction
|
|
229
|
+
# rubocop:disable Metrics/MethodLength
|
|
230
|
+
def nested_rule_head(head, segments, name_token)
|
|
231
|
+
location = name_token.location
|
|
232
|
+
key_segment = segments.first
|
|
233
|
+
remaining = segments.drop(1)
|
|
234
|
+
value_node = head[:value] || AST::BooleanLiteral.new(value: true, location: location)
|
|
235
|
+
|
|
236
|
+
remaining.reverse_each do |segment|
|
|
237
|
+
key_node = AST::StringLiteral.new(value: segment, location: location)
|
|
238
|
+
value_node = AST::ObjectLiteral.new(pairs: [[key_node, value_node]], location: location)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
head.merge(
|
|
242
|
+
type: :partial_object,
|
|
243
|
+
key: AST::StringLiteral.new(value: key_segment, location: location),
|
|
244
|
+
value: value_node,
|
|
245
|
+
nested: remaining.any?
|
|
246
|
+
)
|
|
247
|
+
end
|
|
248
|
+
# rubocop:enable Metrics/MethodLength
|
|
249
|
+
|
|
250
|
+
# :reek:TooManyStatements
|
|
251
|
+
def parse_rule_head_args
|
|
252
|
+
parse_parenthesized_expression_list(
|
|
253
|
+
open_message: "Expected '(' after rule name.",
|
|
254
|
+
close_message: "Expected ')' after rule arguments."
|
|
255
|
+
)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# :reek:TooManyStatements
|
|
259
|
+
def parse_rule_head_key
|
|
260
|
+
consume(TokenType::LBRACKET, "Expected '[' after rule name.")
|
|
261
|
+
consume_newlines
|
|
262
|
+
key = parse_expression
|
|
263
|
+
consume_newlines
|
|
264
|
+
consume(TokenType::RBRACKET, "Expected ']' after rule key.")
|
|
265
|
+
key
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def parse_rule_value
|
|
269
|
+
return nil unless match?(TokenType::ASSIGN, TokenType::UNIFY)
|
|
270
|
+
|
|
271
|
+
advance
|
|
272
|
+
parse_expression
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def parse_rule_body
|
|
276
|
+
advance if match?(TokenType::IF)
|
|
277
|
+
return parse_braced_rule_body if match?(TokenType::LBRACE)
|
|
278
|
+
|
|
279
|
+
parse_query(TokenType::ELSE, TokenType::EOF, TokenType::NEWLINE, newline_delimiter: false)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# :reek:TooManyStatements
|
|
283
|
+
def parse_braced_rule_body
|
|
284
|
+
advance
|
|
285
|
+
consume_newlines
|
|
286
|
+
return parse_empty_rule_body if rbrace_token?
|
|
287
|
+
|
|
288
|
+
body = parse_query(TokenType::RBRACE, newline_delimiter: true)
|
|
289
|
+
consume(TokenType::RBRACE, "Expected '}' after rule body.")
|
|
290
|
+
body
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def parse_empty_rule_body
|
|
294
|
+
advance
|
|
295
|
+
[]
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def parse_else_clause
|
|
299
|
+
keyword = consume(TokenType::ELSE, "Expected 'else' clause.")
|
|
300
|
+
value = nil
|
|
301
|
+
value = parse_rule_value if match?(TokenType::ASSIGN, TokenType::UNIFY)
|
|
302
|
+
body = parse_rule_body if match?(TokenType::IF, TokenType::LBRACE)
|
|
303
|
+
else_clause = parse_else_clause_if_present
|
|
304
|
+
|
|
305
|
+
{ value: value, body: body, location: keyword.location, else_clause: else_clause }
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
# rubocop:enable Metrics/ClassLength
|
|
309
|
+
end
|
|
310
|
+
end
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "errors"
|
|
4
|
+
require_relative "token"
|
|
5
|
+
require_relative "ast"
|
|
6
|
+
require_relative "parser/precedence"
|
|
7
|
+
require_relative "parser/collections"
|
|
8
|
+
require_relative "parser/expressions"
|
|
9
|
+
require_relative "parser/query"
|
|
10
|
+
require_relative "parser/references"
|
|
11
|
+
require_relative "parser/rules"
|
|
12
|
+
|
|
13
|
+
module Ruby
|
|
14
|
+
module Rego
|
|
15
|
+
# Parses a token stream into an AST module.
|
|
16
|
+
# rubocop:disable Metrics/ClassLength
|
|
17
|
+
# :reek:TooManyMethods
|
|
18
|
+
# :reek:TooManyConstants
|
|
19
|
+
# :reek:RepeatedConditional
|
|
20
|
+
# :reek:DataClump
|
|
21
|
+
class Parser
|
|
22
|
+
IDENTIFIER_TOKEN_TYPES = [TokenType::IDENT, TokenType::DATA, TokenType::INPUT].freeze
|
|
23
|
+
IDENTIFIER_TOKEN_NAMES = {
|
|
24
|
+
TokenType::DATA => "data",
|
|
25
|
+
TokenType::INPUT => "input"
|
|
26
|
+
}.freeze
|
|
27
|
+
BINARY_OPERATOR_MAP = {
|
|
28
|
+
TokenType::ASSIGN => :assign,
|
|
29
|
+
TokenType::UNIFY => :unify,
|
|
30
|
+
TokenType::IN => :in,
|
|
31
|
+
TokenType::OR => :or,
|
|
32
|
+
TokenType::AND => :and,
|
|
33
|
+
TokenType::EQ => :eq,
|
|
34
|
+
TokenType::NEQ => :neq,
|
|
35
|
+
TokenType::LT => :lt,
|
|
36
|
+
TokenType::LTE => :lte,
|
|
37
|
+
TokenType::GT => :gt,
|
|
38
|
+
TokenType::GTE => :gte,
|
|
39
|
+
TokenType::PLUS => :plus,
|
|
40
|
+
TokenType::MINUS => :minus,
|
|
41
|
+
TokenType::STAR => :mult,
|
|
42
|
+
TokenType::SLASH => :div,
|
|
43
|
+
TokenType::PERCENT => :mod
|
|
44
|
+
}.freeze
|
|
45
|
+
UNARY_OPERATOR_MAP = {
|
|
46
|
+
TokenType::NOT => :not,
|
|
47
|
+
TokenType::MINUS => :minus
|
|
48
|
+
}.freeze
|
|
49
|
+
PRIMARY_PARSERS = {
|
|
50
|
+
TokenType::STRING => :parse_string_literal,
|
|
51
|
+
TokenType::TEMPLATE_STRING => :parse_template_string,
|
|
52
|
+
TokenType::RAW_STRING => :parse_string_literal,
|
|
53
|
+
TokenType::RAW_TEMPLATE_STRING => :parse_template_string,
|
|
54
|
+
TokenType::NUMBER => :parse_number_literal,
|
|
55
|
+
TokenType::TRUE => :parse_boolean_literal,
|
|
56
|
+
TokenType::FALSE => :parse_boolean_literal,
|
|
57
|
+
TokenType::NULL => :parse_null_literal,
|
|
58
|
+
TokenType::NOT => :parse_unary_expression,
|
|
59
|
+
TokenType::MINUS => :parse_unary_expression,
|
|
60
|
+
TokenType::EVERY => :parse_every,
|
|
61
|
+
TokenType::LPAREN => :parse_parenthesized_expression,
|
|
62
|
+
TokenType::LBRACKET => :parse_array,
|
|
63
|
+
TokenType::LBRACE => :parse_braced_literal,
|
|
64
|
+
TokenType::IDENT => :parse_identifier_expression,
|
|
65
|
+
TokenType::DATA => :parse_identifier_expression,
|
|
66
|
+
TokenType::INPUT => :parse_identifier_expression,
|
|
67
|
+
TokenType::UNDERSCORE => :parse_identifier_expression
|
|
68
|
+
}.freeze
|
|
69
|
+
PACKAGE_PATH_TOKEN_TYPES = [TokenType::IDENT].freeze
|
|
70
|
+
IMPORT_PATH_TOKEN_TYPES = IDENTIFIER_TOKEN_TYPES
|
|
71
|
+
# Bundles identifier parsing configuration for error messages and validation.
|
|
72
|
+
IdentifierContext = Struct.new(:name, :allowed_types, keyword_init: true)
|
|
73
|
+
|
|
74
|
+
# Create a parser from a token list.
|
|
75
|
+
#
|
|
76
|
+
# @param tokens [Array<Token>] token stream
|
|
77
|
+
def initialize(tokens)
|
|
78
|
+
@tokens = tokens.dup
|
|
79
|
+
@current = 0
|
|
80
|
+
@errors = [] # @type var errors: Array[ParserError]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Parse the token stream into an AST module.
|
|
84
|
+
#
|
|
85
|
+
# @return [AST::Module] parsed module
|
|
86
|
+
def parse
|
|
87
|
+
module_node = parse_module
|
|
88
|
+
raise errors.first if errors.any?
|
|
89
|
+
|
|
90
|
+
module_node
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
attr_reader :errors, :tokens
|
|
96
|
+
|
|
97
|
+
def current_token
|
|
98
|
+
safe_token_at(@current)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def peek(distance = 1)
|
|
102
|
+
safe_token_at(@current + distance)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def advance
|
|
106
|
+
previous = current_token
|
|
107
|
+
@current += 1 unless at_end?
|
|
108
|
+
previous
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# :reek:ControlParameter
|
|
112
|
+
def consume(type, message = nil)
|
|
113
|
+
return advance if match?(type)
|
|
114
|
+
|
|
115
|
+
parse_error(message || "Expected #{type} but found #{current_token.type}.")
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def match?(*types)
|
|
119
|
+
types.include?(current_token.type)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def pipe_token?
|
|
123
|
+
match?(TokenType::PIPE)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def rbrace_token?
|
|
127
|
+
match?(TokenType::RBRACE)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def newline_token?
|
|
131
|
+
match?(TokenType::NEWLINE)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def consume_newlines
|
|
135
|
+
advance while newline_token?
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def at_end?
|
|
139
|
+
current_token.type == TokenType::EOF
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def parse_error(message)
|
|
143
|
+
token = current_token
|
|
144
|
+
position = token&.location || { line: 1, column: 1 }
|
|
145
|
+
raise ParserError.from_position(message, position: position, context: token&.to_s)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def synchronize
|
|
149
|
+
advance
|
|
150
|
+
|
|
151
|
+
until at_end?
|
|
152
|
+
return if match?(TokenType::SEMICOLON, TokenType::NEWLINE)
|
|
153
|
+
return if match?(TokenType::PACKAGE, TokenType::IMPORT, TokenType::DEFAULT, TokenType::IDENT)
|
|
154
|
+
|
|
155
|
+
advance
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def record_error(error)
|
|
160
|
+
errors << error
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Shared parsing helpers that do not depend on parser state.
|
|
164
|
+
module Helpers
|
|
165
|
+
def self.precedence_of(operator)
|
|
166
|
+
Precedence::BINARY.fetch(operator, Precedence::LOWEST)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def self.normalize_reference_base(base)
|
|
170
|
+
path = [] # @type var path: Array[AST::RefArg]
|
|
171
|
+
return [base, path] unless base.is_a?(AST::Reference)
|
|
172
|
+
|
|
173
|
+
reference = base # @type var reference: AST::Reference
|
|
174
|
+
[reference.base, reference.path.dup]
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def self.variable_name_for(token)
|
|
178
|
+
token_type = token.type
|
|
179
|
+
case token_type
|
|
180
|
+
when TokenType::IDENT
|
|
181
|
+
token.value.to_s
|
|
182
|
+
when TokenType::UNDERSCORE
|
|
183
|
+
"_"
|
|
184
|
+
else
|
|
185
|
+
IDENTIFIER_TOKEN_NAMES.fetch(token_type) { token_type.to_s.downcase }
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def safe_token_at(index)
|
|
191
|
+
tokens[index] || tokens[-1]
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
class << self
|
|
195
|
+
# Parse a single Rego expression from source.
|
|
196
|
+
#
|
|
197
|
+
# @param source [String]
|
|
198
|
+
# @return [Object]
|
|
199
|
+
def parse_expression_from_string(source)
|
|
200
|
+
tokens = Lexer.new(source.to_s).tokenize
|
|
201
|
+
parser = new(tokens)
|
|
202
|
+
expression = parser.send(:parse_expression)
|
|
203
|
+
parser.send(:consume, TokenType::EOF, "Expected end of expression.")
|
|
204
|
+
expression
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
# rubocop:enable Metrics/ClassLength
|
|
209
|
+
end
|
|
210
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "compiler"
|
|
4
|
+
require_relative "evaluator"
|
|
5
|
+
require_relative "environment_pool"
|
|
6
|
+
require_relative "error_handling"
|
|
7
|
+
|
|
8
|
+
module Ruby
|
|
9
|
+
module Rego
|
|
10
|
+
# Compiled policy for reuse across evaluations.
|
|
11
|
+
class Policy
|
|
12
|
+
# Create a compiled policy from source.
|
|
13
|
+
#
|
|
14
|
+
# @param source [String] Rego source
|
|
15
|
+
# @param environment_pool [EnvironmentPool] optional pool override
|
|
16
|
+
def initialize(source, environment_pool: EnvironmentPool.new)
|
|
17
|
+
@source = source.to_s
|
|
18
|
+
@compiled_module = Ruby::Rego.compile(@source)
|
|
19
|
+
@environment_pool = environment_pool
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Evaluate the policy with the provided input and query.
|
|
23
|
+
#
|
|
24
|
+
# @param input [Object] input document
|
|
25
|
+
# @param data [Object] data document
|
|
26
|
+
# @param query [Object, nil] query path
|
|
27
|
+
# @return [Result] evaluation result
|
|
28
|
+
def evaluate(input: {}, data: {}, query: nil)
|
|
29
|
+
ErrorHandling.wrap("evaluation") { evaluate_with_pool(input: input, data: data, query: query) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
attr_reader :compiled_module, :environment_pool, :source
|
|
35
|
+
|
|
36
|
+
def evaluate_with_pool(input:, data:, query:)
|
|
37
|
+
state = Environment::State.new(
|
|
38
|
+
input: input,
|
|
39
|
+
data: data,
|
|
40
|
+
rules: compiled_module.rules_by_name,
|
|
41
|
+
builtin_registry: Builtins::BuiltinRegistry.instance
|
|
42
|
+
)
|
|
43
|
+
environment = environment_pool.checkout(state)
|
|
44
|
+
Evaluator.from_environment(compiled_module, environment).evaluate(query)
|
|
45
|
+
ensure
|
|
46
|
+
environment_pool.checkin(environment) if environment
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "error_payload"
|
|
5
|
+
require_relative "value"
|
|
6
|
+
|
|
7
|
+
module Ruby
|
|
8
|
+
module Rego
|
|
9
|
+
# Represents the outcome of evaluating a policy or expression.
|
|
10
|
+
class Result
|
|
11
|
+
# Evaluated value.
|
|
12
|
+
#
|
|
13
|
+
# @return [Value]
|
|
14
|
+
attr_reader :value
|
|
15
|
+
|
|
16
|
+
# Variable bindings captured during evaluation.
|
|
17
|
+
#
|
|
18
|
+
# @return [Hash{String => Value}]
|
|
19
|
+
attr_reader :bindings
|
|
20
|
+
|
|
21
|
+
# True when evaluation succeeded and produced a value.
|
|
22
|
+
#
|
|
23
|
+
# @return [Boolean]
|
|
24
|
+
attr_reader :success
|
|
25
|
+
|
|
26
|
+
# Errors collected during evaluation.
|
|
27
|
+
#
|
|
28
|
+
# @return [Array<Object>]
|
|
29
|
+
attr_reader :errors
|
|
30
|
+
|
|
31
|
+
# Create a result wrapper.
|
|
32
|
+
#
|
|
33
|
+
# @param value [Object] evaluation value
|
|
34
|
+
# @param success [Boolean] success flag
|
|
35
|
+
# @param bindings [Hash{String, Symbol => Object}] variable bindings
|
|
36
|
+
# @param errors [Array<Object>] collected errors
|
|
37
|
+
def initialize(value:, success:, bindings: {}, errors: [])
|
|
38
|
+
@value = Value.from_ruby(value)
|
|
39
|
+
@bindings = {} # @type var @bindings: Hash[String, Value]
|
|
40
|
+
add_bindings(bindings)
|
|
41
|
+
@success = success
|
|
42
|
+
@errors = errors.dup
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Convenience success predicate.
|
|
46
|
+
#
|
|
47
|
+
# @return [Boolean]
|
|
48
|
+
def success?
|
|
49
|
+
success
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# True when the value is undefined.
|
|
53
|
+
#
|
|
54
|
+
# @return [Boolean]
|
|
55
|
+
def undefined?
|
|
56
|
+
value.is_a?(UndefinedValue)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Convert the result to a serializable hash.
|
|
60
|
+
#
|
|
61
|
+
# @return [Hash{Symbol => Object}]
|
|
62
|
+
def to_h
|
|
63
|
+
{
|
|
64
|
+
value: value.to_ruby,
|
|
65
|
+
bindings: bindings.transform_values(&:to_ruby),
|
|
66
|
+
success: success,
|
|
67
|
+
errors: errors.map { |error| ErrorPayload.from(error) }
|
|
68
|
+
}
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Serialize the result as JSON.
|
|
72
|
+
#
|
|
73
|
+
# @param _args [Array<Object>]
|
|
74
|
+
# @return [String]
|
|
75
|
+
def to_json(*args)
|
|
76
|
+
options = args.first
|
|
77
|
+
return JSON.generate(to_h) unless options.is_a?(Hash)
|
|
78
|
+
|
|
79
|
+
JSON.generate(to_h, options)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def add_bindings(bindings)
|
|
85
|
+
bindings.each do |(name, binding_value)|
|
|
86
|
+
@bindings[name.to_s] = Value.from_ruby(binding_value)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|