class JSONSchemer::Schema::Base

Constants

BOOLEANS
DEFAULT_REF_RESOLVER
ID_KEYWORD
INSERT_DEFAULT_PROPERTY
Instance
NET_HTTP_REF_RESOLVER
RUBY_REGEX_ANCHORS_TO_ECMA_262

Attributes

formats[R]
keywords[R]
ref_resolver[R]
root[R]

Public Class Methods

new( schema, format: true, insert_property_defaults: false, before_property_validation: nil, after_property_validation: nil, formats: nil, keywords: nil, ref_resolver: DEFAULT_REF_RESOLVER ) click to toggle source
# File lib/json_schemer/schema/base.rb, line 39
def initialize(
  schema,
  format: true,
  insert_property_defaults: false,
  before_property_validation: nil,
  after_property_validation: nil,
  formats: nil,
  keywords: nil,
  ref_resolver: DEFAULT_REF_RESOLVER
)
  raise InvalidSymbolKey, 'schemas must use string keys' if schema.is_a?(Hash) && !schema.empty? && !schema.first.first.is_a?(String)
  @root = schema
  @format = format
  @before_property_validation = [*before_property_validation]
  @before_property_validation.unshift(INSERT_DEFAULT_PROPERTY) if insert_property_defaults
  @after_property_validation = [*after_property_validation]
  @formats = formats
  @keywords = keywords
  @ref_resolver = ref_resolver == 'net/http' ? CachedRefResolver.new(&NET_HTTP_REF_RESOLVER) : ref_resolver
end

Public Instance Methods

valid?(data) click to toggle source
# File lib/json_schemer/schema/base.rb, line 60
def valid?(data)
  valid_instance?(Instance.new(data, '', root, '', nil, @before_property_validation, @after_property_validation))
end
validate(data) click to toggle source
# File lib/json_schemer/schema/base.rb, line 64
def validate(data)
  validate_instance(Instance.new(data, '', root, '', nil, @before_property_validation, @after_property_validation))
end

Protected Instance Methods

ids() click to toggle source
# File lib/json_schemer/schema/base.rb, line 201
def ids
  @ids ||= resolve_ids(root)
end
valid_instance?(instance) click to toggle source
# File lib/json_schemer/schema/base.rb, line 70
def valid_instance?(instance)
  validate_instance(instance).none?
end
validate_instance(instance) { |error(instance, 'schema')| ... } click to toggle source
# File lib/json_schemer/schema/base.rb, line 74
def validate_instance(instance, &block)
  return enum_for(:validate_instance, instance) unless block_given?

  schema = instance.schema

  if schema == false
    yield error(instance, 'schema')
    return
  end

  return if schema == true || schema.empty?

  type = schema['type']
  enum = schema['enum']
  all_of = schema['allOf']
  any_of = schema['anyOf']
  one_of = schema['oneOf']
  not_schema = schema['not']
  if_schema = schema['if']
  then_schema = schema['then']
  else_schema = schema['else']
  format = schema['format']
  ref = schema['$ref']
  id = schema[id_keyword]

  instance.parent_uri = join_uri(instance.parent_uri, id)

  if ref
    validate_ref(instance, ref, &block)
    return
  end

  if format? && custom_format?(format)
    validate_custom_format(instance, formats.fetch(format), &block)
  end

  data = instance.data

  if keywords
    keywords.each do |keyword, callable|
      if schema.key?(keyword)
        result = callable.call(data, schema, instance.data_pointer)
        if result.is_a?(Array)
          result.each(&block)
        elsif !result
          yield error(instance, keyword)
        end
      end
    end
  end

  yield error(instance, 'enum') if enum && !enum.include?(data)
  yield error(instance, 'const') if schema.key?('const') && schema['const'] != data

  if all_of
    all_of.each_with_index do |subschema, index|
      subinstance = instance.merge(
        schema: subschema,
        schema_pointer: "#{instance.schema_pointer}/allOf/#{index}",
        before_property_validation: false,
        after_property_validation: false
      )
      validate_instance(subinstance, &block)
    end
  end

  if any_of
    subschemas = any_of.lazy.with_index.map do |subschema, index|
      subinstance = instance.merge(
        schema: subschema,
        schema_pointer: "#{instance.schema_pointer}/anyOf/#{index}",
        before_property_validation: false,
        after_property_validation: false
      )
      validate_instance(subinstance)
    end
    subschemas.each { |subschema| subschema.each(&block) } unless subschemas.any?(&:none?)
  end

  if one_of
    subschemas = one_of.map.with_index do |subschema, index|
      subinstance = instance.merge(
        schema: subschema,
        schema_pointer: "#{instance.schema_pointer}/oneOf/#{index}",
        before_property_validation: false,
        after_property_validation: false
      )
      validate_instance(subinstance)
    end
    valid_subschema_count = subschemas.count(&:none?)
    if valid_subschema_count > 1
      yield error(instance, 'oneOf')
    elsif valid_subschema_count == 0
      subschemas.each { |subschema| subschema.each(&block) }
    end
  end

  unless not_schema.nil?
    subinstance = instance.merge(
      schema: not_schema,
      schema_pointer: "#{instance.schema_pointer}/not",
      before_property_validation: false,
      after_property_validation: false
    )
    yield error(subinstance, 'not') if valid_instance?(subinstance)
  end

  if if_schema && valid_instance?(instance.merge(schema: if_schema, before_property_validation: false, after_property_validation: false))
    validate_instance(instance.merge(schema: then_schema, schema_pointer: "#{instance.schema_pointer}/then"), &block) unless then_schema.nil?
  elsif if_schema
    validate_instance(instance.merge(schema: else_schema, schema_pointer: "#{instance.schema_pointer}/else"), &block) unless else_schema.nil?
  end

  case type
  when nil
    validate_class(instance, &block)
  when String
    validate_type(instance, type, &block)
  when Array
    if valid_type = type.find { |subtype| valid_instance?(instance.merge(schema: { 'type' => subtype })) }
      validate_type(instance, valid_type, &block)
    else
      yield error(instance, 'type')
    end
  end
end

Private Instance Methods

child(schema) click to toggle source
# File lib/json_schemer/schema/base.rb, line 225
def child(schema)
  JSONSchemer.schema(
    schema,
    format: format?,
    formats: formats,
    keywords: keywords,
    ref_resolver: ref_resolver
  )
end
custom_format?(format) click to toggle source
# File lib/json_schemer/schema/base.rb, line 217
def custom_format?(format)
  !!(formats && formats.key?(format))
end
ecma_262_regex(pattern) click to toggle source
# File lib/json_schemer/schema/base.rb, line 600
def ecma_262_regex(pattern)
  @ecma_262_regex ||= {}
  @ecma_262_regex[pattern] ||= Regexp.new(
    Regexp::Scanner.scan(pattern).map do |type, token, text|
      type == :anchor ? RUBY_REGEX_ANCHORS_TO_ECMA_262.fetch(token, text) : text
    end.join
  )
end
error(instance, type, details = nil) click to toggle source
# File lib/json_schemer/schema/base.rb, line 235
def error(instance, type, details = nil)
  error = {
    'data' => instance.data,
    'data_pointer' => instance.data_pointer,
    'schema' => instance.schema,
    'schema_pointer' => instance.schema_pointer,
    'root_schema' => root,
    'type' => type,
  }
  error['details'] = details if details
  error
end
format?() click to toggle source
# File lib/json_schemer/schema/base.rb, line 213
def format?
  !!@format
end
id_keyword() click to toggle source
# File lib/json_schemer/schema/base.rb, line 209
def id_keyword
  ID_KEYWORD
end
join_uri(a, b) click to toggle source
# File lib/json_schemer/schema/base.rb, line 609
def join_uri(a, b)
  b = URI.parse(b) if b
  if a && b && a.relative? && b.relative?
    b
  elsif a && b
    URI.join(a, b)
  elsif b
    b
  else
    a
  end
end
pointer_uri(schema, pointer) click to toggle source
# File lib/json_schemer/schema/base.rb, line 622
def pointer_uri(schema, pointer)
  uri_parts = nil
  pointer.reduce(schema) do |obj, token|
    next obj.fetch(token.to_i) if obj.is_a?(Array)
    if obj_id = obj[id_keyword]
      uri_parts ||= []
      uri_parts << obj_id
    end
    obj.fetch(token)
  end
  uri_parts ? URI.join(*uri_parts) : nil
end
resolve_ids(schema, ids = {}, parent_uri = nil, pointer = '') click to toggle source
# File lib/json_schemer/schema/base.rb, line 635
def resolve_ids(schema, ids = {}, parent_uri = nil, pointer = '')
  if schema.is_a?(Array)
    schema.each_with_index { |subschema, index| resolve_ids(subschema, ids, parent_uri, "#{pointer}/#{index}") }
  elsif schema.is_a?(Hash)
    uri = join_uri(parent_uri, schema[id_keyword])
    schema.each do |key, value|
      if key == id_keyword && uri != parent_uri
        ids[uri.to_s] = {
          schema: schema,
          pointer: pointer
        }
      end
      resolve_ids(value, ids, uri, "#{pointer}/#{key}")
    end
  end
  ids
end
resolve_ref(uri) click to toggle source
# File lib/json_schemer/schema/base.rb, line 653
def resolve_ref(uri)
  ref_resolver.call(uri) || raise(InvalidRefResolution, uri.to_s)
end
safe_strict_decode64(data) click to toggle source
# File lib/json_schemer/schema/base.rb, line 593
def safe_strict_decode64(data)
  Base64.strict_decode64(data)
rescue ArgumentError => e
  raise e unless e.message == 'invalid base64'
  nil
end
spec_format?(format) click to toggle source
# File lib/json_schemer/schema/base.rb, line 221
def spec_format?(format)
  !custom_format?(format) && supported_format?(format)
end
validate_array(instance) { |error(instance, 'array')| ... } click to toggle source
# File lib/json_schemer/schema/base.rb, line 429
def validate_array(instance, &block)
  data = instance.data

  unless data.is_a?(Array)
    yield error(instance, 'array')
    return
  end

  schema = instance.schema

  items = schema['items']
  additional_items = schema['additionalItems']
  max_items = schema['maxItems']
  min_items = schema['minItems']
  unique_items = schema['uniqueItems']
  contains = schema['contains']

  yield error(instance, 'maxItems') if max_items && data.size > max_items
  yield error(instance, 'minItems') if min_items && data.size < min_items
  yield error(instance, 'uniqueItems') if unique_items && data.size != data.uniq.size
  yield error(instance, 'contains') if !contains.nil? && data.all? { |item| !valid_instance?(instance.merge(data: item, schema: contains)) }

  if items.is_a?(Array)
    data.each_with_index do |item, index|
      if index < items.size
        subinstance = instance.merge(
          data: item,
          data_pointer: "#{instance.data_pointer}/#{index}",
          schema: items[index],
          schema_pointer: "#{instance.schema_pointer}/items/#{index}"
        )
        validate_instance(subinstance, &block)
      elsif !additional_items.nil?
        subinstance = instance.merge(
          data: item,
          data_pointer: "#{instance.data_pointer}/#{index}",
          schema: additional_items,
          schema_pointer: "#{instance.schema_pointer}/additionalItems"
        )
        validate_instance(subinstance, &block)
      else
        break
      end
    end
  elsif !items.nil?
    data.each_with_index do |item, index|
      subinstance = instance.merge(
        data: item,
        data_pointer: "#{instance.data_pointer}/#{index}",
        schema: items,
        schema_pointer: "#{instance.schema_pointer}/items"
      )
      validate_instance(subinstance, &block)
    end
  end
end
validate_class(instance, &block) click to toggle source
# File lib/json_schemer/schema/base.rb, line 248
def validate_class(instance, &block)
  case instance.data
  when Integer
    validate_integer(instance, &block)
  when Numeric
    validate_number(instance, &block)
  when String
    validate_string(instance, &block)
  when Array
    validate_array(instance, &block)
  when Hash
    validate_object(instance, &block)
  end
end
validate_custom_format(instance, custom_format) { |error(instance, 'format')| ... } click to toggle source
# File lib/json_schemer/schema/base.rb, line 329
def validate_custom_format(instance, custom_format)
  yield error(instance, 'format') if custom_format != false && !custom_format.call(instance.data, instance.schema)
end
validate_exclusive_maximum(instance, exclusive_maximum, maximum) { |error(instance, 'exclusiveMaximum')| ... } click to toggle source
# File lib/json_schemer/schema/base.rb, line 333
def validate_exclusive_maximum(instance, exclusive_maximum, maximum)
  yield error(instance, 'exclusiveMaximum') if instance.data >= exclusive_maximum
end
validate_exclusive_minimum(instance, exclusive_minimum, minimum) { |error(instance, 'exclusiveMinimum')| ... } click to toggle source
# File lib/json_schemer/schema/base.rb, line 337
def validate_exclusive_minimum(instance, exclusive_minimum, minimum)
  yield error(instance, 'exclusiveMinimum') if instance.data <= exclusive_minimum
end
validate_integer(instance) { |error(instance, 'integer')| ... } click to toggle source
# File lib/json_schemer/schema/base.rb, line 372
def validate_integer(instance, &block)
  data = instance.data

  if !data.is_a?(Numeric) || (!data.is_a?(Integer) && data.floor != data)
    yield error(instance, 'integer')
    return
  end

  validate_numeric(instance, &block)
end
validate_number(instance) { |error(instance, 'number')| ... } click to toggle source
# File lib/json_schemer/schema/base.rb, line 363
def validate_number(instance, &block)
  unless instance.data.is_a?(Numeric)
    yield error(instance, 'number')
    return
  end

  validate_numeric(instance, &block)
end
validate_numeric(instance) { |error(instance, 'maximum')| ... } click to toggle source
# File lib/json_schemer/schema/base.rb, line 341
def validate_numeric(instance, &block)
  schema = instance.schema
  data = instance.data

  multiple_of = schema['multipleOf']
  maximum = schema['maximum']
  exclusive_maximum = schema['exclusiveMaximum']
  minimum = schema['minimum']
  exclusive_minimum = schema['exclusiveMinimum']

  yield error(instance, 'maximum') if maximum && data > maximum
  yield error(instance, 'minimum') if minimum && data < minimum

  validate_exclusive_maximum(instance, exclusive_maximum, maximum, &block) if exclusive_maximum
  validate_exclusive_minimum(instance, exclusive_minimum, minimum, &block) if exclusive_minimum

  if multiple_of
    quotient = data / multiple_of.to_f
    yield error(instance, 'multipleOf') unless quotient.floor == quotient
  end
end
validate_object(instance) { |error(instance, 'object')| ... } click to toggle source
# File lib/json_schemer/schema/base.rb, line 486
def validate_object(instance, &block)
  data = instance.data

  unless data.is_a?(Hash)
    yield error(instance, 'object')
    return
  end

  schema = instance.schema

  max_properties = schema['maxProperties']
  min_properties = schema['minProperties']
  required = schema['required']
  properties = schema['properties']
  pattern_properties = schema['patternProperties']
  additional_properties = schema['additionalProperties']
  dependencies = schema['dependencies']
  property_names = schema['propertyNames']

  if instance.before_property_validation && properties
    properties.each do |property, property_schema|
      instance.before_property_validation.each do |hook|
        hook.call(data, property, property_schema, schema)
      end
    end
  end

  if dependencies
    dependencies.each do |key, value|
      next unless data.key?(key)
      subschema = value.is_a?(Array) ? { 'required' => value } : value
      subinstance = instance.merge(schema: subschema, schema_pointer: "#{instance.schema_pointer}/dependencies/#{key}")
      validate_instance(subinstance, &block)
    end
  end

  yield error(instance, 'maxProperties') if max_properties && data.size > max_properties
  yield error(instance, 'minProperties') if min_properties && data.size < min_properties
  if required
    missing_keys = required - data.keys
    yield error(instance, 'required', 'missing_keys' => missing_keys) if missing_keys.any?
  end

  regex_pattern_properties = nil
  data.each do |key, value|
    unless property_names.nil?
      subinstance = instance.merge(
        data: key,
        schema: property_names,
        schema_pointer: "#{instance.schema_pointer}/propertyNames"
      )
      validate_instance(subinstance, &block)
    end

    matched_key = false

    if properties && properties.key?(key)
      subinstance = instance.merge(
        data: value,
        data_pointer: "#{instance.data_pointer}/#{key}",
        schema: properties[key],
        schema_pointer: "#{instance.schema_pointer}/properties/#{key}"
      )
      validate_instance(subinstance, &block)
      matched_key = true
    end

    if pattern_properties
      regex_pattern_properties ||= pattern_properties.map do |pattern, property_schema|
        [pattern, ecma_262_regex(pattern), property_schema]
      end
      regex_pattern_properties.each do |pattern, regex, property_schema|
        if regex.match?(key)
          subinstance = instance.merge(
            data: value,
            data_pointer: "#{instance.data_pointer}/#{key}",
            schema: property_schema,
            schema_pointer: "#{instance.schema_pointer}/patternProperties/#{pattern}"
          )
          validate_instance(subinstance, &block)
          matched_key = true
        end
      end
    end

    next if matched_key

    unless additional_properties.nil?
      subinstance = instance.merge(
        data: value,
        data_pointer: "#{instance.data_pointer}/#{key}",
        schema: additional_properties,
        schema_pointer: "#{instance.schema_pointer}/additionalProperties"
      )
      validate_instance(subinstance, &block)
    end
  end

  if instance.after_property_validation && properties
    properties.each do |property, property_schema|
      instance.after_property_validation.each do |hook|
        hook.call(data, property, property_schema, schema)
      end
    end
  end
end
validate_ref(instance, ref, &block) click to toggle source
# File lib/json_schemer/schema/base.rb, line 282
def validate_ref(instance, ref, &block)
  if ref.start_with?('#')
    schema_pointer = ref.slice(1..-1)
    if valid_json_pointer?(schema_pointer)
      ref_pointer = Hana::Pointer.new(URI.decode_www_form_component(schema_pointer))
      subinstance = instance.merge(
        schema: ref_pointer.eval(root),
        schema_pointer: schema_pointer,
        parent_uri: (pointer_uri(root, ref_pointer) || instance.parent_uri)
      )
      validate_instance(subinstance, &block)
      return
    end
  end

  ref_uri = join_uri(instance.parent_uri, ref)

  if valid_json_pointer?(ref_uri.fragment)
    ref_pointer = Hana::Pointer.new(URI.decode_www_form_component(ref_uri.fragment))
    ref_root = resolve_ref(ref_uri)
    ref_object = child(ref_root)
    subinstance = instance.merge(
      schema: ref_pointer.eval(ref_root),
      schema_pointer: ref_uri.fragment,
      parent_uri: (pointer_uri(ref_root, ref_pointer) || ref_uri)
    )
    ref_object.validate_instance(subinstance, &block)
  elsif id = ids[ref_uri.to_s]
    subinstance = instance.merge(
      schema: id.fetch(:schema),
      schema_pointer: id.fetch(:pointer),
      parent_uri: ref_uri
    )
    validate_instance(subinstance, &block)
  else
    ref_root = resolve_ref(ref_uri)
    ref_object = child(ref_root)
    id = ref_object.ids[ref_uri.to_s] || { schema: ref_root, pointer: '' }
    subinstance = instance.merge(
      schema: id.fetch(:schema),
      schema_pointer: id.fetch(:pointer),
      parent_uri: ref_uri
    )
    ref_object.validate_instance(subinstance, &block)
  end
end
validate_string(instance) { |error(instance, 'string')| ... } click to toggle source
# File lib/json_schemer/schema/base.rb, line 383
def validate_string(instance, &block)
  data = instance.data

  unless data.is_a?(String)
    yield error(instance, 'string')
    return
  end

  schema = instance.schema

  max_length = schema['maxLength']
  min_length = schema['minLength']
  pattern = schema['pattern']
  format = schema['format']
  content_encoding = schema['contentEncoding']
  content_media_type = schema['contentMediaType']

  yield error(instance, 'maxLength') if max_length && data.size > max_length
  yield error(instance, 'minLength') if min_length && data.size < min_length
  yield error(instance, 'pattern') if pattern && ecma_262_regex(pattern) !~ data
  yield error(instance, 'format') if format? && spec_format?(format) && !valid_spec_format?(data, format)

  if content_encoding || content_media_type
    decoded_data = data

    if content_encoding
      decoded_data = case content_encoding.downcase
      when 'base64'
        safe_strict_decode64(data)
      else # '7bit', '8bit', 'binary', 'quoted-printable'
        raise NotImplementedError
      end
      yield error(instance, 'contentEncoding') unless decoded_data
    end

    if content_media_type && decoded_data
      case content_media_type.downcase
      when 'application/json'
        yield error(instance, 'contentMediaType') unless valid_json?(decoded_data)
      else
        raise NotImplementedError
      end
    end
  end
end
validate_type(instance, type) { |error(instance, 'null')| ... } click to toggle source
# File lib/json_schemer/schema/base.rb, line 263
def validate_type(instance, type, &block)
  case type
  when 'null'
    yield error(instance, 'null') unless instance.data.nil?
  when 'boolean'
    yield error(instance, 'boolean') unless BOOLEANS.include?(instance.data)
  when 'number'
    validate_number(instance, &block)
  when 'integer'
    validate_integer(instance, &block)
  when 'string'
    validate_string(instance, &block)
  when 'array'
    validate_array(instance, &block)
  when 'object'
    validate_object(instance, &block)
  end
end