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,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruby
4
+ module Rego
5
+ class Evaluator
6
+ # Shared helpers for assignment/unification logic.
7
+ module AssignmentSupport
8
+ private
9
+
10
+ # :reek:TooManyStatements
11
+ def evaluate_assignment(node)
12
+ pattern = node.left
13
+ value = evaluate(node.right)
14
+ binding_sets = unifier.unify(pattern, value, environment)
15
+ return UndefinedValue.new unless binding_sets.size == 1
16
+
17
+ apply_bindings(binding_sets.first)
18
+ value
19
+ end
20
+
21
+ # :reek:TooManyStatements
22
+ def evaluate_unification(node)
23
+ binding_sets, resolved_value = unification_result(node, environment)
24
+ return UndefinedValue.new unless binding_sets.size == 1
25
+
26
+ apply_bindings(binding_sets.first)
27
+ resolved_value
28
+ end
29
+
30
+ # :reek:TooManyStatements
31
+ def unification_binding_sets(node, env)
32
+ binding_sets, = unification_result(node, env)
33
+ binding_sets
34
+ end
35
+
36
+ # :reek:TooManyStatements
37
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
38
+ def unification_result(node, env)
39
+ left_node = node.left
40
+ right_node = node.right
41
+ right_value = evaluate(right_node)
42
+ right_undefined = right_value.is_a?(UndefinedValue)
43
+ if right_undefined && left_node.is_a?(AST::Reference) && right_node.is_a?(AST::Variable)
44
+ bindings = bind_reference_variable(right_node, reference_bindings_for(left_node, env))
45
+ return [bindings, UndefinedValue.new]
46
+ end
47
+ # @type var binding_sets: Array[Hash[String, Value]]
48
+ binding_sets = []
49
+ binding_sets = unifier.unify(left_node, right_value, env) unless right_undefined
50
+ return [binding_sets, right_value] unless binding_sets.empty?
51
+
52
+ left_value = evaluate(left_node)
53
+ return [binding_sets, left_value] if left_value.is_a?(UndefinedValue)
54
+
55
+ [unifier.unify(right_node, left_value, env), left_value]
56
+ end
57
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
58
+
59
+ # :reek:TooManyStatements
60
+ # :reek:UtilityFunction
61
+ def reference_bindings_for(reference, env)
62
+ base_value = reference_base_override(reference)
63
+ unifier.reference_bindings(
64
+ reference,
65
+ env,
66
+ {},
67
+ base_value: base_value,
68
+ variable_resolver: method(:resolve_reference_variable_key)
69
+ )
70
+ end
71
+
72
+ def reference_base_override(reference)
73
+ name = reference_base_name(reference)
74
+ return nil unless name
75
+
76
+ resolve_reference_base(name)
77
+ end
78
+
79
+ def reference_base_name(reference)
80
+ case reference.base
81
+ in AST::Variable[name:]
82
+ return nil unless unresolved_reference_base?(name)
83
+
84
+ name
85
+ else
86
+ nil
87
+ end
88
+ end
89
+
90
+ def unresolved_reference_base?(name)
91
+ !environment.local_bound?(name) && environment.lookup(name).is_a?(UndefinedValue)
92
+ end
93
+
94
+ def resolve_reference_base(name)
95
+ reference_resolver.resolve_import_variable(name) ||
96
+ reference_resolver.resolve_rule_variable(name)
97
+ end
98
+
99
+ # :reek:TooManyStatements
100
+ # :reek:UtilityFunction
101
+ def bind_reference_variable(variable, reference_bindings)
102
+ reference_bindings.filter_map do |(bindings, value)|
103
+ next if value.is_a?(UndefinedValue)
104
+
105
+ name = variable.name
106
+ next bindings if name == "_"
107
+
108
+ existing = bindings[name]
109
+ next if existing && existing != value
110
+
111
+ additions = {} # @type var additions: Hash[String, Value]
112
+ additions[name] = value
113
+ bindings.merge(additions)
114
+ end
115
+ end
116
+
117
+ def apply_bindings(bindings)
118
+ bindings.each do |name, binding_value|
119
+ environment.bind(name, binding_value)
120
+ end
121
+ bindings
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruby
4
+ module Rego
5
+ class Evaluator
6
+ # Shared helpers for collection binding iteration.
7
+ # :reek:DataClump
8
+ module BindingHelpers
9
+ private
10
+
11
+ # :reek:NestedIterators
12
+ # :reek:TooManyStatements
13
+ def each_array_binding(variables, collection_value)
14
+ Enumerator.new do |yielder|
15
+ values = collection_value.to_ruby
16
+ case variables.length
17
+ when 1 then values.each { |value| yielder << bindings_for(variables[0], value) }
18
+ when 2 then values.each_with_index { |value, index| yielder << bindings_for_pair(variables, index, value) }
19
+ end
20
+ end
21
+ end
22
+
23
+ # :reek:NestedIterators
24
+ def each_set_binding(variables, collection_value)
25
+ Enumerator.new do |yielder|
26
+ collection_value.to_ruby.each do |value|
27
+ yielder << bindings_for(variables[0], value)
28
+ end
29
+ end
30
+ end
31
+
32
+ # :reek:NestedIterators
33
+ # :reek:TooManyStatements
34
+ def each_object_binding(variables, collection_value)
35
+ Enumerator.new do |yielder|
36
+ pairs = collection_value.to_ruby
37
+ case variables.length
38
+ when 1 then pairs.each_key { |key| yielder << bindings_for(variables[0], key) }
39
+ when 2 then pairs.each { |key, value| yielder << bindings_for_pair(variables, key, value) }
40
+ end
41
+ end
42
+ end
43
+
44
+ def bindings_for_pair(variables, first_value, second_value)
45
+ bindings = bindings_for(variables[0], first_value)
46
+ bindings.merge!(bindings_for(variables[1], second_value))
47
+ bindings
48
+ end
49
+
50
+ # :reek:UtilityFunction
51
+ def bindings_for(variable, value)
52
+ name = variable.name
53
+ return {} if name == "_"
54
+
55
+ { name => Value.from_ruby(value) }
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruby
4
+ module Rego
5
+ class Evaluator
6
+ # Evaluates comprehensions in isolated scopes.
7
+ # :reek:TooManyMethods
8
+ # rubocop:disable Metrics/ClassLength
9
+ class ComprehensionEvaluator
10
+ # Tracks object keys for conflict detection.
11
+ class ObjectAccumulator
12
+ def initialize
13
+ @key_sources = {} # @type var @key_sources: Hash[Object, Object]
14
+ end
15
+
16
+ # :reek:TooManyStatements
17
+ def add(values, key, value)
18
+ normalized_key = key.is_a?(Symbol) ? key.to_s : key
19
+ if key_sources.key?(normalized_key)
20
+ existing = key_sources[normalized_key]
21
+ raise ObjectKeyConflictError, "Conflicting object keys: #{existing.inspect} and #{key.inspect}"
22
+ end
23
+ key_sources[normalized_key] = key
24
+ values[normalized_key] = value
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :key_sources
30
+ end
31
+
32
+ private_constant :ObjectAccumulator
33
+
34
+ # Bundles object comprehension accumulation state.
35
+ ObjectPairContext = Struct.new(:result_values, :accumulator)
36
+ private_constant :ObjectPairContext
37
+
38
+ # @param expression_evaluator [ExpressionEvaluator]
39
+ # @param environment [Environment]
40
+ def initialize(expression_evaluator:, environment:)
41
+ @expression_evaluator = expression_evaluator
42
+ @environment = environment
43
+ @query_evaluator = nil
44
+ end
45
+
46
+ # @param query_evaluator [RuleEvaluator]
47
+ # @return [void]
48
+ def attach_query_evaluator(query_evaluator)
49
+ @query_evaluator = query_evaluator
50
+ nil
51
+ end
52
+
53
+ # @param node [AST::ArrayComprehension]
54
+ # @return [Value]
55
+ def eval_array(node)
56
+ ArrayValue.new(collect_values(node))
57
+ end
58
+
59
+ # @param node [AST::ObjectComprehension]
60
+ # @return [Value]
61
+ def eval_object(node)
62
+ ObjectValue.new(object_pairs(node))
63
+ end
64
+
65
+ # @param node [AST::SetComprehension]
66
+ # @return [Value]
67
+ def eval_set(node)
68
+ SetValue.new(collect_values(node))
69
+ end
70
+
71
+ private
72
+
73
+ attr_reader :expression_evaluator, :environment, :query_evaluator
74
+
75
+ def object_pairs(node)
76
+ values = {} # @type var values: Hash[Object, Value]
77
+ context = ObjectPairContext.new(values, ObjectAccumulator.new)
78
+ each_comprehension_binding(node.body) do |bindings|
79
+ apply_object_binding(context, node.term, bindings)
80
+ end
81
+ values
82
+ end
83
+
84
+ def apply_object_binding(context, term, bindings)
85
+ environment.with_bindings(bindings) do
86
+ pair = resolve_pair(term)
87
+ return unless pair
88
+
89
+ key, value = pair
90
+ context.accumulator.add(context.result_values, key, value)
91
+ end
92
+ end
93
+
94
+ def resolve_pair(term)
95
+ key = evaluate_defined_key(term[0])
96
+ return nil if key.is_a?(Value) && key.undefined?
97
+
98
+ value = evaluate_defined_value(term[1])
99
+ return nil unless value
100
+
101
+ [key, value]
102
+ end
103
+
104
+ def each_comprehension_binding(body, &)
105
+ with_comprehension_scope(body) { comprehension_solutions(body).each(&) }
106
+ end
107
+
108
+ def collect_values(node)
109
+ values = [] # @type var values: Array[Value]
110
+ each_comprehension_binding(node.body) do |bindings|
111
+ append_value(values, node.term, bindings)
112
+ end
113
+ values
114
+ end
115
+
116
+ def append_value(values, term, bindings)
117
+ environment.with_bindings(bindings) do
118
+ value = evaluate_defined_value(term)
119
+ values << value if value
120
+ end
121
+ end
122
+
123
+ # :reek:FeatureEnvy
124
+ def evaluate_defined_key(node)
125
+ value = expression_evaluator.evaluate(node)
126
+ return UndefinedValue.new if value.is_a?(Value) && value.undefined?
127
+
128
+ value.object_key
129
+ end
130
+
131
+ # :reek:FeatureEnvy
132
+ def evaluate_defined_value(node)
133
+ value = expression_evaluator.evaluate(node)
134
+ return nil if value.is_a?(Value) && value.undefined?
135
+
136
+ value
137
+ end
138
+
139
+ def comprehension_solutions(body)
140
+ return query_evaluator.query_solutions(body, environment) if query_evaluator
141
+
142
+ raise EvaluationError.new("Query evaluator not configured", rule: nil, location: nil)
143
+ end
144
+
145
+ def with_comprehension_scope(body)
146
+ environment.push_scope
147
+ shadow_comprehension_locals(body)
148
+ yield
149
+ ensure
150
+ environment.pop_scope
151
+ end
152
+
153
+ def shadow_comprehension_locals(body)
154
+ details = BoundVariableCollector.new.collect_details(body)
155
+ explicit = details[:explicit]
156
+ shadow_explicit_locals(explicit)
157
+ shadow_unification_locals(details[:unification], explicit)
158
+ end
159
+
160
+ def shadow_explicit_locals(names)
161
+ names.each { |name| bind_undefined(name) }
162
+ end
163
+
164
+ def shadow_unification_locals(names, explicit_names)
165
+ names.each do |name|
166
+ next if explicit_names.include?(name)
167
+ next unless environment.lookup(name).is_a?(UndefinedValue)
168
+
169
+ bind_undefined(name)
170
+ end
171
+ end
172
+
173
+ def bind_undefined(name)
174
+ return if Environment::RESERVED_NAMES.include?(name) || name == "_"
175
+
176
+ environment.bind(name, UndefinedValue.new)
177
+ end
178
+ end
179
+ # rubocop:enable Metrics/ClassLength
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruby
4
+ module Rego
5
+ class Evaluator
6
+ # Dispatches expression evaluation for primitive and AST nodes.
7
+ class ExpressionDispatch
8
+ # @param primitive_types [Array<Class>]
9
+ # @param node_evaluators [Array<Array<Class, Proc>>]
10
+ def initialize(primitive_types:, node_evaluators:)
11
+ @primitive_types = primitive_types
12
+ @node_evaluators = node_evaluators
13
+ @handler_cache = {} # @type var handler_cache: Hash[Class, Proc?]
14
+ end
15
+
16
+ # @param node [Object]
17
+ # @return [Value, nil]
18
+ def primitive_value(node)
19
+ return Value.from_ruby(node) if primitive_types.any? { |klass| node.is_a?(klass) }
20
+
21
+ nil
22
+ end
23
+
24
+ # @param node [Object]
25
+ # @param evaluator [ExpressionEvaluator]
26
+ # @return [Value, nil]
27
+ def dispatch_node(node, evaluator)
28
+ handler = handler_for(node)
29
+ handler&.call(node, evaluator)
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :primitive_types, :node_evaluators, :handler_cache
35
+
36
+ def handler_for(node)
37
+ node_class = node.class
38
+ return handler_cache[node_class] if handler_cache.key?(node_class)
39
+
40
+ handler_cache[node_class] = node_evaluators.find { |klass, _| node.is_a?(klass) }&.last
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end