class JsonSchema::ReferenceExpander

Attributes

errors[RW]
store[RW]

Public Instance Methods

expand(schema, options = {}) click to toggle source
# File lib/json_schema/reference_expander.rb, line 8
def expand(schema, options = {})
  @errors       = []
  @local_store  = DocumentStore.new
  @schema       = schema
  @schema_paths = {}
  @store        = options[:store] || DocumentStore.new

  # If the given JSON schema is _just_ a JSON reference and nothing else,
  # short circuit the whole expansion process and return the result.
  if schema.reference && !schema.expanded?
    return dereference(schema, [])
  end

  @uri = URI.parse(schema.uri)

  @store.each do |uri, store_schema|
    build_schema_paths(uri, store_schema)
  end

  # we run #to_s on lookup for URIs; the #to_s of nil is ""
  build_schema_paths("", schema)

  traverse_schema(schema)

  refs = unresolved_refs(schema).sort
  if refs.count > 0
    message = %{Couldn't resolve references: #{refs.to_a.join(", ")}.}
    @errors << SchemaError.new(schema, message, :unresolved_references)
  end

  @errors.count == 0
end
expand!(schema, options = {}) click to toggle source
# File lib/json_schema/reference_expander.rb, line 41
def expand!(schema, options = {})
  if !expand(schema, options)
    raise AggregateError.new(@errors)
  end
  true
end

Private Instance Methods

add_reference(schema) click to toggle source
# File lib/json_schema/reference_expander.rb, line 50
def add_reference(schema)
  uri = URI.parse(schema.uri)

  # In case we've already added a schema for the same reference, don't
  # re-add it unless the new schema's pointer path is shorter than the one
  # we've already stored.
  stored_schema = lookup_reference(uri)
  if stored_schema && stored_schema.pointer.length < schema.pointer.length
    return
  end

  if uri.absolute?
    @store.add_schema(schema)
  else
    @local_store.add_schema(schema)
  end
end
build_schema_paths(uri, schema) click to toggle source
# File lib/json_schema/reference_expander.rb, line 68
def build_schema_paths(uri, schema)
  return if schema.reference

  paths = @schema_paths[uri] ||= {}
  paths[schema.pointer] = schema

  schema_children(schema) do |subschema|
    build_schema_paths(uri, subschema)
  end

  # Also insert alternate tree for schema's custom URI. O(crazy).
  if schema.uri != uri
    fragment, parent = schema.fragment, schema.parent
    schema.fragment, schema.parent = "#", nil
    build_schema_paths(schema.uri, schema)
    schema.fragment, schema.parent = fragment, parent
  end
end
dereference(ref_schema, ref_stack, parent_ref: nil) click to toggle source
# File lib/json_schema/reference_expander.rb, line 87
def dereference(ref_schema, ref_stack, parent_ref: nil)
  ref = ref_schema.reference

  # Some schemas don't have a reference, but do
  # have children. If that's the case, we need to
  # dereference the subschemas.
  if !ref
    schema_children(ref_schema) do |subschema|
      next unless subschema.reference
      next if ref_schema.uri == parent_ref.uri.to_s

      if !subschema.reference.uri && parent_ref
        subschema.reference = JsonReference::Reference.new("#{parent_ref.uri}#{subschema.reference.pointer}")
      end

      dereference(subschema, ref_stack)
    end
    return true
  end

  # detects a reference cycle
  if ref_stack.include?(ref)
    message = %{Reference loop detected: #{ref_stack.sort.join(", ")}.}
    @errors << SchemaError.new(ref_schema, message, :loop_detected)
    return false
  end

  new_schema = resolve_reference(ref_schema)
  return false unless new_schema

  # if the reference resolved to a new reference we need to continue
  # dereferencing until we either hit a non-reference schema, or a
  # reference which is already resolved
  if new_schema.reference && !new_schema.expanded?
    success = dereference(new_schema, ref_stack + [ref])
    return false unless success
  end

  # If the reference schema is a global reference
  # then we'll need to manually expand any nested
  # references.
  if ref.uri
    schema_children(new_schema) do |subschema|
      # Don't bother if the subschema points to the same
      # schema as the reference schema.
      next if ref_schema == subschema

      if subschema.reference
        # If the subschema has a reference, then
        # we don't need to recurse if the schema is
        # already expanded.
        next if subschema.expanded?

        if !subschema.reference.uri
          # the subschema's ref is local to the file that the
          # subschema is in; however since there's no URI
          # the 'resolve_pointer' method would try to look it up
          # within @schema. So: manually reconstruct the reference to
          # use the URI of the parent ref.
          subschema.reference = JsonReference::Reference.new("#{ref.uri}#{subschema.reference.pointer}")
        end
      end

      if subschema.items && subschema.items.reference
        next if subschema.expanded?

        if !subschema.items.reference.uri
          # The subschema's ref is local to the file that the
          # subschema is in. Manually reconstruct the reference
          # so we can resolve it.
          subschema.items.reference = JsonReference::Reference.new("#{ref.uri}#{subschema.items.reference.pointer}")
        end
      end

      # If we're recursing into a schema via a global reference, then if
      # the current subschema doesn't have a reference, we have no way of
      # figuring out what schema we're in. The resolve_pointer method will
      # default to looking it up in the initial schema. Instead, we're
      # passing the parent ref here, so we can grab the URI
      # later if needed.
      dereference(subschema, ref_stack, parent_ref: ref)
    end
  end

  # copy new schema into existing one while preserving parent, fragment,
  # and reference
  parent = ref_schema.parent
  ref_schema.copy_from(new_schema)
  ref_schema.parent = parent

  # correct all parent references to point back to ref_schema instead of
  # new_schema
  if ref_schema.original?
    schema_children(ref_schema) do |schema|
      schema.parent = ref_schema
    end
  end

  true
end
lookup_pointer(uri, pointer) click to toggle source
# File lib/json_schema/reference_expander.rb, line 188
def lookup_pointer(uri, pointer)
  paths = @schema_paths[uri.to_s] ||= {}
  paths[pointer]
end
lookup_reference(uri) click to toggle source
# File lib/json_schema/reference_expander.rb, line 193
def lookup_reference(uri)
  if uri.absolute?
    @store.lookup_schema(uri.to_s)
  else
    @local_store.lookup_schema(uri.to_s)
  end
end
resolve_pointer(ref_schema, resolved_schema) click to toggle source
# File lib/json_schema/reference_expander.rb, line 201
def resolve_pointer(ref_schema, resolved_schema)
  ref = ref_schema.reference

  if !(new_schema = lookup_pointer(ref.uri, ref.pointer))
    new_schema = JsonPointer.evaluate(resolved_schema, ref.pointer)

    # couldn't resolve pointer within known schema; that's an error
    if new_schema.nil?
      message = %{Couldn't resolve pointer "#{ref.pointer}".}
      @errors << SchemaError.new(resolved_schema, message, :unresolved_pointer)
      return
    end

    # Try to aggressively detect a circular dependency in case of another
    # reference. See:
    #
    #     https://github.com/brandur/json_schema/issues/50
    #
    if new_schema.reference &&
      new_new_schema = lookup_pointer(ref.uri, new_schema.reference.pointer)
        new_new_schema.clones << ref_schema
    else
      # Parse a new schema and use the same parent node. Basically this is
      # exclusively for the case of a reference that needs to be
      # de-referenced again to be resolved.
      build_schema_paths(ref.uri, resolved_schema)
    end
  else
    # insert a clone record so that the expander knows to expand it when
    # the schema traversal is finished
    new_schema.clones << ref_schema
  end
  new_schema
end
resolve_reference(ref_schema) click to toggle source
# File lib/json_schema/reference_expander.rb, line 236
def resolve_reference(ref_schema)
  ref = ref_schema.reference
  uri = ref.uri

  if uri && uri.host
    scheme = uri.scheme || "http"
    # allow resolution if something we've already parsed has claimed the
    # full URL
    if @store.lookup_schema(uri.to_s)
      resolve_uri(ref_schema, uri)
    else
      message =
        %{Reference resolution over #{scheme} is not currently supported (URI: #{uri}).}
      @errors << SchemaError.new(ref_schema, message, :scheme_not_supported)
      nil
    end
  # absolute
  elsif uri && uri.path[0] == "/"
    resolve_uri(ref_schema, uri)
  # relative
  elsif uri
    # Build an absolute path using the URI of the current schema.
    #
    # Note that this code path will never currently be hit because the
    # incoming reference schema will never have a URI.
    if ref_schema.uri
      schema_uri = ref_schema.uri.chomp("/")
      resolve_uri(ref_schema, URI.parse(schema_uri + "/" + uri.path))
    else
      nil
    end

  # just a JSON Pointer -- resolve against schema root
  else
    resolve_pointer(ref_schema, @schema)
  end
end
resolve_uri(ref_schema, uri) click to toggle source
# File lib/json_schema/reference_expander.rb, line 274
def resolve_uri(ref_schema, uri)
  if schema = lookup_reference(uri)
    resolve_pointer(ref_schema, schema)
  else
    message = %{Couldn't resolve URI: #{uri.to_s}.}
    @errors << SchemaError.new(ref_schema, message, :unresolved_pointer)
    nil
  end
end
schema_children(schema) { |s| ... } click to toggle source
# File lib/json_schema/reference_expander.rb, line 284
def schema_children(schema)
  schema.all_of.each { |s| yield s }
  schema.any_of.each { |s| yield s }
  schema.one_of.each { |s| yield s }
  schema.definitions.each { |_, s| yield s }
  schema.pattern_properties.each { |_, s| yield s }
  schema.properties.each { |_, s| yield s }

  if additional = schema.additional_properties
    if additional.is_a?(Schema)
      yield additional
    end
  end

  if schema.not
    yield schema.not
  end

  # can either be a single schema (list validation) or multiple (tuple
  # validation)
  if items = schema.items
    if items.is_a?(Array)
      items.each { |s| yield s }
    else
      yield items
    end
  end

  # dependencies can either be simple or "schema"; only replace the
  # latter
  schema.dependencies.
    each_value { |s| yield s if s.is_a?(Schema) }

  # schemas contained inside hyper-schema links objects
  if schema.links
    schema.links.each { |l|
      yield l.schema if l.schema
      yield l.target_schema if l.target_schema
    }
  end
end
traverse_schema(schema) click to toggle source
# File lib/json_schema/reference_expander.rb, line 341
def traverse_schema(schema)
  add_reference(schema)

  schema_children(schema) do |subschema|
    if subschema.reference && !subschema.expanded?
      dereference(subschema, [])
    end

    if !subschema.reference
      traverse_schema(subschema)
    end
  end

  # after finishing a schema traversal, find all clones and re-hydrate them
  if schema.original?
    schema.clones.each do |clone_schema|
      parent = clone_schema.parent
      clone_schema.copy_from(schema)
      clone_schema.parent = parent
    end
  end
end
unresolved_refs(schema) click to toggle source
# File lib/json_schema/reference_expander.rb, line 326
def unresolved_refs(schema)
  # prevent endless recursion
  return [] unless schema.original?

  arr = []
  schema_children(schema) do |subschema|
    if !subschema.expanded?
      arr += [subschema.reference]
    else
      arr += unresolved_refs(subschema)
    end
  end
  arr
end