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,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require_relative "../errors"
5
+ require_relative "../value"
6
+
7
+ module Ruby
8
+ module Rego
9
+ module Builtins
10
+ # Shared builtin invocation helpers.
11
+ module BuiltinInvocation
12
+ private
13
+
14
+ def normalize_name(name)
15
+ normalized = name.to_s
16
+ raise ArgumentError, "Builtin name cannot be empty" if normalized.strip.empty?
17
+
18
+ normalized
19
+ end
20
+
21
+ # :reek:FeatureEnvy
22
+ # :reek:TooManyStatements
23
+ def invoke_entry(entry, args)
24
+ entry_name = entry.name
25
+ args = ensure_array_args(args, entry_name)
26
+ Builtins::Base.assert_arity(args, entry.arity, name: entry_name)
27
+ Value.from_ruby(entry.handler.call(*args.map { |arg| Value.from_ruby(arg) }))
28
+ rescue Ruby::Rego::BuiltinArgumentError
29
+ UndefinedValue.new
30
+ end
31
+
32
+ # :reek:FeatureEnvy
33
+ def ensure_array_args(args, builtin_name)
34
+ return args if args.is_a?(Array)
35
+
36
+ raise Ruby::Rego::BuiltinArgumentError.new(
37
+ "Expected arguments to be an Array",
38
+ expected: Array,
39
+ actual: args.class,
40
+ context: "builtin #{builtin_name}",
41
+ location: nil
42
+ )
43
+ end
44
+ end
45
+
46
+ # Registry for built-in function implementations.
47
+ class BuiltinRegistry
48
+ include BuiltinInvocation
49
+
50
+ # Represents a registered built-in definition.
51
+ Entry = Struct.new(:name, :arity, :handler, keyword_init: true)
52
+
53
+ # @return [BuiltinRegistry]
54
+ def self.instance
55
+ @instance ||= new
56
+ end
57
+
58
+ private_class_method :new
59
+
60
+ def initialize
61
+ @builtins = {}
62
+ end
63
+
64
+ # @param name [String, Symbol]
65
+ # @param arity [Integer]
66
+ # @yieldparam args [Array<Ruby::Rego::Value>]
67
+ # @return [void]
68
+ def register(name, arity, &block)
69
+ raise ArgumentError, "Builtin registration requires a block" unless block
70
+
71
+ builtin_name = normalize_name(name)
72
+ validate_arity(arity)
73
+ raise ArgumentError, "Builtin already registered: #{builtin_name}" if @builtins.key?(builtin_name)
74
+
75
+ @builtins[builtin_name] = Entry.new(name: builtin_name, arity: arity, handler: block)
76
+ end
77
+
78
+ # @param name [String, Symbol]
79
+ # @param args [Array<Object>]
80
+ # @return [Ruby::Rego::Value]
81
+ def call(name, args)
82
+ entry = fetch_entry(normalize_name(name))
83
+ invoke_entry(entry, args)
84
+ end
85
+
86
+ # @param name [String, Symbol]
87
+ # @return [Entry]
88
+ def entry_for(name)
89
+ fetch_entry(normalize_name(name))
90
+ end
91
+
92
+ # @param name [String, Symbol]
93
+ # @param entry [Entry]
94
+ # @yieldreturn [Object]
95
+ # @return [Object]
96
+ def with_entry_override(name, entry)
97
+ builtin_name = normalize_name(name)
98
+ previous = fetch_entry(builtin_name)
99
+ @builtins[builtin_name] = entry
100
+ yield
101
+ ensure
102
+ @builtins[builtin_name] = previous
103
+ end
104
+
105
+ # @param name [String, Symbol]
106
+ # @param entry [Entry]
107
+ # @return [BuiltinRegistryOverlay]
108
+ def with_override(name, entry)
109
+ BuiltinRegistryOverlay.new(
110
+ base_registry: self,
111
+ overrides: { normalize_name(name) => entry }
112
+ )
113
+ end
114
+
115
+ # @param name [String, Symbol]
116
+ # @return [Boolean]
117
+ def registered?(name)
118
+ @builtins.key?(normalize_name(name))
119
+ end
120
+
121
+ private
122
+
123
+ # :reek:UtilityFunction
124
+ # :reek:FeatureEnvy
125
+ def validate_arity(arity)
126
+ return if integer_arity_valid?(arity)
127
+ return if array_arity_valid?(arity)
128
+
129
+ raise ArgumentError, "Arity must be a non-negative Integer or Array of Integers"
130
+ end
131
+
132
+ # :reek:UtilityFunction
133
+ def integer_arity_valid?(arity)
134
+ arity.is_a?(Integer) && arity >= 0
135
+ end
136
+
137
+ # :reek:UtilityFunction
138
+ def array_arity_valid?(arity)
139
+ return false unless arity.is_a?(Array)
140
+ return false if arity.empty?
141
+
142
+ arity.all? { |value| value.is_a?(Integer) && value >= 0 }
143
+ end
144
+
145
+ def fetch_entry(builtin_name)
146
+ @builtins.fetch(builtin_name) do
147
+ raise EvaluationError, "Undefined built-in function: #{builtin_name}"
148
+ end
149
+ end
150
+ end
151
+
152
+ # Registry wrapper that overlays builtin entries without mutating the base registry.
153
+ class BuiltinRegistryOverlay
154
+ include BuiltinInvocation
155
+
156
+ # @param base_registry [BuiltinRegistry]
157
+ # @param overrides [Hash{String => BuiltinRegistry::Entry}]
158
+ def initialize(base_registry:, overrides: {})
159
+ @base_registry = base_registry
160
+ @overrides = overrides
161
+ end
162
+
163
+ # @param name [String, Symbol]
164
+ # @param entry [BuiltinRegistry::Entry]
165
+ # @return [BuiltinRegistryOverlay]
166
+ def with_override(name, entry)
167
+ normalized = normalize_name(name)
168
+ self.class.new(base_registry: base_registry, overrides: overrides.merge(normalized => entry))
169
+ end
170
+
171
+ # @param name [String, Symbol]
172
+ # @param args [Array<Object>]
173
+ # @return [Ruby::Rego::Value]
174
+ def call(name, args)
175
+ entry = entry_for(name)
176
+ invoke_entry(entry, args)
177
+ end
178
+
179
+ # @param name [String, Symbol]
180
+ # @return [BuiltinRegistry::Entry]
181
+ def entry_for(name)
182
+ normalized = normalize_name(name)
183
+ overrides.fetch(normalized) { base_registry.entry_for(normalized) }
184
+ end
185
+
186
+ # @param name [String, Symbol]
187
+ # @return [Boolean]
188
+ def registered?(name)
189
+ normalized = normalize_name(name)
190
+ overrides.key?(normalized) || base_registry.registered?(normalized)
191
+ end
192
+
193
+ private
194
+
195
+ attr_reader :base_registry, :overrides
196
+ end
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruby
4
+ module Rego
5
+ module Builtins
6
+ # Shared helpers for registering builtin functions.
7
+ module RegistryHelpers
8
+ def register_configured_functions(registry, mapping)
9
+ mapping.each do |name, config|
10
+ register_configured_function(registry, name, config)
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ # :reek:FeatureEnvy
17
+ def register_configured_function(registry, name, config)
18
+ return if registry.registered?(name)
19
+
20
+ registry.register(name, config.fetch(:arity)) do |*args|
21
+ public_send(config.fetch(:handler), *args)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruby
4
+ module Rego
5
+ module Builtins
6
+ # Built-in string helpers.
7
+ module Strings
8
+ # @param string [Ruby::Rego::Value]
9
+ # @return [Ruby::Rego::StringValue]
10
+ def self.lower(string)
11
+ StringValue.new(string_value(string, context: "lower").downcase)
12
+ end
13
+
14
+ # @param string [Ruby::Rego::Value]
15
+ # @return [Ruby::Rego::StringValue]
16
+ def self.upper(string)
17
+ StringValue.new(string_value(string, context: "upper").upcase)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruby
4
+ module Rego
5
+ module Builtins
6
+ # Built-in string helpers.
7
+ module Strings
8
+ # @param delimiter [Ruby::Rego::Value]
9
+ # @param array [Ruby::Rego::Value]
10
+ # @return [Ruby::Rego::StringValue]
11
+ def self.concat(delimiter, array)
12
+ delimiter_string = string_value(delimiter, context: "concat delimiter")
13
+ parts = string_array(array, name: "concat")
14
+ StringValue.new(parts.join(delimiter_string))
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Naming/RescuedExceptionsVariableName
4
+
5
+ module Ruby
6
+ module Rego
7
+ module Builtins
8
+ # Built-in string helpers.
9
+ module Strings
10
+ # @param number [Ruby::Rego::Value]
11
+ # @param base [Ruby::Rego::Value]
12
+ # @return [Ruby::Rego::StringValue]
13
+ def self.format_int(number, base)
14
+ number_value = NumericHelpers.integer_value(number, context: "format_int number")
15
+ base_value = NumericHelpers.integer_value(base, context: "format_int base")
16
+ ensure_base(base_value)
17
+ StringValue.new(base_encode(number_value, base_value))
18
+ end
19
+
20
+ # @param format [Ruby::Rego::Value]
21
+ # @param args [Ruby::Rego::Value]
22
+ # @return [Ruby::Rego::StringValue]
23
+ def self.sprintf(format, args)
24
+ format_value = string_value(format, context: "sprintf format")
25
+ values = sprintf_values(args)
26
+ StringValue.new(Kernel.sprintf(format_value, *values))
27
+ rescue ArgumentError, ::TypeError => error
28
+ raise_sprintf_error(error)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ # rubocop:enable Naming/RescuedExceptionsVariableName
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruby
4
+ module Rego
5
+ module Builtins
6
+ # Built-in string helpers.
7
+ module Strings
8
+ BASE_DIGITS = %w[
9
+ 0 1 2 3 4 5 6 7 8 9
10
+ a b c d e f g h i j
11
+ k l m n o p q r s t
12
+ u v w x y z
13
+ ].freeze
14
+
15
+ def self.string_value(value, context:)
16
+ Base.assert_type(value, expected: StringValue, context: context)
17
+ value.value
18
+ end
19
+ private_class_method :string_value
20
+
21
+ def self.array_values(value, name:)
22
+ Base.assert_type(value, expected: ArrayValue, context: name)
23
+ value.value
24
+ end
25
+ private_class_method :array_values
26
+
27
+ def self.string_array(value, name:)
28
+ array_values(value, name: name).map.with_index do |element, index|
29
+ Base.assert_type(element, expected: StringValue, context: "#{name} element #{index}")
30
+ element.value
31
+ end
32
+ end
33
+ private_class_method :string_array
34
+
35
+ # :reek:LongParameterList
36
+ def self.string_pair(left, right, left_context:, right_context:)
37
+ [
38
+ string_value(left, context: left_context),
39
+ string_value(right, context: right_context)
40
+ ]
41
+ end
42
+ private_class_method :string_pair
43
+
44
+ def self.sprintf_values(args)
45
+ array_values(args, name: "sprintf args").map { |value| Base.to_ruby(value) }
46
+ end
47
+ private_class_method :sprintf_values
48
+
49
+ def self.raise_sprintf_error(error)
50
+ raise Ruby::Rego::BuiltinArgumentError.new(
51
+ error.message,
52
+ expected: "sprintf-compatible arguments",
53
+ actual: error.class.name,
54
+ context: "sprintf",
55
+ location: nil
56
+ )
57
+ end
58
+ private_class_method :raise_sprintf_error
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruby
4
+ module Rego
5
+ module Builtins
6
+ # Built-in string helpers.
7
+ module Strings
8
+ def self.ensure_base(base_value)
9
+ return if base_value.between?(2, 36)
10
+
11
+ raise Ruby::Rego::BuiltinArgumentError.new(
12
+ "Invalid base",
13
+ expected: "base between 2 and 36",
14
+ actual: base_value,
15
+ context: "format_int",
16
+ location: nil
17
+ )
18
+ end
19
+ private_class_method :ensure_base
20
+
21
+ def self.base_encode(number_value, base_value)
22
+ return "0" if number_value.zero?
23
+
24
+ prefix = negative_prefix(number_value)
25
+ encoded = encode_digits(number_value.abs, base_value)
26
+ "#{prefix}#{encoded}"
27
+ end
28
+ private_class_method :base_encode
29
+
30
+ def self.negative_prefix(number_value)
31
+ number_value.negative? ? "-" : ""
32
+ end
33
+ private_class_method :negative_prefix
34
+
35
+ def self.encode_digits(remaining, base_value)
36
+ digits = [] # @type var digits: Array[String]
37
+ while remaining.positive?
38
+ digits << BASE_DIGITS.fetch(remaining % base_value)
39
+ remaining /= base_value
40
+ end
41
+
42
+ digits.reverse.join
43
+ end
44
+ private_class_method :encode_digits
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruby
4
+ module Rego
5
+ module Builtins
6
+ # Built-in string helpers.
7
+ module Strings
8
+ # @param haystack [Ruby::Rego::Value]
9
+ # @param needle [Ruby::Rego::Value]
10
+ # @return [Ruby::Rego::BooleanValue]
11
+ def self.contains(haystack, needle)
12
+ haystack_text, needle_text = string_pair(
13
+ haystack,
14
+ needle,
15
+ left_context: "contains haystack",
16
+ right_context: "contains needle"
17
+ )
18
+ BooleanValue.new(haystack_text.include?(needle_text))
19
+ end
20
+
21
+ # @param string [Ruby::Rego::Value]
22
+ # @param prefix [Ruby::Rego::Value]
23
+ # @return [Ruby::Rego::BooleanValue]
24
+ def self.startswith(string, prefix)
25
+ string_text, prefix_text = string_pair(
26
+ string,
27
+ prefix,
28
+ left_context: "startswith string",
29
+ right_context: "startswith prefix"
30
+ )
31
+ BooleanValue.new(string_text.start_with?(prefix_text))
32
+ end
33
+
34
+ # @param string [Ruby::Rego::Value]
35
+ # @param suffix [Ruby::Rego::Value]
36
+ # @return [Ruby::Rego::BooleanValue]
37
+ def self.endswith(string, suffix)
38
+ string_text, suffix_text = string_pair(
39
+ string,
40
+ suffix,
41
+ left_context: "endswith string",
42
+ right_context: "endswith suffix"
43
+ )
44
+ BooleanValue.new(string_text.end_with?(suffix_text))
45
+ end
46
+
47
+ # @param haystack [Ruby::Rego::Value]
48
+ # @param needle [Ruby::Rego::Value]
49
+ # @return [Ruby::Rego::NumberValue]
50
+ def self.indexof(haystack, needle)
51
+ haystack_text, needle_text = string_pair(
52
+ haystack,
53
+ needle,
54
+ left_context: "indexof haystack",
55
+ right_context: "indexof needle"
56
+ )
57
+ index = haystack_text.index(needle_text)
58
+ NumberValue.new(index || -1)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruby
4
+ module Rego
5
+ module Builtins
6
+ # Built-in string helpers.
7
+ module Strings
8
+ # @param string [Ruby::Rego::Value]
9
+ # @param delimiter [Ruby::Rego::Value]
10
+ # @return [Ruby::Rego::ArrayValue]
11
+ def self.split(string, delimiter)
12
+ string_text = string_value(string, context: "split string")
13
+ delimiter_text = string_value(delimiter, context: "split delimiter")
14
+ ArrayValue.new(string_text.split(delimiter_text, -1))
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruby
4
+ module Rego
5
+ module Builtins
6
+ # Built-in string helpers.
7
+ module Strings
8
+ # @param string [Ruby::Rego::Value]
9
+ # @param offset [Ruby::Rego::Value]
10
+ # @param length [Ruby::Rego::Value]
11
+ # @return [Ruby::Rego::StringValue]
12
+ def self.substring(string, offset, length)
13
+ string_text = string_value(string, context: "substring string")
14
+ offset_value = NumericHelpers.non_negative_integer(offset, context: "substring offset")
15
+ length_value = NumericHelpers.non_negative_integer(length, context: "substring length")
16
+ substring = string_text.slice(offset_value, length_value) || ""
17
+ StringValue.new(substring)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruby
4
+ module Rego
5
+ module Builtins
6
+ # Built-in string helpers.
7
+ module Strings
8
+ # @param string [Ruby::Rego::Value]
9
+ # @param cutset [Ruby::Rego::Value]
10
+ # @return [Ruby::Rego::StringValue]
11
+ def self.trim(string, cutset)
12
+ trim_with_cutset(string, cutset, { mode: :both, name: "trim" })
13
+ end
14
+
15
+ # @param string [Ruby::Rego::Value]
16
+ # @param cutset [Ruby::Rego::Value]
17
+ # @return [Ruby::Rego::StringValue]
18
+ def self.trim_left(string, cutset)
19
+ trim_with_cutset(string, cutset, { mode: :left, name: "trim_left" })
20
+ end
21
+
22
+ # @param string [Ruby::Rego::Value]
23
+ # @param cutset [Ruby::Rego::Value]
24
+ # @return [Ruby::Rego::StringValue]
25
+ def self.trim_right(string, cutset)
26
+ trim_with_cutset(string, cutset, { mode: :right, name: "trim_right" })
27
+ end
28
+
29
+ # @param string [Ruby::Rego::Value]
30
+ # @return [Ruby::Rego::StringValue]
31
+ def self.trim_space(string)
32
+ StringValue.new(string_value(string, context: "trim_space").strip)
33
+ end
34
+
35
+ def self.trim_with_cutset(string, cutset, context)
36
+ StringValue.new(trimmed_text(string, cutset, context))
37
+ end
38
+ private_class_method :trim_with_cutset
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruby
4
+ module Rego
5
+ module Builtins
6
+ # Built-in string helpers.
7
+ module Strings
8
+ def self.trim_regex(cutset_text, mode:)
9
+ escaped = Regexp.escape(cutset_text)
10
+ Regexp.new(trim_patterns(escaped, mode).join("|"))
11
+ end
12
+ private_class_method :trim_regex
13
+
14
+ def self.trimmed_text(string, cutset, context)
15
+ name, mode = trim_context(context)
16
+ string_text, cutset_text = trim_inputs(string, cutset, name)
17
+ apply_trim_or_original(string_text, cutset_text, mode)
18
+ end
19
+ private_class_method :trimmed_text
20
+
21
+ def self.trim_context(context)
22
+ [context.fetch(:name), context.fetch(:mode)]
23
+ end
24
+ private_class_method :trim_context
25
+
26
+ def self.trim_inputs(string, cutset, name)
27
+ [
28
+ string_value(string, context: "#{name} string"),
29
+ string_value(cutset, context: "#{name} cutset")
30
+ ]
31
+ end
32
+ private_class_method :trim_inputs
33
+
34
+ def self.apply_trim_or_original(string_text, cutset_text, mode)
35
+ return string_text if cutset_text.empty?
36
+
37
+ apply_trim(string_text, cutset_text, mode)
38
+ end
39
+ private_class_method :apply_trim_or_original
40
+
41
+ def self.apply_trim(string_text, cutset_text, mode)
42
+ string_text.gsub(trim_regex(cutset_text, mode: mode), "")
43
+ end
44
+ private_class_method :apply_trim
45
+
46
+ def self.trim_patterns(escaped, mode)
47
+ case mode
48
+ when :left
49
+ ["\\A[#{escaped}]+"]
50
+ when :right
51
+ ["[#{escaped}]+\\z"]
52
+ when :both
53
+ ["\\A[#{escaped}]+", "[#{escaped}]+\\z"]
54
+ else
55
+ raise ArgumentError, "Unknown trim mode: #{mode}"
56
+ end
57
+ end
58
+ private_class_method :trim_patterns
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require_relative "registry"
5
+ require_relative "numeric_helpers"
6
+ require_relative "registry_helpers"
7
+ require_relative "strings/helpers"
8
+ require_relative "strings/number_helpers"
9
+ require_relative "strings/trim_helpers"
10
+ require_relative "strings/concat"
11
+ require_relative "strings/search"
12
+ require_relative "strings/case_ops"
13
+ require_relative "strings/formatting"
14
+ require_relative "strings/split"
15
+ require_relative "strings/substring"
16
+ require_relative "strings/trim"
17
+
18
+ module Ruby
19
+ module Rego
20
+ module Builtins
21
+ # Built-in string helpers.
22
+ module Strings
23
+ extend RegistryHelpers
24
+
25
+ STRING_FUNCTIONS = {
26
+ "concat" => { arity: 2, handler: :concat },
27
+ "contains" => { arity: 2, handler: :contains },
28
+ "startswith" => { arity: 2, handler: :startswith },
29
+ "endswith" => { arity: 2, handler: :endswith },
30
+ "format_int" => { arity: 2, handler: :format_int },
31
+ "indexof" => { arity: 2, handler: :indexof },
32
+ "lower" => { arity: 1, handler: :lower },
33
+ "upper" => { arity: 1, handler: :upper },
34
+ "split" => { arity: 2, handler: :split },
35
+ "sprintf" => { arity: 2, handler: :sprintf },
36
+ "substring" => { arity: 3, handler: :substring },
37
+ "trim" => { arity: 2, handler: :trim },
38
+ "trim_left" => { arity: 2, handler: :trim_left },
39
+ "trim_right" => { arity: 2, handler: :trim_right },
40
+ "trim_space" => { arity: 1, handler: :trim_space }
41
+ }.freeze
42
+
43
+ # @return [Ruby::Rego::Builtins::BuiltinRegistry]
44
+ def self.register!
45
+ registry = BuiltinRegistry.instance
46
+
47
+ register_configured_functions(registry, STRING_FUNCTIONS)
48
+
49
+ registry
50
+ end
51
+
52
+ private_class_method :register_configured_functions, :register_configured_function
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ Ruby::Rego::Builtins::Strings.register!