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