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.
- checksums.yaml +7 -0
- data/.reek.yml +80 -0
- data/.vscode/extensions.json +19 -0
- data/.vscode/launch.json +35 -0
- data/.vscode/settings.json +25 -0
- data/.vscode/tasks.json +117 -0
- data/.yardopts +12 -0
- data/ARCHITECTURE.md +39 -0
- data/CHANGELOG.md +25 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +183 -0
- data/RELEASING.md +37 -0
- data/Rakefile +38 -0
- data/SECURITY.md +26 -0
- data/Steepfile +10 -0
- data/TODO.md +35 -0
- data/benchmark/builtin_calls.rb +29 -0
- data/benchmark/complex_policy.rb +19 -0
- data/benchmark/comprehensions.rb +19 -0
- data/benchmark/simple_rules.rb +20 -0
- data/examples/README.md +27 -0
- data/examples/sample_config.yaml +2 -0
- data/examples/simple_policy.rego +7 -0
- data/examples/validation_policy.rego +11 -0
- data/exe/rego-validate +6 -0
- data/lib/ruby/rego/ast/base.rb +95 -0
- data/lib/ruby/rego/ast/binary_op.rb +64 -0
- data/lib/ruby/rego/ast/call.rb +27 -0
- data/lib/ruby/rego/ast/composite.rb +48 -0
- data/lib/ruby/rego/ast/comprehension.rb +63 -0
- data/lib/ruby/rego/ast/every.rb +37 -0
- data/lib/ruby/rego/ast/import.rb +32 -0
- data/lib/ruby/rego/ast/literal.rb +70 -0
- data/lib/ruby/rego/ast/module.rb +32 -0
- data/lib/ruby/rego/ast/package.rb +22 -0
- data/lib/ruby/rego/ast/query.rb +63 -0
- data/lib/ruby/rego/ast/reference.rb +58 -0
- data/lib/ruby/rego/ast/rule.rb +114 -0
- data/lib/ruby/rego/ast/unary_op.rb +42 -0
- data/lib/ruby/rego/ast/variable.rb +22 -0
- data/lib/ruby/rego/ast.rb +17 -0
- data/lib/ruby/rego/builtins/aggregates.rb +124 -0
- data/lib/ruby/rego/builtins/base.rb +95 -0
- data/lib/ruby/rego/builtins/collections/array_ops.rb +103 -0
- data/lib/ruby/rego/builtins/collections/object_ops.rb +120 -0
- data/lib/ruby/rego/builtins/collections/set_ops.rb +51 -0
- data/lib/ruby/rego/builtins/collections.rb +137 -0
- data/lib/ruby/rego/builtins/comparisons/casts.rb +139 -0
- data/lib/ruby/rego/builtins/comparisons.rb +84 -0
- data/lib/ruby/rego/builtins/numeric_helpers.rb +56 -0
- data/lib/ruby/rego/builtins/registry.rb +199 -0
- data/lib/ruby/rego/builtins/registry_helpers.rb +27 -0
- data/lib/ruby/rego/builtins/strings/case_ops.rb +22 -0
- data/lib/ruby/rego/builtins/strings/concat.rb +19 -0
- data/lib/ruby/rego/builtins/strings/formatting.rb +35 -0
- data/lib/ruby/rego/builtins/strings/helpers.rb +62 -0
- data/lib/ruby/rego/builtins/strings/number_helpers.rb +48 -0
- data/lib/ruby/rego/builtins/strings/search.rb +63 -0
- data/lib/ruby/rego/builtins/strings/split.rb +19 -0
- data/lib/ruby/rego/builtins/strings/substring.rb +22 -0
- data/lib/ruby/rego/builtins/strings/trim.rb +42 -0
- data/lib/ruby/rego/builtins/strings/trim_helpers.rb +62 -0
- data/lib/ruby/rego/builtins/strings.rb +58 -0
- data/lib/ruby/rego/builtins/types.rb +89 -0
- data/lib/ruby/rego/call_name.rb +55 -0
- data/lib/ruby/rego/cli.rb +1122 -0
- data/lib/ruby/rego/compiled_module.rb +114 -0
- data/lib/ruby/rego/compiler.rb +1097 -0
- data/lib/ruby/rego/environment/overrides.rb +33 -0
- data/lib/ruby/rego/environment/reference_resolution.rb +86 -0
- data/lib/ruby/rego/environment.rb +230 -0
- data/lib/ruby/rego/environment_pool.rb +71 -0
- data/lib/ruby/rego/error_handling.rb +58 -0
- data/lib/ruby/rego/error_payload.rb +34 -0
- data/lib/ruby/rego/errors.rb +196 -0
- data/lib/ruby/rego/evaluator/assignment_support.rb +126 -0
- data/lib/ruby/rego/evaluator/binding_helpers.rb +60 -0
- data/lib/ruby/rego/evaluator/comprehension_evaluator.rb +182 -0
- data/lib/ruby/rego/evaluator/expression_dispatch.rb +45 -0
- data/lib/ruby/rego/evaluator/expression_evaluator.rb +492 -0
- data/lib/ruby/rego/evaluator/object_literal_evaluator.rb +52 -0
- data/lib/ruby/rego/evaluator/operator_evaluator.rb +163 -0
- data/lib/ruby/rego/evaluator/query_node_builder.rb +38 -0
- data/lib/ruby/rego/evaluator/reference_key_resolver.rb +50 -0
- data/lib/ruby/rego/evaluator/reference_resolver.rb +352 -0
- data/lib/ruby/rego/evaluator/rule_evaluator/bindings.rb +70 -0
- data/lib/ruby/rego/evaluator/rule_evaluator.rb +550 -0
- data/lib/ruby/rego/evaluator/rule_value_provider.rb +56 -0
- data/lib/ruby/rego/evaluator/variable_collector.rb +221 -0
- data/lib/ruby/rego/evaluator.rb +174 -0
- data/lib/ruby/rego/lexer/number_reader.rb +68 -0
- data/lib/ruby/rego/lexer/stream.rb +137 -0
- data/lib/ruby/rego/lexer/string_reader.rb +90 -0
- data/lib/ruby/rego/lexer/template_string_reader.rb +62 -0
- data/lib/ruby/rego/lexer.rb +206 -0
- data/lib/ruby/rego/location.rb +73 -0
- data/lib/ruby/rego/memoization.rb +67 -0
- data/lib/ruby/rego/parser/collections.rb +173 -0
- data/lib/ruby/rego/parser/expressions.rb +216 -0
- data/lib/ruby/rego/parser/precedence.rb +42 -0
- data/lib/ruby/rego/parser/query.rb +139 -0
- data/lib/ruby/rego/parser/references.rb +115 -0
- data/lib/ruby/rego/parser/rules.rb +310 -0
- data/lib/ruby/rego/parser.rb +210 -0
- data/lib/ruby/rego/policy.rb +50 -0
- data/lib/ruby/rego/result.rb +91 -0
- data/lib/ruby/rego/token.rb +206 -0
- data/lib/ruby/rego/unifier.rb +451 -0
- data/lib/ruby/rego/value.rb +379 -0
- data/lib/ruby/rego/version.rb +7 -0
- data/lib/ruby/rego/with_modifiers/with_modifier.rb +37 -0
- data/lib/ruby/rego/with_modifiers/with_modifier_applier.rb +48 -0
- data/lib/ruby/rego/with_modifiers/with_modifier_builtin_override.rb +128 -0
- data/lib/ruby/rego/with_modifiers/with_modifier_context.rb +120 -0
- data/lib/ruby/rego/with_modifiers/with_modifier_path_key_resolver.rb +42 -0
- data/lib/ruby/rego/with_modifiers/with_modifier_path_override.rb +99 -0
- data/lib/ruby/rego/with_modifiers/with_modifier_root_scope.rb +58 -0
- data/lib/ruby/rego.rb +72 -0
- data/sig/objspace.rbs +4 -0
- data/sig/psych.rbs +7 -0
- data/sig/rego_validate.rbs +382 -0
- data/sig/ruby/rego.rbs +2150 -0
- 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!
|