From 5fdc0a3b972c561aa2fd7d13bad26e2f4d95957a Mon Sep 17 00:00:00 2001 From: Marica Odagaki Date: Thu, 23 Nov 2017 19:07:58 -0800 Subject: [PATCH 01/15] Extract a Reference class --- lib/open_api_parser.rb | 1 + lib/open_api_parser/document.rb | 29 ++++----------- lib/open_api_parser/reference.rb | 62 ++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 23 deletions(-) create mode 100644 lib/open_api_parser/reference.rb diff --git a/lib/open_api_parser.rb b/lib/open_api_parser.rb index 5cf4b44..4c5800a 100644 --- a/lib/open_api_parser.rb +++ b/lib/open_api_parser.rb @@ -8,6 +8,7 @@ require "open_api_parser/document" require "open_api_parser/file_cache" require "open_api_parser/pointer" +require "open_api_parser/reference" require "open_api_parser/specification" require "open_api_parser/specification/endpoint" require "open_api_parser/specification/root" diff --git a/lib/open_api_parser/document.rb b/lib/open_api_parser/document.rb index 6084b38..dcfbb52 100644 --- a/lib/open_api_parser/document.rb +++ b/lib/open_api_parser/document.rb @@ -35,34 +35,17 @@ def deeply_expand_refs(fragment, cur_path) def expand_refs(fragment, cur_path) if fragment.is_a?(Hash) && fragment.key?("$ref") - ref = fragment["$ref"] - - if ref.start_with?("file:") - expand_file(ref) + raw_uri = fragment["$ref"] + ref = OpenApiParser::Reference.new(raw_uri) + fully_resolved = ref.resolve(@path, cur_path, @content, @file_cache) + unless fully_resolved + expand_refs(ref.referrent_document, ref.referrent_pointer) else - expand_pointer(ref, cur_path) + [ref.referrent_document, ref.referrent_pointer] end else [fragment, cur_path] end end - - def expand_file(ref) - relative_path = ref.split(":").last - absolute_path = File.expand_path(File.join("..", relative_path), @path) - - Document.resolve(absolute_path, @file_cache) - end - - def expand_pointer(ref, cur_path) - pointer = OpenApiParser::Pointer.new(ref) - - if pointer.exists_in_path?(cur_path) - { "$ref" => ref } - else - fragment = pointer.resolve(@content) - expand_refs(fragment, cur_path + pointer.escaped_pointer) - end - end end end diff --git a/lib/open_api_parser/reference.rb b/lib/open_api_parser/reference.rb new file mode 100644 index 0000000..9755c95 --- /dev/null +++ b/lib/open_api_parser/reference.rb @@ -0,0 +1,62 @@ +module OpenApiParser + # Responsible for interpreting a $ref value and + # resolving it to a raw specification given a base URI. + class Reference + # The resolved document. This gets set only after calling `#resolve`. + attr_reader :referrent_document + + # Pointer of the referrent_document if it's embedded in a larger document. + # This gets set only after calling `#resolve`. + # Empty string means the whole document. + attr_reader :referrent_pointer + + def initialize(raw_uri) + @raw_uri = raw_uri + @resolved = false + end + + # Sets referrent_document and referrent_pointer to the resolved + # raw specification and pointer, respectively. + # + # @return [Boolean] Whether the referrent has been fully expanded. + def resolve(base_path, base_pointer, current_document, file_cache) + if @resolved + fail 'Do not try to resolve an already resolved reference.' + end + @resolved = true + if @raw_uri.start_with?("file:") + expand_file(@raw_uri, base_path, file_cache) + else + expand_pointer(@raw_uri, base_pointer, current_document) + end + end + + private + + # @return [Boolean] Whether the referrent has been fully expanded. + def expand_file(raw_uri, base_path, file_cache) + relative_path = raw_uri.split(":").last + absolute_path = File.expand_path(File.join("..", relative_path), base_path) + + @referrent_document = OpenApiParser::Document.resolve(absolute_path, file_cache) + @referrent_pointer = '' + true + end + + # @return [Boolean] Whether the referrent has been fully expanded. + def expand_pointer(raw_uri, base_pointer, current_document) + pointer = OpenApiParser::Pointer.new(raw_uri) + + if pointer.exists_in_path?(base_pointer) + @referrent_document = { "$ref" => raw_uri } + # @referrent_document is unchanged; pointer stays the same + @referrent_pointer = base_pointer + true + else + @referrent_document = pointer.resolve(current_document) + @referrent_pointer = base_pointer + pointer.escaped_pointer + false + end + end + end +end From 3fe733d42c7e4557b82e6c02d9a08cf169d728cd Mon Sep 17 00:00:00 2001 From: Marica Odagaki Date: Thu, 23 Nov 2017 19:57:36 -0800 Subject: [PATCH 02/15] Allow $ref without a scheme Also supports reference to an external file with a pointer. --- lib/open_api_parser/reference.rb | 62 +++++++++++++-------- spec/open_api_parser/document_spec.rb | 6 ++ spec/resources/file_reference_example.yaml | 2 + spec/resources/mixed_reference_example.yaml | 2 + 4 files changed, 50 insertions(+), 22 deletions(-) diff --git a/lib/open_api_parser/reference.rb b/lib/open_api_parser/reference.rb index 9755c95..a039f53 100644 --- a/lib/open_api_parser/reference.rb +++ b/lib/open_api_parser/reference.rb @@ -24,38 +24,56 @@ def resolve(base_path, base_pointer, current_document, file_cache) fail 'Do not try to resolve an already resolved reference.' end @resolved = true - if @raw_uri.start_with?("file:") - expand_file(@raw_uri, base_path, file_cache) - else - expand_pointer(@raw_uri, base_pointer, current_document) - end + + ref_uri = Addressable::URI.parse(@raw_uri) + + referenced_document, base_pointer = + case ref_uri.scheme + when nil, 'file' + if ref_uri.path.empty? + [current_document, base_pointer] + else + [resolve_file(ref_uri.path, base_path, file_cache), ''] + end + else + fail "$ref with scheme #{ref_uri.scheme} is not supported" + end + + fully_expanded, @referrent_document, @referrent_pointer = + if !ref_uri.fragment.nil? && ref_uri.fragment != '' + resolve_pointer(ref_uri.fragment, base_pointer, referenced_document) + else + [true, referenced_document, ''] + end + + fully_expanded end private - # @return [Boolean] Whether the referrent has been fully expanded. - def expand_file(raw_uri, base_path, file_cache) - relative_path = raw_uri.split(":").last - absolute_path = File.expand_path(File.join("..", relative_path), base_path) + # @return [Hash] Resolved raw document + def resolve_file(path, base_path, file_cache) + absolute_path = File.expand_path(File.join("..", path), base_path) - @referrent_document = OpenApiParser::Document.resolve(absolute_path, file_cache) - @referrent_pointer = '' - true + OpenApiParser::Document.resolve(absolute_path, file_cache) end - # @return [Boolean] Whether the referrent has been fully expanded. - def expand_pointer(raw_uri, base_pointer, current_document) - pointer = OpenApiParser::Pointer.new(raw_uri) + # @return [Array] + # Whether the referrent has been fully expanded, resolved document, and pointer. + def resolve_pointer(raw_pointer, base_pointer, current_document) + pointer = OpenApiParser::Pointer.new(raw_pointer) if pointer.exists_in_path?(base_pointer) - @referrent_document = { "$ref" => raw_uri } - # @referrent_document is unchanged; pointer stays the same - @referrent_pointer = base_pointer - true + # prevent infinite recursion + referrent_document = { "$ref" => '#' + raw_pointer } + # referrent_document is simply a new $ref object pointing + # at the same fragment; pointer to the document stays the same, + # i.e. base_pointer. + [true, referrent_document, base_pointer] else - @referrent_document = pointer.resolve(current_document) - @referrent_pointer = base_pointer + pointer.escaped_pointer - false + referrent_document = pointer.resolve(current_document) + referrent_pointer = base_pointer + pointer.escaped_pointer + [false, referrent_document, referrent_pointer] end end end diff --git a/spec/open_api_parser/document_spec.rb b/spec/open_api_parser/document_spec.rb index c66ed1a..0fad583 100644 --- a/spec/open_api_parser/document_spec.rb +++ b/spec/open_api_parser/document_spec.rb @@ -19,6 +19,10 @@ expect(json["person"]).to eq({ "name" => "Drew" }) + + expect(json["person_without_scheme"]).to eq({ + "name" => "Drew" + }) end it "resolves a mix of pointers and file references" do @@ -32,6 +36,8 @@ expect(json["person"]["stats"]).to eq({ "age" => 34 }) + + expect(json["person_greeting"]).to eq("Drew") end end diff --git a/spec/resources/file_reference_example.yaml b/spec/resources/file_reference_example.yaml index be35a4d..11c5a86 100644 --- a/spec/resources/file_reference_example.yaml +++ b/spec/resources/file_reference_example.yaml @@ -1,2 +1,4 @@ person: $ref: "file:nested/person.yaml" +person_without_scheme: + $ref: "nested/person.yaml" diff --git a/spec/resources/mixed_reference_example.yaml b/spec/resources/mixed_reference_example.yaml index 179e7a8..54aeb47 100644 --- a/spec/resources/mixed_reference_example.yaml +++ b/spec/resources/mixed_reference_example.yaml @@ -1,2 +1,4 @@ person: $ref: "file:nested/mixed_person.yaml" +person_greeting: + $ref: "file:nested/mixed_person.yaml#/greeting/hi" From cbbdf43cd876175fdb50e2b1e22d6b08671f167d Mon Sep 17 00:00:00 2001 From: Marica Odagaki Date: Thu, 23 Nov 2017 21:26:45 -0800 Subject: [PATCH 03/15] Pointer should unescape even when there's no leading pound sign --- lib/open_api_parser/pointer.rb | 15 ++++++++++----- spec/open_api_parser/pointer_spec.rb | 26 ++++++++++++++------------ 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/lib/open_api_parser/pointer.rb b/lib/open_api_parser/pointer.rb index a467245..31aa29b 100644 --- a/lib/open_api_parser/pointer.rb +++ b/lib/open_api_parser/pointer.rb @@ -1,5 +1,8 @@ module OpenApiParser + # Responsible for interpreting the fragment portion of a $ref value + # as a JSON Pointer and resolving it within a given document. class Pointer + # @param raw_pointer [String] This can be both with and without a leading '#'. def initialize(raw_pointer) @raw_pointer = raw_pointer end @@ -17,11 +20,13 @@ def exists_in_path?(path) end def escaped_pointer - if @raw_pointer.start_with?("#") - Addressable::URI.unencode(@raw_pointer[1..-1]) - else - @raw_pointer - end + fragment = + if @raw_pointer.start_with?("#") + @raw_pointer[1..-1] + else + @raw_pointer + end + Addressable::URI.unencode(fragment) end private diff --git a/spec/open_api_parser/pointer_spec.rb b/spec/open_api_parser/pointer_spec.rb index fdcf796..4739a8e 100644 --- a/spec/open_api_parser/pointer_spec.rb +++ b/spec/open_api_parser/pointer_spec.rb @@ -33,27 +33,29 @@ resolutions.each do |pointer, expected| expect(OpenApiParser::Pointer.new(pointer).resolve(DOCUMENT)).to eq(expected) + expect(OpenApiParser::Pointer.new('#' + pointer).resolve(DOCUMENT)).to eq(expected) end end it "works with escaped RFC examples" do resolutions = { - "#" => DOCUMENT, - "#/foo" => ["bar", "baz"], - "#/foo/0" => "bar", - "#/" => 0, - "#/a~1b" => 1, - "#/c%25d" => 2, - "#/e%5Ef" => 3, - "#/g%7Ch" => 4, - "#/i%5Cj" => 5, - "#/k%22l" => 6, - "#/%20" => 7, - "#/m~0n" => 8, + "" => DOCUMENT, + "/foo" => ["bar", "baz"], + "/foo/0" => "bar", + "/" => 0, + "/a~1b" => 1, + "/c%25d" => 2, + "/e%5Ef" => 3, + "/g%7Ch" => 4, + "/i%5Cj" => 5, + "/k%22l" => 6, + "/%20" => 7, + "/m~0n" => 8, } resolutions.each do |pointer, expected| expect(OpenApiParser::Pointer.new(pointer).resolve(DOCUMENT)).to eq(expected) + expect(OpenApiParser::Pointer.new('#' + pointer).resolve(DOCUMENT)).to eq(expected) end end end From 6818a4dd707b28127245a99eefadb21132a83818 Mon Sep 17 00:00:00 2001 From: Marica Odagaki Date: Sat, 25 Nov 2017 19:31:02 -0800 Subject: [PATCH 04/15] Add spec for Reference --- spec/open_api_parser/reference_spec.rb | 162 +++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 spec/open_api_parser/reference_spec.rb diff --git a/spec/open_api_parser/reference_spec.rb b/spec/open_api_parser/reference_spec.rb new file mode 100644 index 0000000..7f23f4a --- /dev/null +++ b/spec/open_api_parser/reference_spec.rb @@ -0,0 +1,162 @@ +require "spec_helper" + +RSpec.describe OpenApiParser::Reference do + let(:file_cache) { OpenApiParser::FileCache.new } + + module PathHelpers + def cwd_relative(path_relative_to_project_root) + abs_path = Pathname.new(absolute(path_relative_to_project_root)) + abs_path.relative_path_from(Pathname.pwd).to_s + end + + def absolute(path_relative_to_project_root) + File.join(project_root, path_relative_to_project_root) + end + + def project_root + @project_root ||= File.expand_path(File.join('..', '..', '..'), __FILE__) + end + end + # Make helpers available in `describe` blocks + extend PathHelpers + # Make helpers available in `it` blocks + include PathHelpers + + describe "#resolve" do + it "cannot be called twice" do + ref = OpenApiParser::Reference.new('') + resolve = -> { ref.resolve('', '', {}, file_cache) } + resolve.call + expect do + resolve.call + end.to raise_error(/already resolved/) + end + + describe "supported schemes" do + it "interprets an empty scheme as a file path" do + ref = OpenApiParser::Reference.new('nested/person.yaml') + ref.resolve(cwd_relative("spec/resources/valid_spec.yaml"), '', {}, file_cache) + expect(ref.referrent_document).to eq({"name" => "Drew"}) + end + + it "does not support URI schemes other than file" do + ref = OpenApiParser::Reference.new('http://example.com/') + expect do + ref.resolve('', '', {}, file_cache) + end.to raise_error(/scheme http is not supported/) + end + end + + # document to use as the current document + STANDARD_DOCUMENT = { + "foo" => "bar", + "base_pointer" => "boo", + } + + context "given an invalid base uri" do + it "raises an error" do + ref = OpenApiParser::Reference.new(cwd_relative('spec/resources/nested/person.yaml')) + expect do + ref.resolve("http:", '', {}, file_cache) + end.to raise_error(Addressable::URI::InvalidURIError) + end + end + + context "given a non-existent base uri" do + it "does not check for base uri's existence" do + ref = OpenApiParser::Reference.new('nested/person.yaml') + bad_base_path = cwd_relative("spec/resources/this-should-never-exist.lmay") + ref.resolve(bad_base_path, '', {}, file_cache) + expect(ref.referrent_document).to eq({"name" => "Drew"}) + end + end + + context "given a non-existent ref path" do + it "raises an error" do + ref = OpenApiParser::Reference.new('nested/this-should-never-exist.lmay') + expect do + ref.resolve(cwd_relative("spec/resources/valid_spec.yaml"), '', {}, file_cache) + end.to raise_error(Errno::ENOENT) + end + end + + describe "path resolution" do + [ + ["nested/person.yaml", cwd_relative("spec/resources/valid_spec.yaml")], + ["nested/person.yaml", absolute("spec/resources/valid_spec.yaml")], + [absolute("spec/resources/nested/person.yaml"), cwd_relative("spec/resources/valid_spec.yaml")], + [absolute("spec/resources/nested/person.yaml"), absolute("spec/resources/valid_spec.yaml")], + ].each do |ref_uri,base_uri| + context "given $ref #{ref_uri} and base_uri #{base_uri}" do + it "resolves successfully" do + ref = OpenApiParser::Reference.new(ref_uri) + ref.resolve(base_uri, '', {}, file_cache) + expect(ref.referrent_document).to eq({"name" => "Drew"}) + end + end + end + end + + describe "pointer resolution" do + context "given a $ref with an empty path" do + let(:document) { STANDARD_DOCUMENT } + let(:base_path) { cwd_relative("spec/resources/standard.yaml") } + let(:ref_path) { "" } + [ + # base pointer, ref pointer, expected referrent doc, expected referrent pointer + ["", "", STANDARD_DOCUMENT, ""], + ["", "#/foo", "bar", "/foo"], + ["", "#/base_pointer", "boo", "/base_pointer"], + ["/base_pointer", "", STANDARD_DOCUMENT, ""], + ["/base_pointer", "#/foo", "bar", "/foo"], + ["/base_pointer", "#/base_pointer", {"$ref" => "#/base_pointer"}, "/base_pointer"], + ].each do |base_pointer,ref_pointer,expected_doc,expected_pointer| + it "resolves '#{ref_pointer}' as expected when base pointer is '#{base_pointer}'" do + ref_uri = ref_path + ref_pointer + ref = OpenApiParser::Reference.new(ref_uri) + ref.resolve(base_path, base_pointer, document, file_cache) + + expect(ref.referrent_document).to eq(expected_doc) + expect(ref.referrent_pointer).to eq(expected_pointer) + end + end + + it "raises an error if referrent fragment does not exist" do + ref = OpenApiParser::Reference.new('#/non-existent-token') + expect do + ref.resolve('', '', document, file_cache) + end.to raise_error(KeyError) + end + end + + context "given a $ref whose path is different than the base_uri" do + let(:document) { STANDARD_DOCUMENT } + let(:base_path) { cwd_relative("spec/resources/standard.yaml") } + let(:ref_path) { "another_standard.yaml" } + + before do + expect(YAML).to( + receive(:load_file).with(cwd_relative("spec/resources/another_standard.yaml")).and_return(document)) + end + [ + # base pointer, ref pointer, expected referrent doc, expected referrent pointer + ["", "", STANDARD_DOCUMENT, ""], + ["", "#/foo", "bar", "/foo"], + ["", "#/base_pointer", "boo", "/base_pointer"], + ["/base_pointer", "", STANDARD_DOCUMENT, ""], + ["/base_pointer", "#/foo", "bar", "/foo"], + ["/base_pointer", "#/base_pointer", "boo", "/base_pointer"], + ].each do |base_pointer,ref_pointer,expected_doc,expected_pointer| + it "resolves '#{ref_pointer}' as expected when base pointer is '#{base_pointer}'" do + ref_uri = ref_path + ref_pointer + ref = OpenApiParser::Reference.new(ref_uri) + ref.resolve(base_path, base_pointer, document, file_cache) + + expect(ref.referrent_document).to eq(expected_doc) + expect(ref.referrent_pointer).to eq(expected_pointer) + end + end + end + end + end +end From c808008306d446a9a39eb8352e17d600c80c0411 Mon Sep 17 00:00:00 2001 From: Marica Odagaki Date: Sat, 25 Nov 2017 19:35:39 -0800 Subject: [PATCH 05/15] Recursion detection was getting triggered on partial matches --- lib/open_api_parser/pointer.rb | 9 +++++-- lib/open_api_parser/reference.rb | 2 +- spec/open_api_parser/reference_spec.rb | 33 ++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/lib/open_api_parser/pointer.rb b/lib/open_api_parser/pointer.rb index 31aa29b..3928eaa 100644 --- a/lib/open_api_parser/pointer.rb +++ b/lib/open_api_parser/pointer.rb @@ -15,8 +15,13 @@ def resolve(document) end end - def exists_in_path?(path) - path.include?(escaped_pointer) + # Is the other pointer either the same as this one or a descendant? + def equal_or_ancestor_of?(other_pointer) + other_tokens = OpenApiParser::Pointer.new(other_pointer).escaped_pointer.split("/") + self_tokens = escaped_pointer.split("/") + perhaps_common_prefix = other_tokens[0...self_tokens.length] + # if the common prefix equals myself, I'm an ancestor of the other pointer + perhaps_common_prefix == self_tokens end def escaped_pointer diff --git a/lib/open_api_parser/reference.rb b/lib/open_api_parser/reference.rb index a039f53..0bf7ca5 100644 --- a/lib/open_api_parser/reference.rb +++ b/lib/open_api_parser/reference.rb @@ -63,7 +63,7 @@ def resolve_file(path, base_path, file_cache) def resolve_pointer(raw_pointer, base_pointer, current_document) pointer = OpenApiParser::Pointer.new(raw_pointer) - if pointer.exists_in_path?(base_pointer) + if pointer.equal_or_ancestor_of?(base_pointer) # prevent infinite recursion referrent_document = { "$ref" => '#' + raw_pointer } # referrent_document is simply a new $ref object pointing diff --git a/spec/open_api_parser/reference_spec.rb b/spec/open_api_parser/reference_spec.rb index 7f23f4a..baf5073 100644 --- a/spec/open_api_parser/reference_spec.rb +++ b/spec/open_api_parser/reference_spec.rb @@ -98,6 +98,39 @@ def project_root end describe "pointer resolution" do + context "given a deeply nested document" do + let(:base_path) { cwd_relative("spec/resources/standard.yaml") } + let(:ref_path) { "" } + let(:document) { + { + "base" => "hello", + "parent" => { + "base" => { + }, + "b" => "parent b", + "base-2" => "parent base-2", + } + } + } + [ + # base pointer, ref pointer, expected referrent doc, expected referrent pointer + ["/parent/base", "#/parent", {"$ref" => "#/parent"}, "/parent/base"], + ["/parent/base", "#/base", "hello", "/base"], + ["/parent/base", "#/parent/b", "parent b", "/parent/b"], + ["/parent/base", "#/parent/base", {"$ref" => "#/parent/base"}, "/parent/base"], + ["/parent/base", "#/parent/base-2", "parent base-2", "/parent/base-2"], + ].each do |base_pointer,ref_pointer,expected_doc,expected_pointer| + it "resolves '#{ref_pointer}' as expected when base pointer is '#{base_pointer}'" do + ref_uri = ref_path + ref_pointer + ref = OpenApiParser::Reference.new(ref_uri) + ref.resolve(base_path, base_pointer, document, file_cache) + + expect(ref.referrent_document).to eq(expected_doc) + expect(ref.referrent_pointer).to eq(expected_pointer) + end + end + end + context "given a $ref with an empty path" do let(:document) { STANDARD_DOCUMENT } let(:base_path) { cwd_relative("spec/resources/standard.yaml") } From d169526667140074752753c9faab774368955e93 Mon Sep 17 00:00:00 2001 From: Marica Odagaki Date: Sat, 25 Nov 2017 19:39:14 -0800 Subject: [PATCH 06/15] Fully expanded or not depends on if the current document is reused --- lib/open_api_parser/reference.rb | 20 +++++--- spec/open_api_parser/reference_spec.rb | 68 ++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 8 deletions(-) diff --git a/lib/open_api_parser/reference.rb b/lib/open_api_parser/reference.rb index 0bf7ca5..bdf4d59 100644 --- a/lib/open_api_parser/reference.rb +++ b/lib/open_api_parser/reference.rb @@ -27,13 +27,13 @@ def resolve(base_path, base_pointer, current_document, file_cache) ref_uri = Addressable::URI.parse(@raw_uri) - referenced_document, base_pointer = + fully_expanded, referenced_document, base_pointer = case ref_uri.scheme when nil, 'file' if ref_uri.path.empty? - [current_document, base_pointer] + [false, current_document, base_pointer] else - [resolve_file(ref_uri.path, base_path, file_cache), ''] + [true, resolve_file(ref_uri.path, base_path, file_cache), ''] end else fail "$ref with scheme #{ref_uri.scheme} is not supported" @@ -41,9 +41,9 @@ def resolve(base_path, base_pointer, current_document, file_cache) fully_expanded, @referrent_document, @referrent_pointer = if !ref_uri.fragment.nil? && ref_uri.fragment != '' - resolve_pointer(ref_uri.fragment, base_pointer, referenced_document) + resolve_pointer(ref_uri.fragment, base_pointer, referenced_document, fully_expanded) else - [true, referenced_document, ''] + [fully_expanded, referenced_document, ''] end fully_expanded @@ -58,9 +58,13 @@ def resolve_file(path, base_path, file_cache) OpenApiParser::Document.resolve(absolute_path, file_cache) end + # @param raw_pointer [String] Pointer to resolve. + # @param base_pointer [String] The location of the $ref being resolved. + # This is empty if `within_document` is not the document where $ref is located. + # @param within_document [Hash] Document in which to evaluate the pointer. # @return [Array] # Whether the referrent has been fully expanded, resolved document, and pointer. - def resolve_pointer(raw_pointer, base_pointer, current_document) + def resolve_pointer(raw_pointer, base_pointer, within_document, fully_expanded) pointer = OpenApiParser::Pointer.new(raw_pointer) if pointer.equal_or_ancestor_of?(base_pointer) @@ -71,9 +75,9 @@ def resolve_pointer(raw_pointer, base_pointer, current_document) # i.e. base_pointer. [true, referrent_document, base_pointer] else - referrent_document = pointer.resolve(current_document) + referrent_document = pointer.resolve(within_document) referrent_pointer = base_pointer + pointer.escaped_pointer - [false, referrent_document, referrent_pointer] + [fully_expanded, referrent_document, referrent_pointer] end end end diff --git a/spec/open_api_parser/reference_spec.rb b/spec/open_api_parser/reference_spec.rb index baf5073..fd7de1f 100644 --- a/spec/open_api_parser/reference_spec.rb +++ b/spec/open_api_parser/reference_spec.rb @@ -191,5 +191,73 @@ def project_root end end end + + describe 'its return value' do + let(:document) { STANDARD_DOCUMENT } + + context "given an empty ref path" do + let(:base_path) { cwd_relative('spec/resources/standard.yaml') } + let(:ref_path) { '' } + + [ + # base pointer, ref pointer, expected + ['', '', false], + ['', '#/foo', false], + ['/base_pointer', '', false], + ['/base_pointer', '#/foo', false], + ['/base_pointer', '#/base_pointer', true], + ].each do |base_pointer,ref_pointer,expected| + it "is #{expected} when $ref pointer is '#{ref_pointer}' and base pointer is '#{base_pointer}'" do + ref_uri = ref_path + ref_pointer + ref = OpenApiParser::Reference.new(ref_uri) + expect(ref.resolve(base_path, base_pointer, document, file_cache)).to be expected + end + end + end + + context "given a ref path same as the base path" do + let(:base_path) { cwd_relative('spec/resources/standard.yaml') } + let(:ref_path) { 'standard.yaml' } + + [ + # base pointer, ref pointer, expected + ['', '', false], + ['', '#/foo', false], + ['/base_pointer', '', false], + ['/base_pointer', '#/foo', false], + ['/base_pointer', '#/base_pointer', true], + ].each do |base_pointer,ref_pointer,expected| + it "is #{expected} when $ref pointer is '#{ref_pointer}' and base pointer is '#{base_pointer}'" do + ref_uri = ref_path + ref_pointer + ref = OpenApiParser::Reference.new(ref_uri) + expect(ref.resolve(base_path, base_pointer, document, file_cache)).to be expected + end + end + end + + context "given a ref path different than the base path" do + let(:base_path) { cwd_relative('spec/resources/standard.yaml') } + let(:ref_path) { 'another_standard.yaml' } + + before do + expect(YAML).to( + receive(:load_file).with(cwd_relative("spec/resources/another_standard.yaml")).and_return(document)) + end + [ + # base pointer, ref pointer, expected + ['', '', true], + ['', '#/foo', true], + ['/base_pointer', '', true], + ['/base_pointer', '#/foo', true], + ['/base_pointer', '#/base_pointer', true], + ].each do |base_pointer,ref_pointer,expected| + it "is #{expected} when $ref pointer is '#{ref_pointer}' and base pointer is '#{base_pointer}'" do + ref_uri = ref_path + ref_pointer + ref = OpenApiParser::Reference.new(ref_uri) + expect(ref.resolve(base_path, base_pointer, document, file_cache)).to be expected + end + end + end + end end end From 1fb90ef3f4c1dcc3140da04ff7a20ae71d7801c5 Mon Sep 17 00:00:00 2001 From: Marica Odagaki Date: Sat, 25 Nov 2017 19:39:57 -0800 Subject: [PATCH 07/15] Pointer of referrent is the pointer of $ref itself --- lib/open_api_parser/reference.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/open_api_parser/reference.rb b/lib/open_api_parser/reference.rb index bdf4d59..5c9fac2 100644 --- a/lib/open_api_parser/reference.rb +++ b/lib/open_api_parser/reference.rb @@ -76,7 +76,7 @@ def resolve_pointer(raw_pointer, base_pointer, within_document, fully_expanded) [true, referrent_document, base_pointer] else referrent_document = pointer.resolve(within_document) - referrent_pointer = base_pointer + pointer.escaped_pointer + referrent_pointer = pointer.escaped_pointer [fully_expanded, referrent_document, referrent_pointer] end end From 64f5808cf03f03cf8a90fd173652482f5fb0b4ab Mon Sep 17 00:00:00 2001 From: Marica Odagaki Date: Sat, 25 Nov 2017 19:52:04 -0800 Subject: [PATCH 08/15] If $ref is pointing at the same document, use the current document --- lib/open_api_parser/reference.rb | 20 ++++++++----- spec/open_api_parser/reference_spec.rb | 41 ++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/lib/open_api_parser/reference.rb b/lib/open_api_parser/reference.rb index 5c9fac2..606f104 100644 --- a/lib/open_api_parser/reference.rb +++ b/lib/open_api_parser/reference.rb @@ -18,6 +18,10 @@ def initialize(raw_uri) # Sets referrent_document and referrent_pointer to the resolved # raw specification and pointer, respectively. # + # @param base_path [String] Location of the document where the $ref originates. + # @param base_pointer [String] Location of the $ref within the document. + # @param current_document [Hash] Document where the $ref originates. + # @param file_cache [OpenApiParser::FileCache] File cache instance. # @return [Boolean] Whether the referrent has been fully expanded. def resolve(base_path, base_pointer, current_document, file_cache) if @resolved @@ -26,14 +30,16 @@ def resolve(base_path, base_pointer, current_document, file_cache) @resolved = true ref_uri = Addressable::URI.parse(@raw_uri) + base_uri = Addressable::URI.parse(base_path).omit(:fragment).normalize + resolved_uri = base_uri.join(ref_uri).omit(:fragment).normalize fully_expanded, referenced_document, base_pointer = - case ref_uri.scheme + case resolved_uri.scheme when nil, 'file' - if ref_uri.path.empty? + if base_uri == resolved_uri [false, current_document, base_pointer] else - [true, resolve_file(ref_uri.path, base_path, file_cache), ''] + [true, resolve_file(resolved_uri, file_cache), ''] end else fail "$ref with scheme #{ref_uri.scheme} is not supported" @@ -51,11 +57,11 @@ def resolve(base_path, base_pointer, current_document, file_cache) private + # @param resolved_uri [Addressable::URI] URI of the referenced document. + # @param file_cache [OpenApiParser::FileCache] File cache instance. # @return [Hash] Resolved raw document - def resolve_file(path, base_path, file_cache) - absolute_path = File.expand_path(File.join("..", path), base_path) - - OpenApiParser::Document.resolve(absolute_path, file_cache) + def resolve_file(resolved_uri, file_cache) + OpenApiParser::Document.resolve(resolved_uri.path, file_cache) end # @param raw_pointer [String] Pointer to resolve. diff --git a/spec/open_api_parser/reference_spec.rb b/spec/open_api_parser/reference_spec.rb index fd7de1f..dcad610 100644 --- a/spec/open_api_parser/reference_spec.rb +++ b/spec/open_api_parser/reference_spec.rb @@ -82,6 +82,7 @@ def project_root describe "path resolution" do [ + # ref_uri, base_uri, expected ["nested/person.yaml", cwd_relative("spec/resources/valid_spec.yaml")], ["nested/person.yaml", absolute("spec/resources/valid_spec.yaml")], [absolute("spec/resources/nested/person.yaml"), cwd_relative("spec/resources/valid_spec.yaml")], @@ -95,6 +96,16 @@ def project_root end end end + + context "given a $ref path the same as the base path" do + it "reuses the current document" do + expect(YAML).to_not receive(:load) + document = {"current" => true} + ref = OpenApiParser::Reference.new('person.yaml') + ref.resolve('person.yaml', '', document, file_cache) + expect(ref.referrent_document).to eq(document) + end + end end describe "pointer resolution" do @@ -162,6 +173,36 @@ def project_root end end + context "given a $ref whose path is the same as base_uri" do + let(:document) { STANDARD_DOCUMENT } + let(:base_path) { cwd_relative("spec/resources/standard.yaml") } + let(:ref_path) { "standard.yaml" } + + before do + expect(YAML).to_not( + receive(:load_file).with(base_path)) + end + + [ + # base pointer, ref pointer, expected referrent doc, expected referrent pointer + ["", "", STANDARD_DOCUMENT, ""], + ["", "#/foo", "bar", "/foo"], + ["", "#/base_pointer", "boo", "/base_pointer"], + ["/base_pointer", "", STANDARD_DOCUMENT, ""], + ["/base_pointer", "#/foo", "bar", "/foo"], + ["/base_pointer", "#/base_pointer", {"$ref" => "#/base_pointer"}, "/base_pointer"], + ].each do |base_pointer,ref_pointer,expected_doc,expected_pointer| + it "resolves '#{ref_pointer}' as expected when base pointer is '#{base_pointer}'" do + ref_uri = ref_path + ref_pointer + ref = OpenApiParser::Reference.new(ref_uri) + ref.resolve(base_path, base_pointer, document, file_cache) + + expect(ref.referrent_document).to eq(expected_doc) + expect(ref.referrent_pointer).to eq(expected_pointer) + end + end + end + context "given a $ref whose path is different than the base_uri" do let(:document) { STANDARD_DOCUMENT } let(:base_path) { cwd_relative("spec/resources/standard.yaml") } From 4ead55594ab96a4a4a88f13b4411adf88dbc656e Mon Sep 17 00:00:00 2001 From: Marica Odagaki Date: Sat, 25 Nov 2017 19:53:01 -0800 Subject: [PATCH 09/15] Inline method now that it's only one line --- lib/open_api_parser/reference.rb | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/open_api_parser/reference.rb b/lib/open_api_parser/reference.rb index 606f104..73f5197 100644 --- a/lib/open_api_parser/reference.rb +++ b/lib/open_api_parser/reference.rb @@ -39,7 +39,7 @@ def resolve(base_path, base_pointer, current_document, file_cache) if base_uri == resolved_uri [false, current_document, base_pointer] else - [true, resolve_file(resolved_uri, file_cache), ''] + [true, OpenApiParser::Document.resolve(resolved_uri.path, file_cache), ''] end else fail "$ref with scheme #{ref_uri.scheme} is not supported" @@ -57,12 +57,6 @@ def resolve(base_path, base_pointer, current_document, file_cache) private - # @param resolved_uri [Addressable::URI] URI of the referenced document. - # @param file_cache [OpenApiParser::FileCache] File cache instance. - # @return [Hash] Resolved raw document - def resolve_file(resolved_uri, file_cache) - OpenApiParser::Document.resolve(resolved_uri.path, file_cache) - end # @param raw_pointer [String] Pointer to resolve. # @param base_pointer [String] The location of the $ref being resolved. From 5ba7ffb1d74e11a0f68d05ddbeda498cae0408fd Mon Sep 17 00:00:00 2001 From: Marica Odagaki Date: Sat, 25 Nov 2017 19:54:35 -0800 Subject: [PATCH 10/15] Support relative URI with an explicit file scheme --- lib/open_api_parser/reference.rb | 28 ++++++++++++++++++++++++-- spec/open_api_parser/reference_spec.rb | 12 +++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/lib/open_api_parser/reference.rb b/lib/open_api_parser/reference.rb index 73f5197..f9e12d7 100644 --- a/lib/open_api_parser/reference.rb +++ b/lib/open_api_parser/reference.rb @@ -29,8 +29,10 @@ def resolve(base_path, base_pointer, current_document, file_cache) end @resolved = true - ref_uri = Addressable::URI.parse(@raw_uri) - base_uri = Addressable::URI.parse(base_path).omit(:fragment).normalize + # ref_uri needs to be normalized before being joined, as normalization + # affects absolute/relativeness. + ref_uri = normalize_file_uri(Addressable::URI.parse(@raw_uri)) + base_uri = normalize_file_uri(Addressable::URI.parse(base_path)).omit(:fragment).normalize resolved_uri = base_uri.join(ref_uri).omit(:fragment).normalize fully_expanded, referenced_document, base_pointer = @@ -57,6 +59,28 @@ def resolve(base_path, base_pointer, current_document, file_cache) private + # Normalizes the given file URI so that when its `path` content is relative, + # the URI considers itself relative as well. + # + # @example + # >> uri = Addressable::URI.parse('file:person.yaml') + # >> uri.path + # => "person.yaml" + # >> uri.absolute? + # => true + # >> normalize_file_uri(uri).absolute? + # => false + # >> normalize_file_uri(uri).path + # => "person.yaml" + # @param uri [Addressable::URI] + # @return [Addressable::URI] + def normalize_file_uri(uri) + if uri.scheme == 'file' && uri.host.nil? + uri.merge(scheme: nil) + else + uri + end + end # @param raw_pointer [String] Pointer to resolve. # @param base_pointer [String] The location of the $ref being resolved. diff --git a/spec/open_api_parser/reference_spec.rb b/spec/open_api_parser/reference_spec.rb index dcad610..998203d 100644 --- a/spec/open_api_parser/reference_spec.rb +++ b/spec/open_api_parser/reference_spec.rb @@ -33,6 +33,18 @@ def project_root end describe "supported schemes" do + it "supports the file scheme with relative path" do + ref = OpenApiParser::Reference.new('file:nested/person.yaml') + ref.resolve(cwd_relative("spec/resources/valid_spec.yaml"), '', {}, file_cache) + expect(ref.referrent_document).to eq({"name" => "Drew"}) + end + + it "supports the file scheme with absolute path" do + ref = OpenApiParser::Reference.new('file:' + absolute('spec/resources/nested/person.yaml')) + ref.resolve(cwd_relative("spec/resources/valid_spec.yaml"), '', {}, file_cache) + expect(ref.referrent_document).to eq({"name" => "Drew"}) + end + it "interprets an empty scheme as a file path" do ref = OpenApiParser::Reference.new('nested/person.yaml') ref.resolve(cwd_relative("spec/resources/valid_spec.yaml"), '', {}, file_cache) From 16723da89c64b83ef8b2c6b21b2001dfaec3b3cf Mon Sep 17 00:00:00 2001 From: Marica Odagaki Date: Sat, 25 Nov 2017 22:10:08 -0800 Subject: [PATCH 11/15] Rename for consistency `path` could mean the path portion of a URI. --- lib/open_api_parser/document.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/open_api_parser/document.rb b/lib/open_api_parser/document.rb index dcfbb52..1ba7d3a 100644 --- a/lib/open_api_parser/document.rb +++ b/lib/open_api_parser/document.rb @@ -19,32 +19,32 @@ def resolve private - def deeply_expand_refs(fragment, cur_path) - fragment, cur_path = expand_refs(fragment, cur_path) + def deeply_expand_refs(fragment, current_pointer) + fragment, current_pointer = expand_refs(fragment, current_pointer) if fragment.is_a?(Hash) fragment.reduce({}) do |hash, (k, v)| - hash.merge(k => deeply_expand_refs(v, "#{cur_path}/#{k}")) + hash.merge(k => deeply_expand_refs(v, "#{current_pointer}/#{k}")) end elsif fragment.is_a?(Array) - fragment.map { |e| deeply_expand_refs(e, cur_path) } + fragment.map { |e| deeply_expand_refs(e, current_pointer) } else fragment end end - def expand_refs(fragment, cur_path) + def expand_refs(fragment, current_pointer) if fragment.is_a?(Hash) && fragment.key?("$ref") raw_uri = fragment["$ref"] ref = OpenApiParser::Reference.new(raw_uri) - fully_resolved = ref.resolve(@path, cur_path, @content, @file_cache) + fully_resolved = ref.resolve(@path, current_pointer, @content, @file_cache) unless fully_resolved expand_refs(ref.referrent_document, ref.referrent_pointer) else [ref.referrent_document, ref.referrent_pointer] end else - [fragment, cur_path] + [fragment, current_pointer] end end end From 9dcc0bd6c92c916d99ae6fa5a7b3838d8368220a Mon Sep 17 00:00:00 2001 From: Marica Odagaki Date: Wed, 13 Dec 2017 22:58:11 -0800 Subject: [PATCH 12/15] Remove class-level comments to be consistent with the rest of the code base --- lib/open_api_parser/pointer.rb | 2 -- lib/open_api_parser/reference.rb | 2 -- 2 files changed, 4 deletions(-) diff --git a/lib/open_api_parser/pointer.rb b/lib/open_api_parser/pointer.rb index 3928eaa..8c28d1e 100644 --- a/lib/open_api_parser/pointer.rb +++ b/lib/open_api_parser/pointer.rb @@ -1,6 +1,4 @@ module OpenApiParser - # Responsible for interpreting the fragment portion of a $ref value - # as a JSON Pointer and resolving it within a given document. class Pointer # @param raw_pointer [String] This can be both with and without a leading '#'. def initialize(raw_pointer) diff --git a/lib/open_api_parser/reference.rb b/lib/open_api_parser/reference.rb index f9e12d7..6a7df52 100644 --- a/lib/open_api_parser/reference.rb +++ b/lib/open_api_parser/reference.rb @@ -1,6 +1,4 @@ module OpenApiParser - # Responsible for interpreting a $ref value and - # resolving it to a raw specification given a base URI. class Reference # The resolved document. This gets set only after calling `#resolve`. attr_reader :referrent_document From 134f3f814c7cc9a61e8dfa109c21c680bba9b63b Mon Sep 17 00:00:00 2001 From: Marica Odagaki Date: Wed, 13 Dec 2017 23:22:11 -0800 Subject: [PATCH 13/15] Make the Reference object stateless and its #resolve method side-effect free --- lib/open_api_parser/document.rb | 7 +-- lib/open_api_parser/reference.rb | 39 ++++++--------- spec/open_api_parser/reference_spec.rb | 68 +++++++++++++------------- 3 files changed, 53 insertions(+), 61 deletions(-) diff --git a/lib/open_api_parser/document.rb b/lib/open_api_parser/document.rb index 1ba7d3a..6620044 100644 --- a/lib/open_api_parser/document.rb +++ b/lib/open_api_parser/document.rb @@ -37,11 +37,12 @@ def expand_refs(fragment, current_pointer) if fragment.is_a?(Hash) && fragment.key?("$ref") raw_uri = fragment["$ref"] ref = OpenApiParser::Reference.new(raw_uri) - fully_resolved = ref.resolve(@path, current_pointer, @content, @file_cache) + fully_resolved, referrent_document, referrent_pointer = + ref.resolve(@path, current_pointer, @content, @file_cache) unless fully_resolved - expand_refs(ref.referrent_document, ref.referrent_pointer) + expand_refs(referrent_document, referrent_pointer) else - [ref.referrent_document, ref.referrent_pointer] + [referrent_document, referrent_pointer] end else [fragment, current_pointer] diff --git a/lib/open_api_parser/reference.rb b/lib/open_api_parser/reference.rb index 6a7df52..5d620b7 100644 --- a/lib/open_api_parser/reference.rb +++ b/lib/open_api_parser/reference.rb @@ -1,32 +1,24 @@ module OpenApiParser class Reference - # The resolved document. This gets set only after calling `#resolve`. - attr_reader :referrent_document - - # Pointer of the referrent_document if it's embedded in a larger document. - # This gets set only after calling `#resolve`. - # Empty string means the whole document. - attr_reader :referrent_pointer - def initialize(raw_uri) @raw_uri = raw_uri - @resolved = false end - # Sets referrent_document and referrent_pointer to the resolved - # raw specification and pointer, respectively. + # Resolve this reference in the given context. + # + # Returns a three-element array of: + # + # - Whether the referrent has been fully expanded + # - The resolved document + # - Pointer of the document if it's embedded in a larger document. + # Empty string means the whole document. # # @param base_path [String] Location of the document where the $ref originates. # @param base_pointer [String] Location of the $ref within the document. # @param current_document [Hash] Document where the $ref originates. # @param file_cache [OpenApiParser::FileCache] File cache instance. - # @return [Boolean] Whether the referrent has been fully expanded. + # @return [Array] def resolve(base_path, base_pointer, current_document, file_cache) - if @resolved - fail 'Do not try to resolve an already resolved reference.' - end - @resolved = true - # ref_uri needs to be normalized before being joined, as normalization # affects absolute/relativeness. ref_uri = normalize_file_uri(Addressable::URI.parse(@raw_uri)) @@ -45,14 +37,11 @@ def resolve(base_path, base_pointer, current_document, file_cache) fail "$ref with scheme #{ref_uri.scheme} is not supported" end - fully_expanded, @referrent_document, @referrent_pointer = - if !ref_uri.fragment.nil? && ref_uri.fragment != '' - resolve_pointer(ref_uri.fragment, base_pointer, referenced_document, fully_expanded) - else - [fully_expanded, referenced_document, ''] - end - - fully_expanded + if !ref_uri.fragment.nil? && ref_uri.fragment != '' + resolve_pointer(ref_uri.fragment, base_pointer, referenced_document, fully_expanded) + else + [fully_expanded, referenced_document, ''] + end end private diff --git a/spec/open_api_parser/reference_spec.rb b/spec/open_api_parser/reference_spec.rb index 998203d..b3ed51f 100644 --- a/spec/open_api_parser/reference_spec.rb +++ b/spec/open_api_parser/reference_spec.rb @@ -23,32 +23,31 @@ def project_root include PathHelpers describe "#resolve" do - it "cannot be called twice" do + it "can be called repeatedly" do ref = OpenApiParser::Reference.new('') - resolve = -> { ref.resolve('', '', {}, file_cache) } - resolve.call expect do - resolve.call - end.to raise_error(/already resolved/) + ref.resolve("http:", '', {}, file_cache) + end.to raise_error(Addressable::URI::InvalidURIError) + expect(ref.resolve('', '', {}, file_cache)).to eq [false, {}, ""] end describe "supported schemes" do it "supports the file scheme with relative path" do ref = OpenApiParser::Reference.new('file:nested/person.yaml') - ref.resolve(cwd_relative("spec/resources/valid_spec.yaml"), '', {}, file_cache) - expect(ref.referrent_document).to eq({"name" => "Drew"}) + _, referrent_doc, _ = ref.resolve(cwd_relative("spec/resources/valid_spec.yaml"), '', {}, file_cache) + expect(referrent_doc).to eq({"name" => "Drew"}) end it "supports the file scheme with absolute path" do ref = OpenApiParser::Reference.new('file:' + absolute('spec/resources/nested/person.yaml')) - ref.resolve(cwd_relative("spec/resources/valid_spec.yaml"), '', {}, file_cache) - expect(ref.referrent_document).to eq({"name" => "Drew"}) + _, referrent_doc, _ = ref.resolve(cwd_relative("spec/resources/valid_spec.yaml"), '', {}, file_cache) + expect(referrent_doc).to eq({"name" => "Drew"}) end it "interprets an empty scheme as a file path" do ref = OpenApiParser::Reference.new('nested/person.yaml') - ref.resolve(cwd_relative("spec/resources/valid_spec.yaml"), '', {}, file_cache) - expect(ref.referrent_document).to eq({"name" => "Drew"}) + _, referrent_doc, _ = ref.resolve(cwd_relative("spec/resources/valid_spec.yaml"), '', {}, file_cache) + expect(referrent_doc).to eq({"name" => "Drew"}) end it "does not support URI schemes other than file" do @@ -78,8 +77,8 @@ def project_root it "does not check for base uri's existence" do ref = OpenApiParser::Reference.new('nested/person.yaml') bad_base_path = cwd_relative("spec/resources/this-should-never-exist.lmay") - ref.resolve(bad_base_path, '', {}, file_cache) - expect(ref.referrent_document).to eq({"name" => "Drew"}) + _, referrent_doc, _ = ref.resolve(bad_base_path, '', {}, file_cache) + expect(referrent_doc).to eq({"name" => "Drew"}) end end @@ -103,8 +102,8 @@ def project_root context "given $ref #{ref_uri} and base_uri #{base_uri}" do it "resolves successfully" do ref = OpenApiParser::Reference.new(ref_uri) - ref.resolve(base_uri, '', {}, file_cache) - expect(ref.referrent_document).to eq({"name" => "Drew"}) + _, referrent_doc, _ = ref.resolve(base_uri, '', {}, file_cache) + expect(referrent_doc).to eq({"name" => "Drew"}) end end end @@ -114,8 +113,8 @@ def project_root expect(YAML).to_not receive(:load) document = {"current" => true} ref = OpenApiParser::Reference.new('person.yaml') - ref.resolve('person.yaml', '', document, file_cache) - expect(ref.referrent_document).to eq(document) + _, referrent_doc, _ = ref.resolve('person.yaml', '', document, file_cache) + expect(referrent_doc).to eq(document) end end end @@ -146,10 +145,10 @@ def project_root it "resolves '#{ref_pointer}' as expected when base pointer is '#{base_pointer}'" do ref_uri = ref_path + ref_pointer ref = OpenApiParser::Reference.new(ref_uri) - ref.resolve(base_path, base_pointer, document, file_cache) + _, referrent_doc, referrent_pointer = ref.resolve(base_path, base_pointer, document, file_cache) - expect(ref.referrent_document).to eq(expected_doc) - expect(ref.referrent_pointer).to eq(expected_pointer) + expect(referrent_doc).to eq(expected_doc) + expect(referrent_pointer).to eq(expected_pointer) end end end @@ -170,10 +169,10 @@ def project_root it "resolves '#{ref_pointer}' as expected when base pointer is '#{base_pointer}'" do ref_uri = ref_path + ref_pointer ref = OpenApiParser::Reference.new(ref_uri) - ref.resolve(base_path, base_pointer, document, file_cache) + _, referrent_doc, referrent_pointer = ref.resolve(base_path, base_pointer, document, file_cache) - expect(ref.referrent_document).to eq(expected_doc) - expect(ref.referrent_pointer).to eq(expected_pointer) + expect(referrent_doc).to eq(expected_doc) + expect(referrent_pointer).to eq(expected_pointer) end end @@ -207,10 +206,10 @@ def project_root it "resolves '#{ref_pointer}' as expected when base pointer is '#{base_pointer}'" do ref_uri = ref_path + ref_pointer ref = OpenApiParser::Reference.new(ref_uri) - ref.resolve(base_path, base_pointer, document, file_cache) + _, referrent_doc, referrent_pointer = ref.resolve(base_path, base_pointer, document, file_cache) - expect(ref.referrent_document).to eq(expected_doc) - expect(ref.referrent_pointer).to eq(expected_pointer) + expect(referrent_doc).to eq(expected_doc) + expect(referrent_pointer).to eq(expected_pointer) end end end @@ -236,16 +235,16 @@ def project_root it "resolves '#{ref_pointer}' as expected when base pointer is '#{base_pointer}'" do ref_uri = ref_path + ref_pointer ref = OpenApiParser::Reference.new(ref_uri) - ref.resolve(base_path, base_pointer, document, file_cache) + _, referrent_doc, referrent_pointer = ref.resolve(base_path, base_pointer, document, file_cache) - expect(ref.referrent_document).to eq(expected_doc) - expect(ref.referrent_pointer).to eq(expected_pointer) + expect(referrent_doc).to eq(expected_doc) + expect(referrent_pointer).to eq(expected_pointer) end end end end - describe 'its return value' do + describe 'return value for fully_expanded' do let(:document) { STANDARD_DOCUMENT } context "given an empty ref path" do @@ -263,7 +262,8 @@ def project_root it "is #{expected} when $ref pointer is '#{ref_pointer}' and base pointer is '#{base_pointer}'" do ref_uri = ref_path + ref_pointer ref = OpenApiParser::Reference.new(ref_uri) - expect(ref.resolve(base_path, base_pointer, document, file_cache)).to be expected + fully_expanded, *_rest = ref.resolve(base_path, base_pointer, document, file_cache) + expect(fully_expanded).to be expected end end end @@ -283,7 +283,8 @@ def project_root it "is #{expected} when $ref pointer is '#{ref_pointer}' and base pointer is '#{base_pointer}'" do ref_uri = ref_path + ref_pointer ref = OpenApiParser::Reference.new(ref_uri) - expect(ref.resolve(base_path, base_pointer, document, file_cache)).to be expected + fully_expanded, *_rest = ref.resolve(base_path, base_pointer, document, file_cache) + expect(fully_expanded).to be expected end end end @@ -307,7 +308,8 @@ def project_root it "is #{expected} when $ref pointer is '#{ref_pointer}' and base pointer is '#{base_pointer}'" do ref_uri = ref_path + ref_pointer ref = OpenApiParser::Reference.new(ref_uri) - expect(ref.resolve(base_path, base_pointer, document, file_cache)).to be expected + fully_expanded, *_rest = ref.resolve(base_path, base_pointer, document, file_cache) + expect(fully_expanded).to be expected end end end From 1ea0e734bd772991e75c229987b3556929add579 Mon Sep 17 00:00:00 2001 From: Marica Odagaki Date: Wed, 13 Dec 2017 23:24:58 -0800 Subject: [PATCH 14/15] Prefer using the test cases from the spec verbatim --- spec/open_api_parser/pointer_spec.rb | 56 +++++++++++++++------------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/spec/open_api_parser/pointer_spec.rb b/spec/open_api_parser/pointer_spec.rb index 4739a8e..7de7a14 100644 --- a/spec/open_api_parser/pointer_spec.rb +++ b/spec/open_api_parser/pointer_spec.rb @@ -17,45 +17,49 @@ describe "resolve" do it "works with RFC examples" do resolutions = { - "" => DOCUMENT, - "/foo" => ["bar", "baz"], - "/foo/0" => "bar", - "/" => 0, - "/a~1b" => 1, - "/c%d" => 2, - "/e^f" => 3, - "/g|h" => 4, - "/i\\j" => 5, - "/k\"l" => 6, - "/ " => 7, - "/m~0n" => 8, + "#" => DOCUMENT, + "#/foo" => ["bar", "baz"], + "#/foo/0" => "bar", + "#/" => 0, + "#/a~1b" => 1, + "#/c%d" => 2, + "#/e^f" => 3, + "#/g|h" => 4, + "#/i\\j" => 5, + "#/k\"l" => 6, + "#/ " => 7, + "#/m~0n" => 8, } resolutions.each do |pointer, expected| + # with the pound sign expect(OpenApiParser::Pointer.new(pointer).resolve(DOCUMENT)).to eq(expected) - expect(OpenApiParser::Pointer.new('#' + pointer).resolve(DOCUMENT)).to eq(expected) + # without the pound sign + expect(OpenApiParser::Pointer.new(pointer[1..-1]).resolve(DOCUMENT)).to eq(expected) end end it "works with escaped RFC examples" do resolutions = { - "" => DOCUMENT, - "/foo" => ["bar", "baz"], - "/foo/0" => "bar", - "/" => 0, - "/a~1b" => 1, - "/c%25d" => 2, - "/e%5Ef" => 3, - "/g%7Ch" => 4, - "/i%5Cj" => 5, - "/k%22l" => 6, - "/%20" => 7, - "/m~0n" => 8, + "#" => DOCUMENT, + "#/foo" => ["bar", "baz"], + "#/foo/0" => "bar", + "#/" => 0, + "#/a~1b" => 1, + "#/c%25d" => 2, + "#/e%5Ef" => 3, + "#/g%7Ch" => 4, + "#/i%5Cj" => 5, + "#/k%22l" => 6, + "#/%20" => 7, + "#/m~0n" => 8, } resolutions.each do |pointer, expected| + # with the pound sign expect(OpenApiParser::Pointer.new(pointer).resolve(DOCUMENT)).to eq(expected) - expect(OpenApiParser::Pointer.new('#' + pointer).resolve(DOCUMENT)).to eq(expected) + # without the pound sign + expect(OpenApiParser::Pointer.new(pointer[1..-1]).resolve(DOCUMENT)).to eq(expected) end end end From 0989f6ac6b653f9d52b4d4467e83064083806ded Mon Sep 17 00:00:00 2001 From: Marica Odagaki Date: Thu, 14 Dec 2017 14:25:15 -0800 Subject: [PATCH 15/15] Remove all code comments to stay consistent with the rest of the code base --- lib/open_api_parser/pointer.rb | 3 -- lib/open_api_parser/reference.rb | 41 -------------------------- spec/open_api_parser/pointer_spec.rb | 4 --- spec/open_api_parser/reference_spec.rb | 11 ------- 4 files changed, 59 deletions(-) diff --git a/lib/open_api_parser/pointer.rb b/lib/open_api_parser/pointer.rb index 8c28d1e..dcbf4cc 100644 --- a/lib/open_api_parser/pointer.rb +++ b/lib/open_api_parser/pointer.rb @@ -1,6 +1,5 @@ module OpenApiParser class Pointer - # @param raw_pointer [String] This can be both with and without a leading '#'. def initialize(raw_pointer) @raw_pointer = raw_pointer end @@ -13,12 +12,10 @@ def resolve(document) end end - # Is the other pointer either the same as this one or a descendant? def equal_or_ancestor_of?(other_pointer) other_tokens = OpenApiParser::Pointer.new(other_pointer).escaped_pointer.split("/") self_tokens = escaped_pointer.split("/") perhaps_common_prefix = other_tokens[0...self_tokens.length] - # if the common prefix equals myself, I'm an ancestor of the other pointer perhaps_common_prefix == self_tokens end diff --git a/lib/open_api_parser/reference.rb b/lib/open_api_parser/reference.rb index 5d620b7..96b3f1b 100644 --- a/lib/open_api_parser/reference.rb +++ b/lib/open_api_parser/reference.rb @@ -4,23 +4,7 @@ def initialize(raw_uri) @raw_uri = raw_uri end - # Resolve this reference in the given context. - # - # Returns a three-element array of: - # - # - Whether the referrent has been fully expanded - # - The resolved document - # - Pointer of the document if it's embedded in a larger document. - # Empty string means the whole document. - # - # @param base_path [String] Location of the document where the $ref originates. - # @param base_pointer [String] Location of the $ref within the document. - # @param current_document [Hash] Document where the $ref originates. - # @param file_cache [OpenApiParser::FileCache] File cache instance. - # @return [Array] def resolve(base_path, base_pointer, current_document, file_cache) - # ref_uri needs to be normalized before being joined, as normalization - # affects absolute/relativeness. ref_uri = normalize_file_uri(Addressable::URI.parse(@raw_uri)) base_uri = normalize_file_uri(Addressable::URI.parse(base_path)).omit(:fragment).normalize resolved_uri = base_uri.join(ref_uri).omit(:fragment).normalize @@ -46,21 +30,6 @@ def resolve(base_path, base_pointer, current_document, file_cache) private - # Normalizes the given file URI so that when its `path` content is relative, - # the URI considers itself relative as well. - # - # @example - # >> uri = Addressable::URI.parse('file:person.yaml') - # >> uri.path - # => "person.yaml" - # >> uri.absolute? - # => true - # >> normalize_file_uri(uri).absolute? - # => false - # >> normalize_file_uri(uri).path - # => "person.yaml" - # @param uri [Addressable::URI] - # @return [Addressable::URI] def normalize_file_uri(uri) if uri.scheme == 'file' && uri.host.nil? uri.merge(scheme: nil) @@ -69,21 +38,11 @@ def normalize_file_uri(uri) end end - # @param raw_pointer [String] Pointer to resolve. - # @param base_pointer [String] The location of the $ref being resolved. - # This is empty if `within_document` is not the document where $ref is located. - # @param within_document [Hash] Document in which to evaluate the pointer. - # @return [Array] - # Whether the referrent has been fully expanded, resolved document, and pointer. def resolve_pointer(raw_pointer, base_pointer, within_document, fully_expanded) pointer = OpenApiParser::Pointer.new(raw_pointer) if pointer.equal_or_ancestor_of?(base_pointer) - # prevent infinite recursion referrent_document = { "$ref" => '#' + raw_pointer } - # referrent_document is simply a new $ref object pointing - # at the same fragment; pointer to the document stays the same, - # i.e. base_pointer. [true, referrent_document, base_pointer] else referrent_document = pointer.resolve(within_document) diff --git a/spec/open_api_parser/pointer_spec.rb b/spec/open_api_parser/pointer_spec.rb index 7de7a14..a1f821d 100644 --- a/spec/open_api_parser/pointer_spec.rb +++ b/spec/open_api_parser/pointer_spec.rb @@ -32,9 +32,7 @@ } resolutions.each do |pointer, expected| - # with the pound sign expect(OpenApiParser::Pointer.new(pointer).resolve(DOCUMENT)).to eq(expected) - # without the pound sign expect(OpenApiParser::Pointer.new(pointer[1..-1]).resolve(DOCUMENT)).to eq(expected) end end @@ -56,9 +54,7 @@ } resolutions.each do |pointer, expected| - # with the pound sign expect(OpenApiParser::Pointer.new(pointer).resolve(DOCUMENT)).to eq(expected) - # without the pound sign expect(OpenApiParser::Pointer.new(pointer[1..-1]).resolve(DOCUMENT)).to eq(expected) end end diff --git a/spec/open_api_parser/reference_spec.rb b/spec/open_api_parser/reference_spec.rb index b3ed51f..3a1cb77 100644 --- a/spec/open_api_parser/reference_spec.rb +++ b/spec/open_api_parser/reference_spec.rb @@ -17,9 +17,7 @@ def project_root @project_root ||= File.expand_path(File.join('..', '..', '..'), __FILE__) end end - # Make helpers available in `describe` blocks extend PathHelpers - # Make helpers available in `it` blocks include PathHelpers describe "#resolve" do @@ -58,7 +56,6 @@ def project_root end end - # document to use as the current document STANDARD_DOCUMENT = { "foo" => "bar", "base_pointer" => "boo", @@ -93,7 +90,6 @@ def project_root describe "path resolution" do [ - # ref_uri, base_uri, expected ["nested/person.yaml", cwd_relative("spec/resources/valid_spec.yaml")], ["nested/person.yaml", absolute("spec/resources/valid_spec.yaml")], [absolute("spec/resources/nested/person.yaml"), cwd_relative("spec/resources/valid_spec.yaml")], @@ -135,7 +131,6 @@ def project_root } } [ - # base pointer, ref pointer, expected referrent doc, expected referrent pointer ["/parent/base", "#/parent", {"$ref" => "#/parent"}, "/parent/base"], ["/parent/base", "#/base", "hello", "/base"], ["/parent/base", "#/parent/b", "parent b", "/parent/b"], @@ -158,7 +153,6 @@ def project_root let(:base_path) { cwd_relative("spec/resources/standard.yaml") } let(:ref_path) { "" } [ - # base pointer, ref pointer, expected referrent doc, expected referrent pointer ["", "", STANDARD_DOCUMENT, ""], ["", "#/foo", "bar", "/foo"], ["", "#/base_pointer", "boo", "/base_pointer"], @@ -195,7 +189,6 @@ def project_root end [ - # base pointer, ref pointer, expected referrent doc, expected referrent pointer ["", "", STANDARD_DOCUMENT, ""], ["", "#/foo", "bar", "/foo"], ["", "#/base_pointer", "boo", "/base_pointer"], @@ -224,7 +217,6 @@ def project_root receive(:load_file).with(cwd_relative("spec/resources/another_standard.yaml")).and_return(document)) end [ - # base pointer, ref pointer, expected referrent doc, expected referrent pointer ["", "", STANDARD_DOCUMENT, ""], ["", "#/foo", "bar", "/foo"], ["", "#/base_pointer", "boo", "/base_pointer"], @@ -252,7 +244,6 @@ def project_root let(:ref_path) { '' } [ - # base pointer, ref pointer, expected ['', '', false], ['', '#/foo', false], ['/base_pointer', '', false], @@ -273,7 +264,6 @@ def project_root let(:ref_path) { 'standard.yaml' } [ - # base pointer, ref pointer, expected ['', '', false], ['', '#/foo', false], ['/base_pointer', '', false], @@ -298,7 +288,6 @@ def project_root receive(:load_file).with(cwd_relative("spec/resources/another_standard.yaml")).and_return(document)) end [ - # base pointer, ref pointer, expected ['', '', true], ['', '#/foo', true], ['/base_pointer', '', true],