# frozen_string_literal: true module JSI # A JSI::Schema::Ref is a reference to a schema identified by a URI, typically from # a `$ref` keyword of a schema. class Schema::Ref # @param ref [String] A reference URI - typically the `$ref` value of the ref_schema # @param ref_schema [JSI::Schema] A schema from which the reference originated. # # If the ref URI consists of only a fragment, it is resolved from the `ref_schema`'s # {Schema#schema_resource_root}. Otherwise the resource is found in the `ref_schema`'s # {SchemaAncestorNode#jsi_schema_registry #jsi_schema_registry} (and any fragment is resolved from there). # @param schema_registry [SchemaRegistry] The registry in which the resource this ref refers to will be found. # This should only be specified in the absence of a `ref_schema`. # If neither is specified, {JSI.schema_registry} is used. def initialize(ref, ref_schema: nil, schema_registry: nil) raise(ArgumentError, "ref is not a string") unless ref.respond_to?(:to_str) @ref = ref @ref_uri = Util.uri(ref) @ref_schema = ref_schema ? Schema.ensure_schema(ref_schema) : nil @schema_registry = schema_registry || (ref_schema ? ref_schema.jsi_schema_registry : JSI.schema_registry) @deref_schema = nil end # @return [String] attr_reader :ref # @return [Addressable::URI] attr_reader :ref_uri # @return [Schema, nil] attr_reader :ref_schema # @return [SchemaRegistry, nil] attr_reader(:schema_registry) # finds the schema this ref points to # @return [JSI::Schema] # @raise [JSI::Schema::NotASchemaError] when the thing this ref points to is not a schema # @raise [JSI::Schema::ReferenceError] when this reference cannot be resolved def deref_schema return @deref_schema if @deref_schema schema_resource_root = nil check_schema_resource_root = -> { unless schema_resource_root raise(Schema::ReferenceError, [ "cannot find schema by ref: #{ref}", ("from: #{ref_schema.pretty_inspect.chomp}" if ref_schema), ].compact.join("\n")) end } ref_uri_nofrag = ref_uri.merge(fragment: nil).freeze if ref_uri_nofrag.empty? unless ref_schema raise(Schema::ReferenceError, [ "cannot find schema by ref: #{ref}", "with no ref schema", ].join("\n")) end # the URI only consists of a fragment (or is empty). # for a fragment pointer, resolve using Schema#resource_root_subschema on the ref_schema. # for a fragment anchor, bootstrap does not support anchors; otherwise use the ref_schema's schema_resource_root. schema_resource_root = ref_schema.is_a?(MetaschemaNode::BootstrapSchema) ? nil : ref_schema.schema_resource_root resolve_fragment_ptr = ref_schema.method(:resource_root_subschema) else # find the schema_resource_root from the non-fragment URI. we will resolve any fragment, either pointer or anchor, from there. if ref_uri_nofrag.absolute? ref_abs_uri = ref_uri_nofrag elsif ref_schema && ref_schema.jsi_resource_ancestor_uri ref_abs_uri = ref_schema.jsi_resource_ancestor_uri.join(ref_uri_nofrag).freeze else ref_abs_uri = nil end if ref_abs_uri unless schema_registry raise(Schema::ReferenceError, [ "could not resolve remote ref with no schema_registry specified", "ref URI: #{ref_uri.to_s}", ("from: #{ref_schema.pretty_inspect.chomp}" if ref_schema), ].compact.join("\n")) end schema_resource_root = schema_registry.find(ref_abs_uri) end unless schema_resource_root # HAX for how google does refs and ids if ref_schema && ref_schema.jsi_document.respond_to?(:to_hash) && ref_schema.jsi_document['schemas'].respond_to?(:to_hash) ref_schema.jsi_document['schemas'].each do |k, v| if Addressable::URI.parse(v['id']) == ref_uri_nofrag schema_resource_root = ref_schema.resource_root_subschema(['schemas', k]) end end end end check_schema_resource_root.call if schema_resource_root.is_a?(Schema) resolve_fragment_ptr = schema_resource_root.method(:resource_root_subschema) else # Note: Schema#resource_root_subschema will reinstantiate nonschemas as schemas. # not implemented for remote refs when the schema_resource_root is not a schema. resolve_fragment_ptr = -> (ptr) { schema_resource_root.jsi_descendent_node(ptr) } end end fragment = ref_uri.fragment if fragment begin ptr_from_fragment = Ptr.from_fragment(fragment) rescue Ptr::PointerSyntaxError end end if ptr_from_fragment begin result_schema = resolve_fragment_ptr.call(ptr_from_fragment) rescue Ptr::ResolutionError raise(Schema::ReferenceError, [ "could not resolve pointer: #{ptr_from_fragment.pointer.inspect}", ("from: #{ref_schema.pretty_inspect.chomp}" if ref_schema), ("in schema resource root: #{schema_resource_root.pretty_inspect.chomp}" if schema_resource_root), ].compact.join("\n")) end elsif fragment.nil? check_schema_resource_root.call result_schema = schema_resource_root else check_schema_resource_root.call # find an anchor that resembles the fragment result_schemas = schema_resource_root.jsi_anchor_subschemas(fragment) if result_schemas.size == 1 result_schema = result_schemas.first elsif result_schemas.size == 0 raise(Schema::ReferenceError, [ "could not find schema by fragment: #{fragment.inspect}", "in schema resource root: #{schema_resource_root.pretty_inspect.chomp}", ].join("\n")) else raise(Schema::ReferenceError, [ "found multiple schemas for plain name fragment #{fragment.inspect}:", *result_schemas.map { |s| s.pretty_inspect.chomp }, ].join("\n")) end end Schema.ensure_schema(result_schema, msg: "object identified by uri #{ref} is not a schema:") return @deref_schema = result_schema end # @return [String] def inspect -%Q(\#<#{self.class.name} #{ref}>) end alias_method :to_s, :inspect # pretty-prints a representation of self to the given printer # @return [void] def pretty_print(q) q.text '#<' q.text self.class.name q.text ' ' q.text ref q.text '>' end # see {Util::Private::FingerprintHash} # @api private def jsi_fingerprint {class: self.class, ref: ref, ref_schema: ref_schema} end include Util::FingerprintHash end end