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,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../ast"
4
+ require_relative "../errors"
5
+ require_relative "../value"
6
+ require_relative "with_modifier_builtin_override"
7
+ require_relative "with_modifier_path_key_resolver"
8
+ require_relative "with_modifier_path_override"
9
+ require_relative "with_modifier_root_scope"
10
+
11
+ module Ruby
12
+ module Rego
13
+ module WithModifiers
14
+ # Applies a single with modifier in a given environment.
15
+ class WithModifierContext
16
+ # @param modifier [WithModifier]
17
+ # @param environment [Environment]
18
+ # @param expression_evaluator [Evaluator::ExpressionEvaluator]
19
+ def initialize(modifier:, environment:, expression_evaluator:)
20
+ @modifier = modifier
21
+ @environment = environment
22
+ @expression_evaluator = expression_evaluator
23
+ end
24
+
25
+ # @yieldparam environment [Environment]
26
+ # @return [Object]
27
+ def apply(&)
28
+ return apply_reference(&) if target.is_a?(AST::Reference)
29
+ return apply_variable(&) if target.is_a?(AST::Variable)
30
+
31
+ raise EvaluationError.new("Unsupported with target: #{target.class}", rule: nil, location: target.location)
32
+ end
33
+
34
+ private
35
+
36
+ attr_reader :modifier, :environment, :expression_evaluator
37
+
38
+ def target
39
+ modifier.target
40
+ end
41
+
42
+ def value
43
+ modifier.value
44
+ end
45
+
46
+ def apply_reference(&)
47
+ scope = reference_scope
48
+ overridden = reference_override(scope)
49
+ return overridden if overridden.is_a?(UndefinedValue)
50
+
51
+ scope.with_override(overridden, &)
52
+ end
53
+
54
+ def reference_scope
55
+ base = target.base
56
+ location = target.location
57
+ unless base.is_a?(AST::Variable)
58
+ raise EvaluationError.new("Unsupported with target base: #{base.class}", rule: nil, location: location)
59
+ end
60
+
61
+ WithModifierRootScope.new(
62
+ environment: environment,
63
+ name: base.name,
64
+ location: location
65
+ )
66
+ end
67
+
68
+ def reference_override(scope)
69
+ keys = reference_path_keys
70
+ return UndefinedValue.new if keys.is_a?(UndefinedValue)
71
+
72
+ replacement = resolved_value
73
+ WithModifierPathOverride.new(
74
+ base_value: scope.base_value,
75
+ keys: keys,
76
+ replacement: replacement,
77
+ location: target.location
78
+ ).apply
79
+ end
80
+
81
+ # :reek:TooManyStatements
82
+ # Returns UndefinedValue when any path key is undefined to short-circuit modifier application.
83
+ def reference_path_keys
84
+ resolver = WithModifierPathKeyResolver.new(expression_evaluator: expression_evaluator)
85
+ keys = target.path.map { |segment| resolver.resolve(segment) }
86
+ undefined_key = keys.find { |key| key.is_a?(UndefinedValue) }
87
+ undefined_key || keys
88
+ end
89
+
90
+ def apply_variable(&)
91
+ name = target.name
92
+ if WithModifierRootScope::ROOT_NAMES.include?(name)
93
+ scope = WithModifierRootScope.new(
94
+ environment: environment,
95
+ name: name,
96
+ location: target.location
97
+ )
98
+ return scope.with_override(resolved_value, &)
99
+ end
100
+
101
+ apply_builtin_override(name, &)
102
+ end
103
+
104
+ def apply_builtin_override(name, &)
105
+ WithModifierBuiltinOverride.new(
106
+ name: name,
107
+ value: value,
108
+ expression_evaluator: expression_evaluator,
109
+ location: target.location
110
+ ).apply(environment, &)
111
+ end
112
+
113
+ def resolved_value
114
+ resolved = expression_evaluator.evaluate(value)
115
+ resolved.is_a?(Value) ? resolved.to_ruby : resolved
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../ast"
4
+ require_relative "../value"
5
+
6
+ module Ruby
7
+ module Rego
8
+ module WithModifiers
9
+ # Resolves reference path segments for with modifiers.
10
+ class WithModifierPathKeyResolver
11
+ # @param expression_evaluator [Evaluator::ExpressionEvaluator]
12
+ def initialize(expression_evaluator:)
13
+ @expression_evaluator = expression_evaluator
14
+ end
15
+
16
+ # @param segment [Object]
17
+ # @return [Object]
18
+ # :reek:FeatureEnvy
19
+ # :reek:TooManyStatements
20
+ def resolve(segment)
21
+ raw = segment.is_a?(AST::RefArg) ? segment.value : segment
22
+ key = resolved_key(raw)
23
+ return key if key.is_a?(UndefinedValue)
24
+
25
+ key.is_a?(Symbol) ? key.to_s : key
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :expression_evaluator
31
+
32
+ # :reek:FeatureEnvy
33
+ def resolved_key(raw)
34
+ resolved = expression_evaluator.evaluate(raw)
35
+ return resolved if resolved.is_a?(UndefinedValue)
36
+
37
+ resolved.is_a?(Value) ? resolved.to_ruby : resolved
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../errors"
4
+
5
+ module Ruby
6
+ module Rego
7
+ module WithModifiers
8
+ # Applies a path-based override to a Ruby object.
9
+ # :reek:DataClump
10
+ # :reek:FeatureEnvy
11
+ # :reek:TooManyStatements
12
+ # :reek:DuplicateMethodCall
13
+ class WithModifierPathOverride
14
+ # @param base_value [Object]
15
+ # @param keys [Array<Object>]
16
+ # @param replacement [Object]
17
+ # @param location [Location, nil]
18
+ def initialize(base_value:, keys:, replacement:, location: nil)
19
+ @base_value = base_value
20
+ @keys = keys
21
+ @replacement = replacement
22
+ @location = location
23
+ end
24
+
25
+ # @return [Object]
26
+ def apply
27
+ apply_to(base_value, keys)
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :base_value, :keys, :replacement, :location
33
+
34
+ def apply_to(container, path_keys)
35
+ return replacement if path_keys.empty?
36
+
37
+ return apply_hash(container, path_keys) if container.is_a?(Hash)
38
+ return apply_array(container, path_keys) if container.is_a?(Array)
39
+
40
+ apply_missing_container(path_keys)
41
+ end
42
+
43
+ def apply_hash(container, path_keys)
44
+ key = path_keys.first
45
+ rest = path_keys.drop(1)
46
+ updated = container.dup
47
+ normalized_key = key.is_a?(Symbol) ? key.to_s : key
48
+ updated[normalized_key] = apply_to(container[normalized_key], rest)
49
+ updated
50
+ end
51
+
52
+ def apply_array(container, path_keys)
53
+ key = path_keys.first
54
+ rest = path_keys.drop(1)
55
+ index = array_index(key)
56
+ updated = container.dup
57
+ updated[index] = apply_to(container[index], rest)
58
+ updated
59
+ end
60
+
61
+ def array_index(key)
62
+ return key if key.is_a?(Integer)
63
+ return Integer(key, 10) if numeric_key?(key)
64
+
65
+ error = invalid_index_error(key)
66
+ raise error
67
+ rescue ArgumentError
68
+ error = invalid_index_error(key)
69
+ raise error
70
+ end
71
+
72
+ def apply_missing_container(path_keys)
73
+ key = path_keys.first
74
+ rest = path_keys.drop(1)
75
+ # @type var replacement_container: Array[Object] | Hash[Object, Object]
76
+ replacement_container = array_index_key?(key) ? [] : {}
77
+ apply_to(replacement_container, [key] + rest)
78
+ end
79
+
80
+ # :reek:UtilityFunction
81
+ def array_index_key?(key)
82
+ numeric_key?(key)
83
+ end
84
+
85
+ # :reek:UtilityFunction
86
+ def numeric_key?(key)
87
+ return true if key.is_a?(Integer)
88
+
89
+ key.is_a?(String) && key.match?(/\A-?\d+\z/)
90
+ end
91
+
92
+ def invalid_index_error(key)
93
+ message = "Invalid array index for with modifier: #{key.inspect}"
94
+ EvaluationError.new(message, rule: nil, location: location)
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../errors"
4
+
5
+ module Ruby
6
+ module Rego
7
+ module WithModifiers
8
+ # Encapsulates input/data root access for with modifiers.
9
+ class WithModifierRootScope
10
+ ROOT_NAMES = %w[input data].freeze
11
+
12
+ # @param environment [Environment]
13
+ # @param name [String]
14
+ # @param location [Location, nil]
15
+ def initialize(environment:, name:, location: nil)
16
+ @environment = environment
17
+ @name = name
18
+ @location = location
19
+ validate_name
20
+ end
21
+
22
+ # @return [Object]
23
+ def base_value
24
+ input_scope? ? environment.input.to_ruby : environment.data.to_ruby
25
+ end
26
+
27
+ # @param overridden [Object]
28
+ # @yieldparam environment [Environment]
29
+ # @return [Object]
30
+ def with_override(overridden, &)
31
+ if input_scope?
32
+ environment.with_overrides(input: overridden, &)
33
+ else
34
+ environment.with_overrides(data: overridden, &)
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ attr_reader :environment, :name, :location
41
+
42
+ def input_scope?
43
+ name == "input"
44
+ end
45
+
46
+ def validate_name
47
+ return if ROOT_NAMES.include?(name)
48
+
49
+ raise EvaluationError.new(
50
+ "With modifier expects input or data reference",
51
+ rule: nil,
52
+ location: location
53
+ )
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
data/lib/ruby/rego.rb ADDED
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rego/version"
4
+ require_relative "rego/location"
5
+ require_relative "rego/errors"
6
+ require_relative "rego/error_handling"
7
+ require_relative "rego/token"
8
+ require_relative "rego/lexer"
9
+ require_relative "rego/ast"
10
+ require_relative "rego/call_name"
11
+ require_relative "rego/parser"
12
+ require_relative "rego/value"
13
+ require_relative "rego/builtins/base"
14
+ require_relative "rego/builtins/registry"
15
+ require_relative "rego/builtins/types"
16
+ require_relative "rego/builtins/aggregates"
17
+ require_relative "rego/builtins/strings"
18
+ require_relative "rego/builtins/collections"
19
+ require_relative "rego/builtins/comparisons"
20
+ require_relative "rego/memoization"
21
+ require_relative "rego/environment"
22
+ require_relative "rego/environment_pool"
23
+ require_relative "rego/compiled_module"
24
+ require_relative "rego/compiler"
25
+ require_relative "rego/with_modifiers/with_modifier"
26
+ require_relative "rego/unifier"
27
+ require_relative "rego/result"
28
+ require_relative "rego/evaluator"
29
+ require_relative "rego/policy"
30
+
31
+ module Ruby
32
+ # Top-level namespace for the Ruby Rego gem.
33
+ module Rego
34
+ class << self
35
+ # Parse Rego source into an AST module.
36
+ #
37
+ # @param source [String] Rego source code
38
+ # @return [AST::Module] parsed AST module
39
+ def parse(source)
40
+ ErrorHandling.wrap("parsing") do
41
+ tokens = Lexer.new(source).tokenize
42
+ Parser.new(tokens).parse
43
+ end
44
+ end
45
+
46
+ # Compile Rego source into an immutable compiled module.
47
+ #
48
+ # @param source [String] Rego source code
49
+ # @return [CompiledModule] compiled and indexed module
50
+ def compile(source)
51
+ ErrorHandling.wrap("compilation") do
52
+ Compiler.new.compile(parse(source))
53
+ end
54
+ end
55
+
56
+ # Evaluate Rego source against input and data.
57
+ #
58
+ # @param source [String] Rego source code
59
+ # @param input [Object] input document
60
+ # @param data [Object] data document
61
+ # @param query [Object, nil] optional query path
62
+ # @return [Result] evaluation result
63
+ # :reek:LongParameterList
64
+ def evaluate(source, input: {}, data: {}, query: nil)
65
+ compiled_module = compile(source)
66
+ ErrorHandling.wrap("evaluation") do
67
+ Evaluator.new(compiled_module, input: input, data: data).evaluate(query)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
data/sig/objspace.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module ObjectSpace
2
+ def self.memsize_of_all: () -> Integer
3
+ def self.count_objects: () -> Hash[Symbol, Integer]
4
+ end
data/sig/psych.rbs ADDED
@@ -0,0 +1,7 @@
1
+ module Psych
2
+ class BadAlias < StandardError
3
+ end
4
+
5
+ class SyntaxError < StandardError
6
+ end
7
+ end