From c162c0a408aaf608424c44c1afaea11a9628e1cf Mon Sep 17 00:00:00 2001 From: Andreas Haller Date: Fri, 6 Dec 2024 16:16:47 +0100 Subject: [PATCH] Put file loading code inside FileLoader module --- lib/openapi_first.rb | 4 +- lib/openapi_first/builder.rb | 2 +- lib/openapi_first/file_loader.rb | 20 +++++++ lib/openapi_first/refs.rb | 61 --------------------- lib/openapi_first/resolved.rb | 12 ++-- spec/data/petstore.yml | 94 ++++++++++++++++++++++++++++++++ spec/file_loader_spec.rb | 24 ++++++++ spec/refs_spec.rb | 78 -------------------------- spec/resolved_spec.rb | 3 +- 9 files changed, 150 insertions(+), 148 deletions(-) create mode 100644 lib/openapi_first/file_loader.rb delete mode 100644 lib/openapi_first/refs.rb create mode 100644 spec/data/petstore.yml create mode 100644 spec/file_loader_spec.rb delete mode 100644 spec/refs_spec.rb diff --git a/lib/openapi_first.rb b/lib/openapi_first.rb index 278add67..2604465b 100644 --- a/lib/openapi_first.rb +++ b/lib/openapi_first.rb @@ -2,7 +2,7 @@ require 'yaml' require 'multi_json' -require_relative 'openapi_first/refs' +require_relative 'openapi_first/file_loader' require_relative 'openapi_first/errors' require_relative 'openapi_first/configuration' require_relative 'openapi_first/definition' @@ -53,7 +53,7 @@ def self.find_error_response(name) def self.load(filepath, only: nil, &) raise FileNotFoundError, "File not found: #{filepath}" unless File.exist?(filepath) - contents = Refs.load_file(filepath) + contents = FileLoader.load(filepath) parse(contents, only:, filepath:, &) end diff --git a/lib/openapi_first/builder.rb b/lib/openapi_first/builder.rb index fcc48c55..2b555a07 100644 --- a/lib/openapi_first/builder.rb +++ b/lib/openapi_first/builder.rb @@ -18,7 +18,7 @@ def self.build_router(contents, filepath:, config:) def initialize(contents, filepath:, config:) ref_resolver = JSONSchemer::CachedResolver.new do |uri| - Refs.load_file(File.join(File.dirname(filepath), uri.path)) + FileLoader.load(File.join(File.dirname(filepath), uri.path)) end configuration = JSONSchemer::Configuration.new( ref_resolver:, diff --git a/lib/openapi_first/file_loader.rb b/lib/openapi_first/file_loader.rb new file mode 100644 index 00000000..7b8f28dd --- /dev/null +++ b/lib/openapi_first/file_loader.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module OpenapiFirst + # Functions to handle $refs + # @!visibility private + module FileLoader + module_function + + def load(file_path) + raise FileNotFoundError, "File not found #{file_path}" unless File.exist?(file_path) + + body = File.read(file_path) + extname = File.extname(file_path) + return JSON.parse(body) if extname == '.json' + return YAML.unsafe_load(body) if ['.yaml', '.yml'].include?(extname) + + body + end + end +end diff --git a/lib/openapi_first/refs.rb b/lib/openapi_first/refs.rb deleted file mode 100644 index cd973049..00000000 --- a/lib/openapi_first/refs.rb +++ /dev/null @@ -1,61 +0,0 @@ -# frozen_string_literal: true - -module OpenapiFirst - # Functions to handle $refs - # @!visibility private - module Refs - module_function - - def resolve_file(file_path) - @files_cache ||= {} - @files_cache[File.expand_path(file_path)] ||= begin - data = load_file(file_path) - resolve_data!(data, context: data, dir: File.dirname(file_path)) - end - rescue FileNotFoundError => e - raise e.class, "Problem while loading file referenced in #{file_path}: #{e.message}" - end - - def load_file(file_path) - raise FileNotFoundError, "File not found #{file_path}" unless File.exist?(file_path) - - body = File.read(file_path) - extname = File.extname(file_path) - return JSON.parse(body) if extname == '.json' - return YAML.unsafe_load(body) if ['.yaml', '.yml'].include?(extname) - - body - end - - def resolve_data!(data, context:, dir:) - case data - when Hash - return data if data.key?('discriminator') - - if data.key?('$ref') - referenced_value = resolve_ref(data.delete('$ref'), context:, dir:) - data.merge!(referenced_value) if referenced_value.is_a?(Hash) - end - data.transform_values! do |value| - resolve_data!(value, context:, dir:) - end - when Array - data.map! do |value| - resolve_data!(value, context:, dir:) - end - end - data - end - - def resolve_ref(pointer, context:, dir:) - return Hana::Pointer.new(pointer[1..]).eval(context) if pointer.start_with?('#') - - file_path, file_pointer = pointer.split('#') - file_context = resolve_file(File.expand_path(file_path, dir)) - return file_context unless file_pointer - - data = Hana::Pointer.new(file_pointer).eval(file_context) - resolve_data!(data, context: file_context, dir: File.dirname(file_path)) - end - end -end diff --git a/lib/openapi_first/resolved.rb b/lib/openapi_first/resolved.rb index 3e6a50cb..1a2eabfa 100644 --- a/lib/openapi_first/resolved.rb +++ b/lib/openapi_first/resolved.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module OpenapiFirst # This is here to give easy access to resolved $refs # @visibility private @@ -15,10 +17,8 @@ def [](key) self.class.new(value, context:) end - def each - resolved.each do |key, value| - yield key, value - end + def each(&) + resolved.each(&) end def resolved @@ -27,9 +27,11 @@ def resolved elsif value.is_a?(Array) return value.map do |item| break item.resolved if item.is_a?(Resolved) + item end end + value end @@ -38,10 +40,10 @@ def resolved private attr_accessor :value private attr_accessor :context - def resolve_ref(pointer) value = Hana::Pointer.new(pointer[1..]).eval(context) raise "Unknown reference #{pointer} in #{context}" unless value + value end end diff --git a/spec/data/petstore.yml b/spec/data/petstore.yml new file mode 100644 index 00000000..5af22548 --- /dev/null +++ b/spec/data/petstore.yml @@ -0,0 +1,94 @@ +openapi: "3.0.0" +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +servers: + - url: http://petstore.swagger.io/v1 +paths: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int32 + responses: + '200': + description: A paged array of pets + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + $ref: "#/components/schemas/Pets" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + post: + summary: Create a pet + operationId: createPets + tags: + - pets + responses: + '201': + description: Null response + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /pets/{petId}: + get: + summary: Info for a specific pet + operationId: showPetById + tags: + - pets + parameters: + - name: petId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: string + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: "#/components/schemas/Pets" +components: + schemas: + Pets: + type: array + title: Pets + items: + $ref: "#/components/schemas/Pet" + Pet: + $ref: "./components/schemas/pet.yaml#/Pet" + Error: + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string diff --git a/spec/file_loader_spec.rb b/spec/file_loader_spec.rb new file mode 100644 index 00000000..e07ac2f0 --- /dev/null +++ b/spec/file_loader_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +RSpec.describe OpenapiFirst::FileLoader do + describe '.load' do + it 'loads .json' do + contents = described_class.load('./spec/data/petstore.json') + expect(contents['openapi']).to eq('3.0.0') + end + + it 'loads .yaml' do + contents = described_class.load('./spec/data/petstore.yaml') + expect(contents['openapi']).to eq('3.0.0') + end + + it 'loads .yml' do + contents = described_class.load('./spec/data/petstore.yml') + expect(contents['openapi']).to eq('3.0.0') + end + + it 'raises FileNotFoundError if file was not found' do + expect { described_class.load('./spec/data/unknown.yaml') }.to raise_error(OpenapiFirst::FileNotFoundError, 'File not found ./spec/data/unknown.yaml') + end + end +end diff --git a/spec/refs_spec.rb b/spec/refs_spec.rb deleted file mode 100644 index 208597a0..00000000 --- a/spec/refs_spec.rb +++ /dev/null @@ -1,78 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe OpenapiFirst::Refs do - describe '.resolve_file' do - it 'resolves refs' do - resolved = described_class.resolve_file('spec/data/train-travel-api/openapi.yaml') - expect(resolved.dig('paths', '/stations', 'get', 'responses', '200', 'headers', 'RateLimit', 'schema', 'type')).to eq('string') - end - - it 'resolves refs across files' do - resolved = described_class.resolve_file('spec/data/petstore.yaml') - pp resolved.dig('paths', '/pets', 'get', 'responses', '200', 'content', 'application/json').inspect - expect(resolved.dig('components', 'schemas', 'Pet', 'properties', 'name')).to eq({ 'type' => 'string' }) - expect(resolved.dig('paths', '/pets', 'get', 'responses', '200', 'content', 'application/json', 'schema', 'items', 'required')).to eq(%w[id name]) - end - end - - describe '.resolve_data!' do - it 'resolves refs in Hashes' do - data = { - '$definitions' => { - 'Thing' => { 'type' => 'object' } - }, - 'hash' => { - '$ref' => '#/$definitions/Thing' - } - } - resolved = described_class.resolve_data!(data, context: data, dir: '.') - expect(resolved).to eq({ - '$definitions' => { - 'Thing' => { 'type' => 'object' } - }, - 'hash' => { - # '$ref' => '#/$definitions/Thing', - 'type' => 'object' - } - }) - end - - it 'resolves refs in Arrays' do - data = { - '$definitions' => { - 'Thing' => { 'type' => 'object' } - }, - 'array' => [ - { - '$ref' => '#/$definitions/Thing' - }, - { - '$ref' => '#/$definitions/Thing' - } - ] - } - resolved = described_class.resolve_data!(data, context: data, dir: '.') - expect(resolved).to eq({ - '$definitions' => { - 'Thing' => { 'type' => 'object' } - }, - 'array' => [ - { - # '$ref' => '#/$definitions/Thing', - 'type' => 'object' - }, - { - # '$ref' => '#/$definitions/Thing', - 'type' => 'object' - } - ] - }) - end - - it 'returns the original object' do - data = '42' - resolved = described_class.resolve_data!(data, context: data, dir: '.') - expect(resolved).to be(data) - end - end -end diff --git a/spec/resolved_spec.rb b/spec/resolved_spec.rb index 8557dee7..ef14cf57 100644 --- a/spec/resolved_spec.rb +++ b/spec/resolved_spec.rb @@ -1,5 +1,6 @@ -require_relative '../lib/openapi_first/resolved' +# frozen_string_literal: true +require_relative '../lib/openapi_first/resolved' RSpec.describe OpenapiFirst::Resolved do let(:original_hash) do