From f331e064fdaeb0809a703f212ca802693c3b74d8 Mon Sep 17 00:00:00 2001 From: Rutger Gelling Date: Tue, 28 Jan 2020 02:24:27 +0100 Subject: [PATCH] Add yaml support (#7) --- README.md | 4 +-- .../lib/open_api/rswag/api/middleware.rb | 30 +++++++++++++++++-- .../rswag/api/fixtures/swagger/v1/swagger.yml | 5 ++++ .../open_api/rswag/api/middleware_spec.rb | 15 ++++++++++ .../specs/install/templates/swagger_helper.rb | 6 ++++ rswag-specs/lib/open_api/rswag/specs.rb | 1 + .../lib/open_api/rswag/specs/configuration.rb | 8 +++++ .../open_api/rswag/specs/swagger_formatter.rb | 16 +++++++++- .../spec/rswag/specs/configuration_spec.rb | 25 +++++++++++++++- .../rswag/specs/swagger_formatter_spec.rb | 23 ++++++++++++-- .../rswag/ui/install/templates/rswag-ui.rb | 6 ++-- 11 files changed, 126 insertions(+), 13 deletions(-) create mode 100644 rswag-api/spec/open_api/rswag/api/fixtures/swagger/v1/swagger.yml diff --git a/README.md b/README.md index 7c89c30..fcfbf9c 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Currently, this is still a work in progress. It will output Open API 3.0 compati OpenApi Rswag creates [Swagger](http://swagger.io) tooling for Rails API's. Generate beautiful API documentation, including a UI to explore and test operations, directly from your rspec integration tests. -Rswag extends rspec-rails "request specs" with a Swagger-based DSL for describing and testing API operations. You describe your API operations with a succinct, intuitive syntax, and it automaticaly runs the tests. Once you have green tests, run a rake task to auto-generate corresponding Swagger files and expose them as JSON endpoints. Rswag also provides an embedded version of the awesome [swagger-ui](https://github.com/swagger-api/swagger-ui) that's powered by the exposed JSON. This toolchain makes it seamless to go from integration specs, which you're probably doing in some form already, to living documentation for your API consumers. +Rswag extends rspec-rails "request specs" with a Swagger-based DSL for describing and testing API operations. You describe your API operations with a succinct, intuitive syntax, and it automaticaly runs the tests. Once you have green tests, run a rake task to auto-generate corresponding Swagger files and expose them as YAML or JSON endpoints. Rswag also provides an embedded version of the awesome [swagger-ui](https://github.com/swagger-api/swagger-ui) that's powered by the exposed file. This toolchain makes it seamless to go from integration specs, which you're probably doing in some form already, to living documentation for your API consumers. And that's not all ... @@ -266,7 +266,7 @@ By default, the paths, operations and responses defined in your spec files will ```ruby # spec/integration/v2/blogs_spec.rb -describe 'Blogs API', swagger_doc: 'v2/swagger.json' do +describe 'Blogs API', swagger_doc: 'v2/swagger.yaml' do path '/blogs' do ... diff --git a/rswag-api/lib/open_api/rswag/api/middleware.rb b/rswag-api/lib/open_api/rswag/api/middleware.rb index d7f791b..8c12936 100644 --- a/rswag-api/lib/open_api/rswag/api/middleware.rb +++ b/rswag-api/lib/open_api/rswag/api/middleware.rb @@ -1,4 +1,6 @@ require 'json' +require 'yaml' +require 'rack/mime' module OpenApi module Rswag @@ -15,13 +17,15 @@ module OpenApi filename = "#{@config.resolve_swagger_root(env)}/#{path}" if env['REQUEST_METHOD'] == 'GET' && File.file?(filename) - swagger = load_json(filename) + swagger = parse_file(filename) @config.swagger_filter.call(swagger, env) unless @config.swagger_filter.nil? + mime = Rack::Mime.mime_type(::File.extname(path), 'text/plain') + body = unload_swagger(filename, swagger) return [ '200', - { 'Content-Type' => 'application/json' }, - [ JSON.dump(swagger) ] + { 'Content-Type' => 'mine' }, + [ body ] ] end @@ -30,9 +34,29 @@ module OpenApi private + def parse_file(filename) + if /\.ya?ml$/ === filename + load_yaml(filename) + else + load_json(filename) + end + end + + def load_yaml(filename) + YAML.safe_load(File.read(filename)) + end + def load_json(filename) JSON.parse(File.read(filename)) end + + def unload_swagger(filename, swagger) + if /\.ya?ml$/ === filename + YAML.dump(swagger) + else + JSON.dump(swagger) + end + end end end end diff --git a/rswag-api/spec/open_api/rswag/api/fixtures/swagger/v1/swagger.yml b/rswag-api/spec/open_api/rswag/api/fixtures/swagger/v1/swagger.yml new file mode 100644 index 0000000..9aca152 --- /dev/null +++ b/rswag-api/spec/open_api/rswag/api/fixtures/swagger/v1/swagger.yml @@ -0,0 +1,5 @@ +openapi: 3.0.0 +info: + title: API V1 + version: v1 +paths: {} diff --git a/rswag-api/spec/open_api/rswag/api/middleware_spec.rb b/rswag-api/spec/open_api/rswag/api/middleware_spec.rb index 3273c19..1ade658 100644 --- a/rswag-api/spec/open_api/rswag/api/middleware_spec.rb +++ b/rswag-api/spec/open_api/rswag/api/middleware_spec.rb @@ -76,6 +76,21 @@ module OpenApi::Rswag expect(response[2].join).to include('"host":"tempuri.org"') end end + + context 'when a path maps to a yaml swagger file' do + let(:env) { env_defaults.merge('PATH_INFO' => 'v1/swagger.yml') } + + it 'returns a 200 status' do + expect(response.length).to eql(3) + expect(response.first).to eql('200') + end + + it 'returns contents of the swagger file' do + expect(response.length).to eql(3) + expect(response[1]).to include( 'Content-Type' => 'text/yaml') + expect(response[2].join).to include('title: API V1') + end + end end end end diff --git a/rswag-specs/lib/generators/rswag/specs/install/templates/swagger_helper.rb b/rswag-specs/lib/generators/rswag/specs/install/templates/swagger_helper.rb index a5d4325..8b18f83 100644 --- a/rswag-specs/lib/generators/rswag/specs/install/templates/swagger_helper.rb +++ b/rswag-specs/lib/generators/rswag/specs/install/templates/swagger_helper.rb @@ -32,4 +32,10 @@ RSpec.configure do |config| ] } } + + # Specify the format of the output Swagger file when running 'rswag:specs:swaggerize'. + # The swagger_docs configuration option has the filename including format in + # the key, this may want to be changed to avoid putting yaml in json files. + # Defaults to json. Accepts ':json' and ':yaml'. + config.swagger_format = :yaml end diff --git a/rswag-specs/lib/open_api/rswag/specs.rb b/rswag-specs/lib/open_api/rswag/specs.rb index 8ede89a..6d60437 100644 --- a/rswag-specs/lib/open_api/rswag/specs.rb +++ b/rswag-specs/lib/open_api/rswag/specs.rb @@ -13,6 +13,7 @@ module OpenApi c.add_setting :swagger_root c.add_setting :swagger_docs c.add_setting :swagger_dry_run + c.add_setting :swagger_format c.extend ExampleGroupHelpers, type: :request c.include ExampleHelpers, type: :request end diff --git a/rswag-specs/lib/open_api/rswag/specs/configuration.rb b/rswag-specs/lib/open_api/rswag/specs/configuration.rb index b552f28..60274f2 100644 --- a/rswag-specs/lib/open_api/rswag/specs/configuration.rb +++ b/rswag-specs/lib/open_api/rswag/specs/configuration.rb @@ -34,6 +34,14 @@ module OpenApi end end + def swagger_format + @swagger_format ||= begin + @rspec_config.swagger_format = :json if @rspec_config.swagger_format.nil? || @rspec_config.swagger_format.empty? + raise ConfigurationError, "Unknown swagger_format '#{@rspec_config.swagger_format}'" unless [:json, :yaml].include?(@rspec_config.swagger_format) + @rspec_config.swagger_format + end + end + def get_swagger_doc(name) return swagger_docs.values.first if name.nil? raise ConfigurationError, "Unknown swagger_doc '#{name}'" unless swagger_docs[name] diff --git a/rswag-specs/lib/open_api/rswag/specs/swagger_formatter.rb b/rswag-specs/lib/open_api/rswag/specs/swagger_formatter.rb index ad0818a..006366e 100644 --- a/rswag-specs/lib/open_api/rswag/specs/swagger_formatter.rb +++ b/rswag-specs/lib/open_api/rswag/specs/swagger_formatter.rb @@ -60,7 +60,7 @@ module OpenApi FileUtils.mkdir_p dirname unless File.exist?(dirname) File.open(file_path, 'w') do |file| - file.write(JSON.pretty_generate(doc)) + file.write(pretty_generate(doc)) end @output.puts "Swagger doc generated at #{file_path}" @@ -69,6 +69,20 @@ module OpenApi private + def pretty_generate(doc) + if @config.swagger_format == :yaml + clean_doc = yaml_prepare(doc) + YAML.dump(clean_doc) + else # config errors are thrown in 'def swagger_format', no throw needed here + JSON.pretty_generate(doc) + end + end + + def yaml_prepare(doc) + json_doc = JSON.pretty_generate(doc) + JSON.parse(json_doc) + end + def metadata_to_swagger(metadata) response_code = metadata[:response][:code] response = metadata[:response].reject { |k, _v| k == :code } diff --git a/rswag-specs/spec/rswag/specs/configuration_spec.rb b/rswag-specs/spec/rswag/specs/configuration_spec.rb index 5791baf..a98f56d 100644 --- a/rswag-specs/spec/rswag/specs/configuration_spec.rb +++ b/rswag-specs/spec/rswag/specs/configuration_spec.rb @@ -8,7 +8,9 @@ module OpenApi describe Configuration do subject { described_class.new(rspec_config) } - let(:rspec_config) { OpenStruct.new(swagger_root: swagger_root, swagger_docs: swagger_docs) } + let(:rspec_config) do + OpenStruct.new(swagger_root: swagger_root, swagger_docs: swagger_docs, swagger_format: swagger_format) + end let(:swagger_root) { 'foobar' } let(:swagger_docs) do { @@ -16,6 +18,7 @@ module OpenApi 'v2/swagger.json' => { info: { title: 'v2' } } } end + let(:swagger_format) { :yaml } describe '#swagger_root' do let(:response) { subject.swagger_root } @@ -48,6 +51,26 @@ module OpenApi end end + describe '#swagger_format' do + let(:response) { subject.swagger_format } + + context 'provided in rspec config' do + it { expect(response).to be_an_instance_of(Symbol) } + end + + context 'unsupported format provided' do + let(:swagger_format) { :xml } + + it { expect { response }.to raise_error ConfigurationError } + end + + context 'not provided' do + let(:swagger_format) { nil } + + it { expect(response).to eq(:json) } + end + end + describe '#get_swagger_doc(tag=nil)' do let(:swagger_doc) { subject.get_swagger_doc(tag) } diff --git a/rswag-specs/spec/rswag/specs/swagger_formatter_spec.rb b/rswag-specs/spec/rswag/specs/swagger_formatter_spec.rb index ece4dc3..1c52e9f 100644 --- a/rswag-specs/spec/rswag/specs/swagger_formatter_spec.rb +++ b/rswag-specs/spec/rswag/specs/swagger_formatter_spec.rb @@ -55,14 +55,31 @@ module OpenApi 'v1/swagger.json' => { info: { version: 'v1' } }, 'v2/swagger.json' => { info: { version: 'v2' } } ) + allow(config).to receive(:swagger_format).and_return(swagger_format) subject.stop(notification) end let(:notification) { double('notification') } - it 'writes the swagger_doc(s) to file' do - expect(File).to exist("#{swagger_root}/v1/swagger.json") - expect(File).to exist("#{swagger_root}/v2/swagger.json") + context 'with default format' do + let(:swagger_format) { :json } + + it 'writes the swagger_doc(s) to file' do + expect(File).to exist("#{swagger_root}/v1/swagger.json") + expect(File).to exist("#{swagger_root}/v2/swagger.json") + expect { JSON.parse(File.read("#{swagger_root}/v2/swagger.json")) }.not_to raise_error + end + end + + context 'with yaml format' do + let(:swagger_format) { :yaml } + + it 'writes the swagger_doc(s) as yaml' do + expect(File).to exist("#{swagger_root}/v1/swagger.json") + expect { JSON.parse(File.read("#{swagger_root}/v1/swagger.json")) }.to raise_error(JSON::ParserError) + # Psych::DisallowedClass would be raised if we do not pre-process ruby symbols + expect { YAML.safe_load(File.read("#{swagger_root}/v1/swagger.json")) }.not_to raise_error + end end after do diff --git a/rswag-ui/lib/generators/rswag/ui/install/templates/rswag-ui.rb b/rswag-ui/lib/generators/rswag/ui/install/templates/rswag-ui.rb index 2fa7986..5a489ea 100644 --- a/rswag-ui/lib/generators/rswag/ui/install/templates/rswag-ui.rb +++ b/rswag-ui/lib/generators/rswag/ui/install/templates/rswag-ui.rb @@ -2,9 +2,9 @@ OpenApi::Rswag::Ui.configure do |c| # List the Swagger endpoints that you want to be documented through the swagger-ui # The first parameter is the path (absolute or relative to the UI host) to the corresponding - # JSON endpoint and the second is a title that will be displayed in the document selector - # NOTE: If you're using rspec-api to expose Swagger files (under swagger_root) as JSON endpoints, + # endpoint and the second is a title that will be displayed in the document selector + # NOTE: If you're using rspec-api to expose Swagger files (under swagger_root) as YAML or JSON endpoints, # then the list below should correspond to the relative paths for those endpoints - c.swagger_endpoint '/api-docs/v1/swagger.json', 'API V1 Docs' + c.swagger_endpoint '/api-docs/v1/swagger.yaml', 'API V1 Docs' end