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,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