Module: Ruby::Rego::Builtins::Codecs::JsonSchema::Formats

Defined in:
lib/ruby/rego/builtins/codecs/json_schema_formats.rb,
lib/ruby/rego/builtins/codecs/json_schema_email.rb

Overview

The gojsonschema format checkers (format_checkers.go) OPA 1.17 enforces. See the file header.

Defined Under Namespace

Modules: Rfc2047 Classes: MailAddress

Constant Summary collapse

HOST_LABEL =

gojsonschema’s hostname label, then the dotted regex anchored \A..\z for Go’s whole-text ^..$.

"[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]"
HOSTNAME =
/\A(#{HOST_LABEL})(\.(#{HOST_LABEL}))*\z/
UUID =
/\A[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\z/i
JSON_POINTER =
%r{\A(?:/(?:[^~/]|~0|~1)*)*\z}
RELATIVE_JSON_POINTER =
%r{\A(?:0|[1-9][0-9]*)(?:#|(?:/(?:[^~/]|~0|~1)*)*)\z}
DATE =

Go time.Parse layouts: hour is “15” (1-2 digits, <=23), minute/second are “04”/”05” (exactly 2); fractional seconds are an optional run of digits after a period OR comma (Go accepts both, per ISO 8601 §5.6); a zone is “Z” or “+hh:mm”/”-hh:mm” where Go range-checks the offset (hour <=24, minute <=60 — wider than the time-of-day fields). The offset hh:mm is captured so it can be range-checked; an absent zone / “Z” leaves those groups nil (->0, which passes). date-time additionally accepts a bare date or bare time (gojsonschema tries five layouts), and full RFC3339 requires an uppercase “T” and a zone.

/\A(\d{4})-(\d{2})-(\d{2})\z/
TIME =
/\A(\d{1,2}):(\d{2}):(\d{2})(?:[.,]\d+)?(?:Z|[+-](\d{2}):(\d{2}))?\z/
RFC3339 =
/\A(\d{4})-(\d{2})-(\d{2})T(\d{1,2}):(\d{2}):(\d{2})(?:[.,]\d+)?(?:Z|[+-](\d{2}):(\d{2}))\z/
URI_TEMPLATE_PATH =

gojsonschema’s uri-template path check, verbatim, compiled with the re2 engine (Go’s regexp) so it is byte-exact and linear (Ruby’s Onigmo would risk backtracking on the nested quantifier).

RE2::Regexp.new("^([^{]*({[^}]*})?)*$", log_errors: false)

Class Method Summary collapse

Class Method Details

.absolute_uri?(value) ⇒ Boolean

uri / iri: a parseable URL with a non-empty scheme. :reek:NilCheck

Returns:

  • (Boolean)


178
179
180
181
# File 'lib/ruby/rego/builtins/codecs/json_schema_formats.rb', line 178

def self.absolute_uri?(value)
  components = url_components(value)
  !components.nil? && !components.scheme.to_s.empty?
end

.date?(value) ⇒ Boolean

Returns:

  • (Boolean)


105
106
107
# File 'lib/ruby/rego/builtins/codecs/json_schema_formats.rb', line 105

def self.date?(value)
  with_parts(value, DATE) { |year, month, day| valid_date?(year, month, day) }
end

.email?(value) ⇒ Boolean

email / idn-email: gojsonschema runs both through Go’s net/mail.ParseAddress (idn-email is the same checker – RFC 6532 UTF-8 is allowed in atoms either way). See MailAddress (json_schema_email). The scannable? guard ensures the parser only sees valid UTF-8 (matching Go, which rejects invalid-UTF-8 addresses anyway).

Returns:

  • (Boolean)


203
204
205
# File 'lib/ruby/rego/builtins/codecs/json_schema_formats.rb', line 203

def self.email?(value)
  scannable?(value) && MailAddress.valid?(value)
end

.hostname?(value) ⇒ Boolean

gojsonschema: the hostname regex AND len(input) < 256 (bytes).

Returns:

  • (Boolean)


101
102
103
# File 'lib/ruby/rego/builtins/codecs/json_schema_formats.rb', line 101

def self.hostname?(value)
  value.bytesize < 256 && re_match?(value, HOSTNAME)
end

.ip?(value, separator) ⇒ Boolean

Go net.ParseIP + a separator check. net.ParseIP rejects a zone id (%) and CIDR (/) that Ruby’s IPAddr would otherwise accept; otherwise IPAddr matches net.ParseIP exactly (verified differentially). ipv4 also requires a “.”, ipv6 a “:”. Total: any parse failure -> false.

Returns:

  • (Boolean)


155
156
157
158
159
160
161
162
# File 'lib/ruby/rego/builtins/codecs/json_schema_formats.rb', line 155

def self.ip?(value, separator)
  return false if value.include?("%") || value.include?("/")

  IPAddr.new(value)
  value.include?(separator)
rescue StandardError
  false
end

.match?(name, value) ⇒ bool

rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength :reek:ControlParameter :reek:TooManyStatements :reek:DuplicateMethodCall

Parameters:

  • name (String)

    the format keyword value

  • value (String)

    the string instance being validated

Returns:

  • (bool)


67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/ruby/rego/builtins/codecs/json_schema_formats.rb', line 67

def self.match?(name, value)
  case name
  when "hostname" then hostname?(value)
  when "uuid" then re_match?(value, UUID)
  when "json-pointer" then re_match?(value, JSON_POINTER)
  when "relative-json-pointer" then re_match?(value, RELATIVE_JSON_POINTER)
  when "regex" then scannable?(value) && JsonSchema.re2_valid?(value)
  when "date" then date?(value)
  when "time" then time?(value)
  when "date-time" then date?(value) || time?(value) || rfc3339?(value)
  when "ipv4" then ip?(value, ".")
  when "ipv6" then ip?(value, ":")
  when "uri", "iri" then absolute_uri?(value)
  when "uri-reference", "iri-reference" then uri_reference?(value)
  when "uri-template" then uri_template?(value)
  when "email", "idn-email" then email?(value)
  else true # idn-hostname / duration / unknown -> annotation only
  end
end

.re_match?(value, regexp) ⇒ Boolean

A Ruby-regex format match, total on binary / invalid-encoding input.

Returns:

  • (Boolean)


96
97
98
# File 'lib/ruby/rego/builtins/codecs/json_schema_formats.rb', line 96

def self.re_match?(value, regexp)
  scannable?(value) && regexp.match?(value)
end

.rfc3339?(value) ⇒ Boolean

rubocop:disable Metrics/ParameterLists – named date/time/offset fields read clearer than parts[]

Returns:

  • (Boolean)


116
117
118
119
120
121
# File 'lib/ruby/rego/builtins/codecs/json_schema_formats.rb', line 116

def self.rfc3339?(value)
  with_parts(value, RFC3339) do |year, month, day, hour, minute, second, offset_hour, offset_minute|
    valid_date?(year, month, day) && valid_clock?(hour, minute, second) &&
      valid_offset?(offset_hour, offset_minute)
  end
end

.scannable?(value) ⇒ Boolean

A string the Ruby regex engine can scan without raising: valid in its encoding and either pure ASCII or UTF-8 (a binary high-byte string would raise on match?). Rego strings reaching OPA are always UTF-8, so a non-scannable value is unreachable in practice; rejecting it keeps totality.

Returns:

  • (Boolean)


91
92
93
# File 'lib/ruby/rego/builtins/codecs/json_schema_formats.rb', line 91

def self.scannable?(value)
  value.valid_encoding? && (value.ascii_only? || value.encoding == Encoding::UTF_8)
end

.time?(value) ⇒ Boolean

Returns:

  • (Boolean)


109
110
111
112
113
# File 'lib/ruby/rego/builtins/codecs/json_schema_formats.rb', line 109

def self.time?(value)
  with_parts(value, TIME) do |hour, minute, second, offset_hour, offset_minute|
    valid_clock?(hour, minute, second) && valid_offset?(offset_hour, offset_minute)
  end
end

.uri_reference?(value) ⇒ Boolean

uri-reference / iri-reference: a parseable URL (scheme optional). :reek:NilCheck

Returns:

  • (Boolean)


185
186
187
# File 'lib/ruby/rego/builtins/codecs/json_schema_formats.rb', line 185

def self.uri_reference?(value)
  !url_components(value).nil?
end

.uri_template?(value) ⇒ Boolean

uri-template: a parseable URL whose path matches gojsonschema’s template regex. The path is percent-DECODED by the parser, so it can hold bytes that are invalid UTF-8 (e.g. %FF); Go’s regexp engine substitutes U+FFFD for each undecodable byte and keeps matching, whereas re2 won’t match an undecodable subject — so scrub the path the same way Go’s decoder would first. :reek:NilCheck

Returns:

  • (Boolean)


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

def self.uri_template?(value)
  components = url_components(value)
  !components.nil? && URI_TEMPLATE_PATH.match?(components.path.to_s.scrub("\uFFFD"))
end

.url_components(value) ⇒ Object

Go net/url.Parse (via Uri::Parser) of a value with no backslash. nil if it does not parse, is not scannable (Uri::Parser raises on invalid-UTF-8), or contains a backslash (gojsonschema rejects \ explicitly, separate from url.Parse). The rescue keeps it total against any other parse error. :reek:NilCheck



168
169
170
171
172
173
174
# File 'lib/ruby/rego/builtins/codecs/json_schema_formats.rb', line 168

def self.url_components(value)
  return nil unless scannable?(value) && !value.include?("\\")

  Uri::Parser.parse(value)
rescue StandardError
  nil
end

.valid_clock?(hour, minute, second) ⇒ Boolean

Returns:

  • (Boolean)


142
143
144
# File 'lib/ruby/rego/builtins/codecs/json_schema_formats.rb', line 142

def self.valid_clock?(hour, minute, second)
  hour <= 23 && minute <= 59 && second <= 59
end

.valid_date?(year, month, day) ⇒ Boolean

Proleptic-Gregorian calendar validity (Go’s time package is proleptic Gregorian; Ruby’s default Date switches to Julian before 1582, so pin Date::GREGORIAN).

Returns:

  • (Boolean)


138
139
140
# File 'lib/ruby/rego/builtins/codecs/json_schema_formats.rb', line 138

def self.valid_date?(year, month, day)
  Date.valid_date?(year, month, day, Date::GREGORIAN)
end

.valid_offset?(offset_hour, offset_minute) ⇒ Boolean

Go time.Parse range-checks the numeric zone offset more loosely than the clock: hour <=24, minute <=60. Absent zone / “Z” arrive as 0, which passes.

Returns:

  • (Boolean)


148
149
150
# File 'lib/ruby/rego/builtins/codecs/json_schema_formats.rb', line 148

def self.valid_offset?(offset_hour, offset_minute)
  offset_hour <= 24 && offset_minute <= 60
end

.with_parts(value, regexp) {|match.captures.map { |group| group.to_s.to_i }| ... } ⇒ Object

Yield the numeric capture groups of regexp matched against a scannable value (absent optional groups arrive as 0) so the caller can range-check named fields; false if it does not match. :reek:NilCheck

Yields:

  • (match.captures.map { |group| group.to_s.to_i })


127
128
129
130
131
132
133
134
# File 'lib/ruby/rego/builtins/codecs/json_schema_formats.rb', line 127

def self.with_parts(value, regexp)
  return false unless scannable?(value)

  match = regexp.match(value)
  return false if match.nil?

  yield(*match.captures.map { |group| group.to_s.to_i })
end