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,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Ruby
6
+ module Rego
7
+ module AST
8
+ # Represents a query literal with optional with modifiers.
9
+ class QueryLiteral < Base
10
+ # @param expression [Object]
11
+ # @param with_modifiers [Array<WithModifier>]
12
+ # @param location [Location, nil]
13
+ def initialize(expression:, with_modifiers: [], location: nil)
14
+ super(location: location)
15
+ @expression = expression
16
+ @with_modifiers = with_modifiers.dup.freeze
17
+ end
18
+
19
+ # @return [Object]
20
+ attr_reader :expression
21
+
22
+ # @return [Array<WithModifier>]
23
+ attr_reader :with_modifiers
24
+ end
25
+
26
+ # Represents a `some` declaration.
27
+ class SomeDecl < Base
28
+ # @param variables [Array<Variable>]
29
+ # @param collection [Object, nil]
30
+ # @param location [Location, nil]
31
+ def initialize(variables:, collection: nil, location: nil)
32
+ super(location: location)
33
+ @variables = variables.dup.freeze
34
+ @collection = collection
35
+ end
36
+
37
+ # @return [Array<Variable>]
38
+ attr_reader :variables
39
+
40
+ # @return [Object, nil]
41
+ attr_reader :collection
42
+ end
43
+
44
+ # Represents a `with` modifier.
45
+ class WithModifier < Base
46
+ # @param target [Object]
47
+ # @param value [Object]
48
+ # @param location [Location, nil]
49
+ def initialize(target:, value:, location: nil)
50
+ super(location: location)
51
+ @target = target
52
+ @value = value
53
+ end
54
+
55
+ # @return [Object]
56
+ attr_reader :target
57
+
58
+ # @return [Object]
59
+ attr_reader :value
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Ruby
6
+ module Rego
7
+ module AST
8
+ # Represents a reference to nested data (e.g. input.user.roles[0]).
9
+ class Reference < Base
10
+ # @param base [Object]
11
+ # @param path [Array<RefArg>]
12
+ # @param location [Location, nil]
13
+ def initialize(base:, path:, location: nil)
14
+ super(location: location)
15
+ @base = base
16
+ @path = path.dup.freeze
17
+ end
18
+
19
+ # @return [Object]
20
+ attr_reader :base
21
+
22
+ # @return [Array<RefArg>]
23
+ attr_reader :path
24
+ end
25
+
26
+ # Base class for reference path arguments.
27
+ class RefArg < Base
28
+ # @param value [Object]
29
+ # @param location [Location, nil]
30
+ def initialize(value:, location: nil)
31
+ super(location: location)
32
+ @value = value
33
+ end
34
+
35
+ # @return [Object]
36
+ attr_reader :value
37
+ end
38
+
39
+ # Dot-based reference argument (e.g. .foo).
40
+ class DotRefArg < RefArg
41
+ # @param value [Object]
42
+ # @param location [Location, nil]
43
+ def initialize(value:, location: nil)
44
+ super
45
+ end
46
+ end
47
+
48
+ # Bracket-based reference argument (e.g. [0]).
49
+ class BracketRefArg < RefArg
50
+ # @param value [Object]
51
+ # @param location [Location, nil]
52
+ def initialize(value:, location: nil)
53
+ super
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Ruby
6
+ module Rego
7
+ module AST
8
+ # Represents a rule definition.
9
+ class Rule < Base
10
+ RULE_TYPE_LOOKUP = %i[rule_type type kind].freeze
11
+
12
+ # Bundles rule components for storage.
13
+ Definition = Struct.new(:head, :body, :default_value, :else_clause, keyword_init: true)
14
+
15
+ # @param name [String]
16
+ # @param head [Object, nil]
17
+ # @param body [Object, nil]
18
+ # @param default_value [Object, nil]
19
+ # @param else_clause [Object, nil]
20
+ # @param location [Location, nil]
21
+ # :reek:LongParameterList
22
+ def initialize(name:, head: nil, body: nil, default_value: nil, else_clause: nil, location: nil) # rubocop:disable Metrics/ParameterLists
23
+ super(location: location)
24
+ @name = name
25
+ @definition = Definition.new(
26
+ head: head,
27
+ body: body,
28
+ default_value: default_value,
29
+ else_clause: else_clause
30
+ )
31
+ end
32
+
33
+ # @return [String]
34
+ attr_reader :name
35
+
36
+ # @return [Object, nil]
37
+ def head
38
+ definition.head
39
+ end
40
+
41
+ # @return [Object, nil]
42
+ def body
43
+ definition.body
44
+ end
45
+
46
+ # @return [Object, nil]
47
+ def default_value
48
+ definition.default_value
49
+ end
50
+
51
+ # @return [Object, nil]
52
+ def else_clause
53
+ definition.else_clause
54
+ end
55
+
56
+ # @return [Boolean]
57
+ def complete?
58
+ rule_type == :complete
59
+ end
60
+
61
+ # @return [Boolean]
62
+ def partial_set?
63
+ rule_type == :partial_set
64
+ end
65
+
66
+ # @return [Boolean]
67
+ def partial_object?
68
+ rule_type == :partial_object
69
+ end
70
+
71
+ # @return [Boolean]
72
+ def function?
73
+ rule_type == :function
74
+ end
75
+
76
+ private
77
+
78
+ # @return [Symbol, nil]
79
+ def rule_type
80
+ return nil unless head
81
+
82
+ type = resolve_rule_type
83
+
84
+ type.is_a?(String) ? type.to_sym : type
85
+ end
86
+
87
+ def resolve_rule_type
88
+ type_from_object || type_from_hash
89
+ end
90
+
91
+ # :reek:ManualDispatch
92
+ def type_from_object
93
+ RULE_TYPE_LOOKUP.each do |method|
94
+ return head.public_send(method) if head.respond_to?(method)
95
+ end
96
+
97
+ nil
98
+ end
99
+
100
+ def type_from_hash
101
+ return nil unless head.is_a?(Hash)
102
+
103
+ RULE_TYPE_LOOKUP.each do |key|
104
+ return head[key] if head.key?(key)
105
+ end
106
+
107
+ nil
108
+ end
109
+
110
+ attr_reader :definition
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Ruby
6
+ module Rego
7
+ module AST
8
+ # Represents a unary operation (e.g. negation).
9
+ class UnaryOp < Base
10
+ OPERATORS = %i[not minus].freeze
11
+
12
+ # @param operator [Symbol]
13
+ # @param operand [Object]
14
+ # @param location [Location, nil]
15
+ def initialize(operator:, operand:, location: nil)
16
+ @operator = operator
17
+ validate_operator!
18
+ super(location: location)
19
+ @operand = operand
20
+ end
21
+
22
+ # @return [Symbol]
23
+ attr_reader :operator
24
+
25
+ # @return [Object]
26
+ attr_reader :operand
27
+
28
+ private
29
+
30
+ def validate_operator # rubocop:disable Naming/PredicateMethod
31
+ OPERATORS.include?(@operator)
32
+ end
33
+
34
+ def validate_operator!
35
+ return if validate_operator
36
+
37
+ raise ArgumentError, "Unknown unary operator: #{@operator.inspect}"
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Ruby
6
+ module Rego
7
+ module AST
8
+ # Represents a variable identifier.
9
+ class Variable < Base
10
+ # @param name [String]
11
+ # @param location [Location, nil]
12
+ def initialize(name:, location: nil)
13
+ super(location: location)
14
+ @name = name
15
+ end
16
+
17
+ # @return [String]
18
+ attr_reader :name
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ast/base"
4
+ require_relative "ast/literal"
5
+ require_relative "ast/variable"
6
+ require_relative "ast/reference"
7
+ require_relative "ast/binary_op"
8
+ require_relative "ast/unary_op"
9
+ require_relative "ast/composite"
10
+ require_relative "ast/comprehension"
11
+ require_relative "ast/every"
12
+ require_relative "ast/query"
13
+ require_relative "ast/call"
14
+ require_relative "ast/module"
15
+ require_relative "ast/package"
16
+ require_relative "ast/import"
17
+ require_relative "ast/rule"
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require_relative "registry"
5
+
6
+ module Ruby
7
+ module Rego
8
+ module Builtins
9
+ # Built-in aggregation helpers.
10
+ module Aggregates
11
+ AGGREGATE_FUNCTIONS = {
12
+ "count" => :count,
13
+ "sum" => :sum,
14
+ "max" => :max,
15
+ "min" => :min,
16
+ "all" => :all,
17
+ "any" => :any
18
+ }.freeze
19
+
20
+ # @return [Ruby::Rego::Builtins::BuiltinRegistry]
21
+ def self.register!
22
+ registry = BuiltinRegistry.instance
23
+
24
+ AGGREGATE_FUNCTIONS.each do |name, handler|
25
+ register_function(registry, name, handler)
26
+ end
27
+
28
+ registry
29
+ end
30
+
31
+ def self.register_function(registry, name, handler)
32
+ return if registry.registered?(name)
33
+
34
+ registry.register(name, 1) { |value| public_send(handler, value) }
35
+ end
36
+ private_class_method :register_function
37
+
38
+ # @param collection [Ruby::Rego::Value]
39
+ # @return [Ruby::Rego::NumberValue]
40
+ def self.count(collection)
41
+ Base.assert_type(
42
+ collection,
43
+ expected: [ArrayValue, ObjectValue, SetValue, StringValue],
44
+ context: "count"
45
+ )
46
+
47
+ NumberValue.new(collection.value.size)
48
+ end
49
+
50
+ # @param array [Ruby::Rego::Value]
51
+ # @return [Ruby::Rego::NumberValue]
52
+ def self.sum(array)
53
+ numbers = numeric_array(array, name: "sum")
54
+ NumberValue.new(numbers.sum)
55
+ end
56
+
57
+ # @param array [Ruby::Rego::Value]
58
+ # @return [Ruby::Rego::NumberValue]
59
+ def self.max(array)
60
+ numbers = numeric_array(array, name: "max")
61
+ ensure_non_empty(numbers, name: "max")
62
+ NumberValue.new(numbers.max)
63
+ end
64
+
65
+ # @param array [Ruby::Rego::Value]
66
+ # @return [Ruby::Rego::NumberValue]
67
+ def self.min(array)
68
+ numbers = numeric_array(array, name: "min")
69
+ ensure_non_empty(numbers, name: "min")
70
+ NumberValue.new(numbers.min)
71
+ end
72
+
73
+ # @param array [Ruby::Rego::Value]
74
+ # @return [Ruby::Rego::BooleanValue]
75
+ def self.all(array)
76
+ Base.assert_type(array, expected: ArrayValue, context: "all")
77
+ BooleanValue.new(array.value.all?(&:truthy?))
78
+ end
79
+
80
+ # @param array [Ruby::Rego::Value]
81
+ # @return [Ruby::Rego::BooleanValue]
82
+ def self.any(array)
83
+ Base.assert_type(array, expected: ArrayValue, context: "any")
84
+ BooleanValue.new(array.value.any?(&:truthy?))
85
+ end
86
+
87
+ # @param array [Ruby::Rego::Value]
88
+ # @param name [String]
89
+ # @return [Array<Numeric>]
90
+ def self.numeric_array(array, name:)
91
+ Base.assert_type(array, expected: ArrayValue, context: name)
92
+
93
+ array.value.map.with_index do |element, index|
94
+ Base.assert_type(
95
+ element,
96
+ expected: NumberValue,
97
+ context: "#{name} element #{index}"
98
+ )
99
+ element.value
100
+ end
101
+ end
102
+ private_class_method :numeric_array
103
+
104
+ # @param numbers [Array<Numeric>]
105
+ # @param name [String]
106
+ # @return [void]
107
+ def self.ensure_non_empty(numbers, name:)
108
+ return unless numbers.empty?
109
+
110
+ raise Ruby::Rego::BuiltinArgumentError.new(
111
+ "Expected a non-empty array",
112
+ expected: "non-empty array",
113
+ actual: numbers.size,
114
+ context: name,
115
+ location: nil
116
+ )
117
+ end
118
+ private_class_method :ensure_non_empty
119
+ end
120
+ end
121
+ end
122
+ end
123
+
124
+ Ruby::Rego::Builtins::Aggregates.register!
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../errors"
4
+ require_relative "../value"
5
+
6
+ module Ruby
7
+ module Rego
8
+ module Builtins
9
+ # Shared helpers for built-in function implementations.
10
+ module Base
11
+ # @param args [Array<Object>]
12
+ # @param expected [Integer, Array<Integer>]
13
+ # @param name [String, nil]
14
+ # @return [void]
15
+ def self.assert_arity(args, expected, name: nil)
16
+ actual = args.size
17
+ return if arity_valid?(actual, expected)
18
+
19
+ raise_builtin_arity_error(actual, expected, name)
20
+ end
21
+
22
+ # @param value [Object]
23
+ # @param expected [Class, Array<Class>]
24
+ # @param context [String, nil]
25
+ # @return [void]
26
+ def self.assert_type(value, expected:, context: nil)
27
+ expected_classes = normalize_expected(expected)
28
+ return if expected_classes.any? { |klass| value.is_a?(klass) }
29
+
30
+ raise_type_error(
31
+ expected: expected_classes.map(&:name).join(" or "),
32
+ actual: value.class.name,
33
+ context: context
34
+ )
35
+ end
36
+
37
+ # @param value [Object]
38
+ # @return [Object]
39
+ def self.to_ruby(value)
40
+ value.is_a?(Ruby::Rego::Value) ? value.to_ruby : value
41
+ end
42
+
43
+ # @param value [Object]
44
+ # @return [Ruby::Rego::Value]
45
+ def self.to_value(value)
46
+ Ruby::Rego::Value.from_ruby(value)
47
+ end
48
+
49
+ def self.normalize_expected(expected)
50
+ expected.is_a?(Array) ? expected : [expected]
51
+ end
52
+ private_class_method :normalize_expected
53
+
54
+ def self.normalize_arity(expected)
55
+ expected.is_a?(Array) ? expected : [expected]
56
+ end
57
+ private_class_method :normalize_arity
58
+
59
+ def self.format_arity(expected_list)
60
+ expected_list.map(&:to_i).uniq.sort.join(" or ")
61
+ end
62
+ private_class_method :format_arity
63
+
64
+ def self.arity_valid?(actual, expected)
65
+ normalize_arity(expected).include?(actual)
66
+ end
67
+ private_class_method :arity_valid?
68
+
69
+ def self.raise_builtin_arity_error(actual, expected, name)
70
+ context = name ? "builtin #{name}" : nil
71
+ expected_list = normalize_arity(expected)
72
+ raise Ruby::Rego::BuiltinArgumentError.new(
73
+ "Wrong number of arguments",
74
+ expected: format_arity(expected_list),
75
+ actual: actual,
76
+ context: context,
77
+ location: nil
78
+ )
79
+ end
80
+ private_class_method :raise_builtin_arity_error
81
+
82
+ def self.raise_type_error(expected:, actual:, context: nil)
83
+ raise Ruby::Rego::BuiltinArgumentError.new(
84
+ "Type mismatch",
85
+ expected: expected,
86
+ actual: actual,
87
+ context: context,
88
+ location: nil
89
+ )
90
+ end
91
+ private_class_method :raise_type_error
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base"
4
+ require_relative "../numeric_helpers"
5
+ require_relative "../../errors"
6
+ require_relative "../../value"
7
+
8
+ module Ruby
9
+ module Rego
10
+ module Builtins
11
+ module Collections
12
+ # Array-focused collection helpers.
13
+ module ArrayOps
14
+ # @param array [Ruby::Rego::Value]
15
+ # @return [Ruby::Rego::ArrayValue]
16
+ def self.sort(array)
17
+ elements = array_values(array, name: "sort")
18
+ return ArrayValue.new([]) if elements.empty?
19
+
20
+ ensure_uniform_sort_type(elements)
21
+ sorted = elements.sort_by(&:to_ruby)
22
+ ArrayValue.new(sorted)
23
+ end
24
+
25
+ # @param left [Ruby::Rego::Value]
26
+ # @param right [Ruby::Rego::Value]
27
+ # @return [Ruby::Rego::ArrayValue]
28
+ def self.array_concat(left, right)
29
+ left_values = array_values(left, name: "array.concat left")
30
+ right_values = array_values(right, name: "array.concat right")
31
+ ArrayValue.new(left_values + right_values)
32
+ end
33
+
34
+ # @param array [Ruby::Rego::Value]
35
+ # @param start [Ruby::Rego::Value]
36
+ # @param stop [Ruby::Rego::Value]
37
+ # @return [Ruby::Rego::ArrayValue]
38
+ def self.array_slice(array, start, stop)
39
+ elements = array_values(array, name: "array.slice array")
40
+ start_index, stop_index = slice_indices(start, stop)
41
+ ArrayValue.new(slice_elements(elements, start_index, stop_index))
42
+ end
43
+
44
+ def self.array_values(value, name:)
45
+ Base.assert_type(value, expected: ArrayValue, context: name)
46
+ value.value
47
+ end
48
+ private_class_method :array_values
49
+
50
+ def self.ensure_uniform_sort_type(elements)
51
+ type = sort_type(elements)
52
+ elements.drop(1).each_with_index do |element, index|
53
+ validate_sort_element(element, type, index + 1)
54
+ end
55
+ end
56
+ private_class_method :ensure_uniform_sort_type
57
+
58
+ def self.sort_type(elements)
59
+ first = elements.first
60
+ Base.assert_type(first, expected: [NumberValue, StringValue], context: "sort element 0")
61
+ first.class
62
+ end
63
+ private_class_method :sort_type
64
+
65
+ def self.validate_sort_element(element, type, index)
66
+ Base.assert_type(element, expected: [NumberValue, StringValue], context: "sort element #{index}")
67
+ return if element.is_a?(type)
68
+
69
+ raise_sort_type_error(type, element.class)
70
+ end
71
+ private_class_method :validate_sort_element
72
+
73
+ def self.raise_sort_type_error(expected_type, actual_type)
74
+ raise Ruby::Rego::BuiltinArgumentError.new(
75
+ "Mixed types cannot be sorted",
76
+ expected: expected_type.name,
77
+ actual: actual_type.name,
78
+ context: "sort",
79
+ location: nil
80
+ )
81
+ end
82
+ private_class_method :raise_sort_type_error
83
+
84
+ def self.slice_indices(start, stop)
85
+ [
86
+ NumericHelpers.non_negative_integer(start, context: "array.slice start"),
87
+ NumericHelpers.non_negative_integer(stop, context: "array.slice stop")
88
+ ]
89
+ end
90
+ private_class_method :slice_indices
91
+
92
+ def self.slice_elements(elements, start_index, stop_index)
93
+ length = stop_index - start_index
94
+ return [] if length <= 0 || start_index >= elements.length
95
+
96
+ elements.slice(start_index, length) || []
97
+ end
98
+ private_class_method :slice_elements
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end