Module: Ruby::Rego::Builtins::Strings

Extended by:
RegistryHelpers
Defined in:
lib/ruby/rego/builtins/strings.rb,
lib/ruby/rego/builtins/strings/trim.rb,
lib/ruby/rego/builtins/strings/split.rb,
lib/ruby/rego/builtins/strings/concat.rb,
lib/ruby/rego/builtins/strings/search.rb,
lib/ruby/rego/builtins/strings/helpers.rb,
lib/ruby/rego/builtins/strings/case_ops.rb,
lib/ruby/rego/builtins/strings/matching.rb,
lib/ruby/rego/builtins/strings/substring.rb,
lib/ruby/rego/builtins/strings/transform.rb,
lib/ruby/rego/builtins/strings/formatting.rb,
lib/ruby/rego/builtins/strings/trim_helpers.rb,
lib/ruby/rego/builtins/strings/number_helpers.rb

Overview

Built-in string helpers.

Constant Summary collapse

STRING_FUNCTIONS =
{
  "concat" => { arity: 2, handler: :concat },
  "contains" => { arity: 2, handler: :contains },
  "startswith" => { arity: 2, handler: :startswith },
  "endswith" => { arity: 2, handler: :endswith },
  "format_int" => { arity: 2, handler: :format_int },
  "indexof" => { arity: 2, handler: :indexof },
  "lower" => { arity: 1, handler: :lower },
  "upper" => { arity: 1, handler: :upper },
  "split" => { arity: 2, handler: :split },
  "sprintf" => { arity: 2, handler: :sprintf },
  "substring" => { arity: 3, handler: :substring },
  "trim" => { arity: 2, handler: :trim },
  "trim_left" => { arity: 2, handler: :trim_left },
  "trim_right" => { arity: 2, handler: :trim_right },
  "trim_space" => { arity: 1, handler: :trim_space },
  "trim_prefix" => { arity: 2, handler: :trim_prefix },
  "trim_suffix" => { arity: 2, handler: :trim_suffix },
  "replace" => { arity: 3, handler: :replace },
  "strings.reverse" => { arity: 1, handler: :strings_reverse },
  "strings.count" => { arity: 2, handler: :strings_count },
  "indexof_n" => { arity: 2, handler: :indexof_n },
  "strings.replace_n" => { arity: 2, handler: :replace_n },
  "strings.any_prefix_match" => { arity: 2, handler: :any_prefix_match },
  "strings.any_suffix_match" => { arity: 2, handler: :any_suffix_match }
}.freeze
BASE_DIGITS =
%w[
  0 1 2 3 4 5 6 7 8 9
  a b c d e f g h i j
  k l m n o p q r s t
  u v w x y z
].freeze

Class Method Summary collapse

Methods included from RegistryHelpers

register_configured_functions

Class Method Details

.any_prefix_match(search, base) ⇒ Ruby::Rego::BooleanValue

True if any string in search starts with any string in base, matching OPA’s strings.any_prefix_match. Each argument may be a string, array, or set of strings; an empty collection yields false, and an empty base string is a prefix of everything.

Parameters:

Returns:



16
17
18
19
20
# File 'lib/ruby/rego/builtins/strings/matching.rb', line 16

def self.any_prefix_match(search, base)
  searches = string_collection(search, name: "strings.any_prefix_match search")
  bases = string_collection(base, name: "strings.any_prefix_match base")
  BooleanValue.new(any_pair_match?(searches, bases) { |candidate, affix| candidate.start_with?(affix) })
end

.any_suffix_match(search, base) ⇒ Ruby::Rego::BooleanValue

True if any string in search ends with any string in base, matching OPA’s strings.any_suffix_match. Argument typing mirrors any_prefix_match.

Parameters:

Returns:



28
29
30
31
32
# File 'lib/ruby/rego/builtins/strings/matching.rb', line 28

def self.any_suffix_match(search, base)
  searches = string_collection(search, name: "strings.any_suffix_match search")
  bases = string_collection(base, name: "strings.any_suffix_match base")
  BooleanValue.new(any_pair_match?(searches, bases) { |candidate, affix| candidate.end_with?(affix) })
end

.concat(delimiter, array) ⇒ Ruby::Rego::StringValue

Parameters:

Returns:



11
12
13
14
15
# File 'lib/ruby/rego/builtins/strings/concat.rb', line 11

def self.concat(delimiter, array)
  delimiter_string = string_value(delimiter, context: "concat delimiter")
  parts = string_array(array, name: "concat")
  StringValue.new(parts.join(delimiter_string))
end

.contains(haystack, needle) ⇒ Ruby::Rego::BooleanValue

Parameters:

Returns:



11
12
13
14
15
16
17
18
19
# File 'lib/ruby/rego/builtins/strings/search.rb', line 11

def self.contains(haystack, needle)
  haystack_text, needle_text = string_pair(
    haystack,
    needle,
    left_context: "contains haystack",
    right_context: "contains needle"
  )
  BooleanValue.new(haystack_text.include?(needle_text))
end

.endswith(string, suffix) ⇒ Ruby::Rego::BooleanValue

Parameters:

Returns:



37
38
39
40
41
42
43
44
45
# File 'lib/ruby/rego/builtins/strings/search.rb', line 37

def self.endswith(string, suffix)
  string_text, suffix_text = string_pair(
    string,
    suffix,
    left_context: "endswith string",
    right_context: "endswith suffix"
  )
  BooleanValue.new(string_text.end_with?(suffix_text))
end

.format_int(number, base) ⇒ Ruby::Rego::StringValue

Parameters:

Returns:



13
14
15
16
17
18
# File 'lib/ruby/rego/builtins/strings/formatting.rb', line 13

def self.format_int(number, base)
  number_value = NumericHelpers.integer_value(number, context: "format_int number")
  base_value = NumericHelpers.integer_value(base, context: "format_int base")
  ensure_base(base_value)
  StringValue.new(base_encode(number_value, base_value))
end

.indexof(haystack, needle) ⇒ Ruby::Rego::NumberValue

Parameters:

Returns:



50
51
52
53
54
55
56
57
58
59
# File 'lib/ruby/rego/builtins/strings/search.rb', line 50

def self.indexof(haystack, needle)
  haystack_text, needle_text = string_pair(
    haystack,
    needle,
    left_context: "indexof haystack",
    right_context: "indexof needle"
  )
  index = haystack_text.index(needle_text)
  NumberValue.new(index || -1)
end

.indexof_n(string, search) ⇒ Ruby::Rego::ArrayValue

All start indices (by character) of search in string, including overlapping matches (matching OPA: indexof_n(“aaa”,”aa”) => [0,1]). An empty search yields an undefined result (matching OPA).

Parameters:

Returns:



81
82
83
84
85
86
87
# File 'lib/ruby/rego/builtins/strings/search.rb', line 81

def self.indexof_n(string, search)
  haystack, needle = string_pair(
    string, search, left_context: "indexof_n string", right_context: "indexof_n search"
  )
  raise_empty_search("indexof_n") if needle.empty?
  ArrayValue.new(match_indices(haystack, needle).map { |index| NumberValue.new(index) })
end

.lower(string) ⇒ Ruby::Rego::StringValue

Parameters:

Returns:



10
11
12
# File 'lib/ruby/rego/builtins/strings/case_ops.rb', line 10

def self.lower(string)
  StringValue.new(string_value(string, context: "lower").downcase)
end

.register!Ruby::Rego::Builtins::BuiltinRegistry



55
56
57
58
59
60
61
# File 'lib/ruby/rego/builtins/strings.rb', line 55

def self.register!
  registry = BuiltinRegistry.instance

  register_configured_functions(registry, STRING_FUNCTIONS)

  registry
end

.replace(string, old, replacement) ⇒ Ruby::Rego::StringValue

Replaces every non-overlapping literal occurrence of old with new (matching OPA’s replace; the search is literal, not a regex).

Parameters:

Returns:



15
16
17
18
19
20
# File 'lib/ruby/rego/builtins/strings/transform.rb', line 15

def self.replace(string, old, replacement)
  text = string_value(string, context: "replace string")
  old_text = string_value(old, context: "replace substring")
  new_text = string_value(replacement, context: "replace value")
  StringValue.new(text.gsub(old_text) { new_text })
end

.replace_n(patterns, value) ⇒ Ruby::Rego::StringValue

Replaces occurrences of each key in patterns with its value, matching OPA’s strings.replace_n (modeled on Go’s strings.Replacer): keys are applied in ascending sort order with a single left-to-right pass, replaced text is never rescanned, and on overlapping matches the earliest-sorted key wins.

One deliberate divergence from OPA: scanning is by Unicode codepoint, not byte. This matters only for an empty (“”) key against multibyte text — OPA (byte-based) inserts the replacement between a character’s bytes, producing invalid UTF-8, whereas this inserts only at codepoint boundaries, keeping the output valid UTF-8.

Parameters:

Returns:



43
44
45
46
47
# File 'lib/ruby/rego/builtins/strings/transform.rb', line 43

def self.replace_n(patterns, value)
  mapping = string_hash(patterns, name: "strings.replace_n patterns")
  text = string_value(value, context: "strings.replace_n value")
  StringValue.new(apply_replace_n(text, mapping))
end

.split(string, delimiter) ⇒ Ruby::Rego::ArrayValue

Parameters:

Returns:



11
12
13
14
15
# File 'lib/ruby/rego/builtins/strings/split.rb', line 11

def self.split(string, delimiter)
  string_text = string_value(string, context: "split string")
  delimiter_text = string_value(delimiter, context: "split delimiter")
  ArrayValue.new(string_text.split(delimiter_text, -1))
end

.sprintf(format, args) ⇒ Ruby::Rego::StringValue

Parameters:

Returns:



23
24
25
26
27
28
29
# File 'lib/ruby/rego/builtins/strings/formatting.rb', line 23

def self.sprintf(format, args)
  format_value = string_value(format, context: "sprintf format")
  values = sprintf_values(args)
  StringValue.new(Kernel.sprintf(format_value, *values))
rescue ArgumentError, ::TypeError => error
  raise_sprintf_error(error)
end

.startswith(string, prefix) ⇒ Ruby::Rego::BooleanValue

Parameters:

Returns:



24
25
26
27
28
29
30
31
32
# File 'lib/ruby/rego/builtins/strings/search.rb', line 24

def self.startswith(string, prefix)
  string_text, prefix_text = string_pair(
    string,
    prefix,
    left_context: "startswith string",
    right_context: "startswith prefix"
  )
  BooleanValue.new(string_text.start_with?(prefix_text))
end

.strings_count(string, search) ⇒ Ruby::Rego::NumberValue

Counts non-overlapping occurrences of search in string (OPA’s strings.count). An empty search counts as the codepoint count plus one.

Parameters:

Returns:



67
68
69
70
71
72
# File 'lib/ruby/rego/builtins/strings/search.rb', line 67

def self.strings_count(string, search)
  haystack, needle = string_pair(
    string, search, left_context: "strings.count string", right_context: "strings.count search"
  )
  NumberValue.new(haystack.scan(needle).size)
end

.strings_reverse(string) ⇒ Ruby::Rego::StringValue

Reverses by Unicode codepoint (not grapheme cluster), matching OPA.

Parameters:

Returns:



26
27
28
# File 'lib/ruby/rego/builtins/strings/transform.rb', line 26

def self.strings_reverse(string)
  StringValue.new(string_value(string, context: "strings.reverse").reverse)
end

.substring(string, offset, length) ⇒ Ruby::Rego::StringValue

Parameters:

Returns:



12
13
14
15
16
17
18
# File 'lib/ruby/rego/builtins/strings/substring.rb', line 12

def self.substring(string, offset, length)
  string_text = string_value(string, context: "substring string")
  offset_value = NumericHelpers.non_negative_integer(offset, context: "substring offset")
  length_value = NumericHelpers.non_negative_integer(length, context: "substring length")
  substring = string_text.slice(offset_value, length_value) || ""
  StringValue.new(substring)
end

.trim(string, cutset) ⇒ Ruby::Rego::StringValue

Parameters:

Returns:



11
12
13
# File 'lib/ruby/rego/builtins/strings/trim.rb', line 11

def self.trim(string, cutset)
  trim_with_cutset(string, cutset, { mode: :both, name: "trim" })
end

.trim_left(string, cutset) ⇒ Ruby::Rego::StringValue

Parameters:

Returns:



18
19
20
# File 'lib/ruby/rego/builtins/strings/trim.rb', line 18

def self.trim_left(string, cutset)
  trim_with_cutset(string, cutset, { mode: :left, name: "trim_left" })
end

.trim_prefix(string, prefix) ⇒ Ruby::Rego::StringValue

Parameters:

Returns:



38
39
40
41
42
43
# File 'lib/ruby/rego/builtins/strings/trim.rb', line 38

def self.trim_prefix(string, prefix)
  text, prefix_text = string_pair(
    string, prefix, left_context: "trim_prefix string", right_context: "trim_prefix prefix"
  )
  StringValue.new(text.delete_prefix(prefix_text))
end

.trim_right(string, cutset) ⇒ Ruby::Rego::StringValue

Parameters:

Returns:



25
26
27
# File 'lib/ruby/rego/builtins/strings/trim.rb', line 25

def self.trim_right(string, cutset)
  trim_with_cutset(string, cutset, { mode: :right, name: "trim_right" })
end

.trim_space(string) ⇒ Ruby::Rego::StringValue

Parameters:

Returns:



31
32
33
# File 'lib/ruby/rego/builtins/strings/trim.rb', line 31

def self.trim_space(string)
  StringValue.new(string_value(string, context: "trim_space").strip)
end

.trim_suffix(string, suffix) ⇒ Ruby::Rego::StringValue

Parameters:

Returns:



48
49
50
51
52
53
# File 'lib/ruby/rego/builtins/strings/trim.rb', line 48

def self.trim_suffix(string, suffix)
  text, suffix_text = string_pair(
    string, suffix, left_context: "trim_suffix string", right_context: "trim_suffix suffix"
  )
  StringValue.new(text.delete_suffix(suffix_text))
end

.upper(string) ⇒ Ruby::Rego::StringValue

Parameters:

Returns:



16
17
18
# File 'lib/ruby/rego/builtins/strings/case_ops.rb', line 16

def self.upper(string)
  StringValue.new(string_value(string, context: "upper").upcase)
end