Module: Ruby::Rego::Builtins::Codecs

Extended by:
RegistryHelpers
Defined in:
lib/ruby/rego/builtins/codecs.rb,
lib/ruby/rego/builtins/codecs/url_query.rb,
lib/ruby/rego/builtins/codecs/json_format.rb,
lib/ruby/rego/builtins/codecs/json_schema.rb,
lib/ruby/rego/builtins/codecs/json_decoder.rb,
lib/ruby/rego/builtins/codecs/json_schema_email.rb,
lib/ruby/rego/builtins/codecs/json_schema_match.rb,
lib/ruby/rego/builtins/codecs/json_schema_formats.rb

Overview

Built-in encoding/decoding helpers.

Defined Under Namespace

Modules: JsonDecoder, JsonSchema

Constant Summary collapse

CODEC_FUNCTIONS =
{
  "json.marshal" => { arity: 1, handler: :json_marshal },
  "json.marshal_with_options" => { arity: 2, handler: :json_marshal_with_options },
  "json.unmarshal" => { arity: 1, handler: :json_unmarshal },
  "json.is_valid" => { arity: 1, handler: :json_is_valid },
  "json.verify_schema" => { arity: 1, handler: :json_verify_schema },
  "json.match_schema" => { arity: 2, handler: :json_match_schema },
  "base64.encode" => { arity: 1, handler: :base64_encode },
  "base64.decode" => { arity: 1, handler: :base64_decode },
  "base64.is_valid" => { arity: 1, handler: :base64_is_valid },
  "base64url.encode" => { arity: 1, handler: :base64url_encode },
  "base64url.encode_no_pad" => { arity: 1, handler: :base64url_encode_no_pad },
  "base64url.decode" => { arity: 1, handler: :base64url_decode },
  "hex.encode" => { arity: 1, handler: :hex_encode },
  "hex.decode" => { arity: 1, handler: :hex_decode },
  "urlquery.encode" => { arity: 1, handler: :urlquery_encode },
  "urlquery.encode_object" => { arity: 1, handler: :urlquery_encode_object },
  "urlquery.decode" => { arity: 1, handler: :urlquery_decode },
  "urlquery.decode_object" => { arity: 1, handler: :urlquery_decode_object }
}.freeze
MALFORMED_PERCENT =

A % not followed by two hex digits — a malformed percent-escape that OPA (Go’s url.QueryUnescape) rejects but CGI.unescape would pass through.

/%(?![0-9a-fA-F]{2})/
INDENT_SENTINEL =

A NUL indent sentinel for json.marshal_with_options: JSON.generate never emits a literal NUL inside a value (it escapes to \u0000) and escape_html ignores it, so it can carry the structural indent through HTML-escaping and be swapped for the real indent.

"\u0000"

Class Method Summary collapse

Methods included from RegistryHelpers

register_configured_functions

Class Method Details

.base64_decode(value) ⇒ Ruby::Rego::StringValue

Parameters:

Returns:



229
230
231
232
# File 'lib/ruby/rego/builtins/codecs.rb', line 229

def self.base64_decode(value)
  string = string_arg(value, "base64.decode")
  decoded("base64.decode") { StringValue.new(Base64.strict_decode64(string)) }
end

.base64_encode(value) ⇒ Ruby::Rego::StringValue

Parameters:

Returns:



223
224
225
# File 'lib/ruby/rego/builtins/codecs.rb', line 223

def self.base64_encode(value)
  StringValue.new(Base64.strict_encode64(string_arg(value, "base64.encode")))
end

.base64_is_valid(value) ⇒ Ruby::Rego::BooleanValue

Parameters:

Returns:



236
237
238
239
240
241
# File 'lib/ruby/rego/builtins/codecs.rb', line 236

def self.base64_is_valid(value)
  Base64.strict_decode64(string_arg(value, "base64.is_valid"))
  BooleanValue.new(true)
rescue ArgumentError
  BooleanValue.new(false)
end

.base64url_decode(value) ⇒ Ruby::Rego::StringValue

OPA accepts padded and unpadded URL-safe base64 but rejects the standard-base64 ‘+’/’/’ and non-canonical padding; Base64Url.strict_decode enforces that (shared with io.jwt.decode), and the decoded block maps its ArgumentError to undefined.

Parameters:

Returns:



255
256
257
258
# File 'lib/ruby/rego/builtins/codecs.rb', line 255

def self.base64url_decode(value)
  string = string_arg(value, "base64url.decode")
  decoded("base64url.decode") { StringValue.new(Base64Url.strict_decode(string)) }
end

.base64url_encode(value) ⇒ Ruby::Rego::StringValue

Parameters:

Returns:



245
246
247
# File 'lib/ruby/rego/builtins/codecs.rb', line 245

def self.base64url_encode(value)
  StringValue.new(Base64.urlsafe_encode64(string_arg(value, "base64url.encode")))
end

.base64url_encode_no_pad(value) ⇒ Ruby::Rego::StringValue

Parameters:

Returns:



299
300
301
# File 'lib/ruby/rego/builtins/codecs.rb', line 299

def self.base64url_encode_no_pad(value)
  StringValue.new(Base64.urlsafe_encode64(string_arg(value, "base64url.encode_no_pad"), padding: false))
end

.canonical_json(value) ⇒ String

The exact bytes OPA’s io.jwt.encode_sign signs for a header/payload object — json.marshal’s serialization (sorted keys, sets as sorted arrays, Go HTML escaping of <>& and U+2028/U+2029) as a raw String. Raises BuiltinArgumentError (-> undefined) on a non-marshalable value.

Rego numbers (Number / Integer) serialise byte-exactly with OPA: a Number emits its canonical text (1.50 stays 1.50, 1e308 stays 1e308) and integers are exact via Bignum. A Float only reaches here from a not-yet-migrated builtin output, where it falls back to Ruby’s Float#to_s (the residual JSON-number divergence tracked for the builtin number sweep); a non-finite Float raises JSON::JSONError and is mapped to undefined by the rescue below.

Parameters:

Returns:

  • (String)


77
78
79
80
81
# File 'lib/ruby/rego/builtins/codecs.rb', line 77

def self.canonical_json(value)
  escape_html(JSON.generate(jsonify(value.to_ruby)))
rescue JSON::JSONError, ArgumentError => e
  raise_marshal_error(e, "json.marshal")
end

.hex_decode(value) ⇒ Ruby::Rego::StringValue

Parameters:

Returns:



268
269
270
271
272
273
274
275
# File 'lib/ruby/rego/builtins/codecs.rb', line 268

def self.hex_decode(value)
  string = string_arg(value, "hex.decode")
  decoded("hex.decode") do
    raise ArgumentError, "invalid hex string" unless string.match?(/\A(?:[0-9a-fA-F]{2})*\z/)

    StringValue.new([string].pack("H*"))
  end
end

.hex_encode(value) ⇒ Ruby::Rego::StringValue

Parameters:

Returns:



262
263
264
# File 'lib/ruby/rego/builtins/codecs.rb', line 262

def self.hex_encode(value)
  StringValue.new(string_arg(value, "hex.encode").unpack1("H*").to_s)
end

.json_is_valid(value) ⇒ Ruby::Rego::BooleanValue

Parameters:

Returns:



195
196
197
# File 'lib/ruby/rego/builtins/codecs.rb', line 195

def self.json_is_valid(value)
  BooleanValue.new(JsonDecoder.valid?(string_arg(value, "json.is_valid")))
end

.json_marshal(value) ⇒ Ruby::Rego::StringValue

Compact JSON with object keys sorted, sets rendered as sorted arrays, and Go-style HTML escaping, matching OPA’s json.marshal output.

Parameters:

Returns:



88
89
90
# File 'lib/ruby/rego/builtins/codecs.rb', line 88

def self.json_marshal(value)
  StringValue.new(canonical_json(value))
end

.json_marshal_with_options(value, options_value) ⇒ Ruby::Rego::StringValue

Compact or pretty-printed JSON, matching OPA’s json.marshal_with_options. The options object takes prefix and indent strings and a pretty boolean; pretty-printing is enabled by pretty: true, or — when pretty is absent — implicitly by supplying a prefix or indent. Pretty output uses Go’s json.MarshalIndent indent-per-depth layout; OPA then prepends the prefix to every line, including the first (MarshalIndent itself omits it on line one). A non-object options argument, an unknown option key, a wrongly-typed option value, or an unmarshalable document yields undefined.

:reek:TooManyStatements

Parameters:

Returns:



104
105
106
107
108
109
110
111
# File 'lib/ruby/rego/builtins/codecs.rb', line 104

def self.json_marshal_with_options(value, options_value)
  prefix, indent, pretty = marshal_options(options_value)
  ruby = jsonify(value.to_ruby)
  output = pretty ? pretty_json(ruby, prefix, indent) : escape_html(JSON.generate(ruby))
  StringValue.new(output)
rescue JSON::JSONError, ArgumentError => e
  raise_marshal_error(e, "json.marshal_with_options")
end

.json_match_schema(document_value, schema_value) ⇒ Ruby::Rego::Value

json.match_schema(document, schema): [match, errors] — whether document (a JSON string or object/array) validates against schema. Undefined when an argument is unusable or the schema is not well-formed, matching OPA’s gojsonschema. The boolean match is byte-exact; the errors array is best-effort (presence only). See Codecs::JsonSchema.match.

Parameters:

Returns:



216
217
218
219
# File 'lib/ruby/rego/builtins/codecs.rb', line 216

def self.json_match_schema(document_value, schema_value)
  result = JsonSchema.match(document_value, schema_value)
  result == :undefined ? UndefinedValue.new : Value.from_ruby(result)
end

.json_unmarshal(value) ⇒ Ruby::Rego::Value

Parameters:

Returns:



188
189
190
191
# File 'lib/ruby/rego/builtins/codecs.rb', line 188

def self.json_unmarshal(value)
  string = string_arg(value, "json.unmarshal")
  decoded("json.unmarshal") { Value.from_ruby(JsonDecoder.parse(string)) }
end

.json_verify_schema(value) ⇒ Ruby::Rego::ArrayValue

json.verify_schema(schema): [valid, error] — true/null when schema (a JSON string or object) is a well-formed JSON schema, else false and an error string. Matches OPA’s gojsonschema on the boolean; the error wording is best-effort (a documented divergence). See Codecs::JsonSchema.

Parameters:

Returns:



204
205
206
207
# File 'lib/ruby/rego/builtins/codecs.rb', line 204

def self.json_verify_schema(value)
  valid, error = JsonSchema.verify(value)
  Value.from_ruby([valid, error])
end

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



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

def self.register!
  registry = BuiltinRegistry.instance
  register_configured_functions(registry, CODEC_FUNCTIONS)
  registry
end

.urlquery_decode(value) ⇒ Ruby::Rego::StringValue

OPA (Go’s url.QueryUnescape) rejects malformed percent-escapes; CGI.unescape passes them through, so they are validated and rejected to match OPA.

Parameters:

Returns:



288
289
290
291
292
293
294
295
# File 'lib/ruby/rego/builtins/codecs.rb', line 288

def self.urlquery_decode(value)
  string = string_arg(value, "urlquery.decode")
  decoded("urlquery.decode") do
    raise ArgumentError, "invalid percent-encoding" if string.match?(MALFORMED_PERCENT)

    StringValue.new(CGI.unescape(string))
  end
end

.urlquery_decode_object(value) ⇒ Ruby::Rego::ObjectValue

Decodes a query string to an object mapping each key to its array of values, matching OPA (Go’s url.ParseQuery). Reuses urlquery.decode’s percent validation and unescaping; a malformed percent-escape (in any key or value) yields undefined, as does a literal ; — Go’s ParseQuery rejects it as a separator (a ; in a value must be percent-encoded).

Parameters:

Returns:



90
91
92
93
94
95
96
97
98
# File 'lib/ruby/rego/builtins/codecs/url_query.rb', line 90

def self.urlquery_decode_object(value)
  string = string_arg(value, "urlquery.decode_object")
  decoded("urlquery.decode_object") do
    raise ArgumentError, "invalid percent-encoding" if string.match?(MALFORMED_PERCENT)
    raise ArgumentError, "semicolon separator" if string.include?(";")

    ObjectValue.new(grouped_query(string))
  end
end

.urlquery_encode(value) ⇒ Ruby::Rego::StringValue

Parameters:

Returns:



279
280
281
# File 'lib/ruby/rego/builtins/codecs.rb', line 279

def self.urlquery_encode(value)
  StringValue.new(CGI.escape(string_arg(value, "urlquery.encode")))
end

.urlquery_encode_object(value) ⇒ Ruby::Rego::StringValue

Encodes an object as a query string, matching OPA (Go’s url.Values.Encode): keys are sorted; a string value emits one pair, an array/set value one pair per element (arrays keep order; sets are sorted and de-duplicated). A non-object, a non-string key, or a value that is not a string or string array/set yields undefined.

Parameters:

Returns:



15
16
17
18
19
20
21
22
# File 'lib/ruby/rego/builtins/codecs/url_query.rb', line 15

def self.urlquery_encode_object(value)
  Base.assert_type(value, expected: ObjectValue, context: "urlquery.encode_object")
  StringValue.new(query_pairs(value.value).join("&"))
rescue EncodingError
  # A non-ASCII-compatible key/value (Ruby API only) makes CGI.escape raise; treat
  # it as an invalid object rather than letting it escape.
  raise_invalid_object
end