From 73b84101cc7e229dd2d7d401547adb0708306a55 Mon Sep 17 00:00:00 2001 From: Greg Myers Date: Sat, 2 Nov 2019 10:45:35 +0000 Subject: [PATCH 1/9] Adding yaml as option for generator New installations will get :yaml as it's default with openapi 3 as the version. Old installations will have the key missing and will default to :json with an easy upgrade path. --- .../specs/install/templates/swagger_helper.rb | 10 ++++++-- rswag-specs/lib/rswag/specs.rb | 1 + rswag-specs/lib/rswag/specs/configuration.rb | 8 ++++++ .../spec/rswag/specs/configuration_spec.rb | 25 ++++++++++++++++++- 4 files changed, 41 insertions(+), 3 deletions(-) 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 3855920..327b2c8 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 @@ -13,8 +13,8 @@ RSpec.configure do |config| # document below. You can override this behavior by adding a swagger_doc tag to the # the root example_group in your specs, e.g. describe '...', swagger_doc: 'v2/swagger.json' config.swagger_docs = { - 'v1/swagger.json' => { - swagger: '2.0', + 'v1/swagger.yaml' => { + openapi: '3.0.1', info: { title: 'API V1', version: 'v1' @@ -22,4 +22,10 @@ RSpec.configure do |config| paths: {} } } + + # 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/rswag/specs.rb b/rswag-specs/lib/rswag/specs.rb index de29ce9..a3f0c16 100644 --- a/rswag-specs/lib/rswag/specs.rb +++ b/rswag-specs/lib/rswag/specs.rb @@ -12,6 +12,7 @@ module Rswag 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/rswag/specs/configuration.rb b/rswag-specs/lib/rswag/specs/configuration.rb index 4adf33c..4c6ee68 100644 --- a/rswag-specs/lib/rswag/specs/configuration.rb +++ b/rswag-specs/lib/rswag/specs/configuration.rb @@ -31,6 +31,14 @@ module Rswag 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/spec/rswag/specs/configuration_spec.rb b/rswag-specs/spec/rswag/specs/configuration_spec.rb index 30da333..e3aacdc 100644 --- a/rswag-specs/spec/rswag/specs/configuration_spec.rb +++ b/rswag-specs/spec/rswag/specs/configuration_spec.rb @@ -6,7 +6,9 @@ module Rswag RSpec.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 { @@ -14,6 +16,7 @@ module Rswag 'v2/swagger.json' => { info: { title: 'v2' } } } end + let(:swagger_format) { :yaml } describe '#swagger_root' do let(:response) { subject.swagger_root } @@ -46,6 +49,26 @@ module Rswag 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) } From 0e04635b156011b0d83a246e5038046d5dbb2c67 Mon Sep 17 00:00:00 2001 From: Greg Myers Date: Sat, 2 Nov 2019 11:19:01 +0000 Subject: [PATCH 2/9] Write the files using specified format --- .../lib/rswag/specs/swagger_formatter.rb | 10 +++++++++- .../rswag/specs/swagger_formatter_spec.rb | 20 ++++++++++++++++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/rswag-specs/lib/rswag/specs/swagger_formatter.rb b/rswag-specs/lib/rswag/specs/swagger_formatter.rb index 794a9d9..b8061dc 100644 --- a/rswag-specs/lib/rswag/specs/swagger_formatter.rb +++ b/rswag-specs/lib/rswag/specs/swagger_formatter.rb @@ -37,7 +37,7 @@ module Rswag FileUtils.mkdir_p dirname unless File.exists?(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}" @@ -46,6 +46,14 @@ module Rswag private + def pretty_generate(doc) + if @config.swagger_format == :yaml + YAML.dump(doc) + else # config errors are thrown in 'def swagger_format', no throw needed here + JSON.pretty_generate(doc) + end + 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/swagger_formatter_spec.rb b/rswag-specs/spec/rswag/specs/swagger_formatter_spec.rb index b5b7ad8..2a29cde 100644 --- a/rswag-specs/spec/rswag/specs/swagger_formatter_spec.rb +++ b/rswag-specs/spec/rswag/specs/swagger_formatter_spec.rb @@ -53,14 +53,28 @@ module Rswag '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') } + 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") + 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) + end end after do From 3ff1de5d65abc0ee4c52d5b8468b9f65320c7fd9 Mon Sep 17 00:00:00 2001 From: Greg Myers Date: Sat, 2 Nov 2019 11:40:07 +0000 Subject: [PATCH 3/9] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa57b8b..bb1a888 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- New swagger_format config option for setting YAML output [#251](https://github.com/rswag/rswag/pull/251) ### Changed ### Deprecated ### Removed From acab437a7d36c3b8c75851c9eae4790f87386da3 Mon Sep 17 00:00:00 2001 From: Greg Myers Date: Sat, 2 Nov 2019 12:55:09 +0000 Subject: [PATCH 4/9] Add failing test showing Psych errors --- rswag-specs/lib/rswag/specs/swagger_formatter.rb | 2 ++ rswag-specs/spec/rswag/specs/swagger_formatter_spec.rb | 2 ++ 2 files changed, 4 insertions(+) diff --git a/rswag-specs/lib/rswag/specs/swagger_formatter.rb b/rswag-specs/lib/rswag/specs/swagger_formatter.rb index b8061dc..d16aeb2 100644 --- a/rswag-specs/lib/rswag/specs/swagger_formatter.rb +++ b/rswag-specs/lib/rswag/specs/swagger_formatter.rb @@ -48,6 +48,8 @@ module Rswag def pretty_generate(doc) if @config.swagger_format == :yaml + # NOTE: Yaml will quite happily embed ruby-only classes such as symbols. + # clean_doc = stringify(doc) YAML.dump(doc) else # config errors are thrown in 'def swagger_format', no throw needed here JSON.pretty_generate(doc) diff --git a/rswag-specs/spec/rswag/specs/swagger_formatter_spec.rb b/rswag-specs/spec/rswag/specs/swagger_formatter_spec.rb index 2a29cde..6561f8a 100644 --- a/rswag-specs/spec/rswag/specs/swagger_formatter_spec.rb +++ b/rswag-specs/spec/rswag/specs/swagger_formatter_spec.rb @@ -74,6 +74,8 @@ module Rswag 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 From 2c0f3c939682dbd8706b4c4424af864590dde263 Mon Sep 17 00:00:00 2001 From: Greg Myers Date: Sat, 2 Nov 2019 12:58:36 +0000 Subject: [PATCH 5/9] Fix invalid Swagger in YAML --- rswag-specs/lib/rswag/specs/swagger_formatter.rb | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/rswag-specs/lib/rswag/specs/swagger_formatter.rb b/rswag-specs/lib/rswag/specs/swagger_formatter.rb index d16aeb2..7a8687a 100644 --- a/rswag-specs/lib/rswag/specs/swagger_formatter.rb +++ b/rswag-specs/lib/rswag/specs/swagger_formatter.rb @@ -48,14 +48,17 @@ module Rswag def pretty_generate(doc) if @config.swagger_format == :yaml - # NOTE: Yaml will quite happily embed ruby-only classes such as symbols. - # clean_doc = stringify(doc) - YAML.dump(doc) + YAML.dump(doc.deep_stringify_keys) else # config errors are thrown in 'def swagger_format', no throw needed here JSON.pretty_generate(doc) end end + def deep_stringify_hash_keys(doc) + + doc + end + def metadata_to_swagger(metadata) response_code = metadata[:response][:code] response = metadata[:response].reject { |k,v| k == :code } From eeb10266918ca52c6aa1e1e86fedbc3a7c1f65d4 Mon Sep 17 00:00:00 2001 From: Greg Myers Date: Sat, 2 Nov 2019 13:13:02 +0000 Subject: [PATCH 6/9] Fix invalid Swagger in YAML values The original fix failed because though the Keys were now strings, some of the values for path variables were also symbols. Psych does have a safe_load which has a whitelist of classes but it does not have a safe_dump mode. We could have used deep_transform_values and manually converted the classes we did not want, but why risk a buggy implementation when JSON.generate works just fine? --- rswag-specs/lib/rswag/specs/swagger_formatter.rb | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/rswag-specs/lib/rswag/specs/swagger_formatter.rb b/rswag-specs/lib/rswag/specs/swagger_formatter.rb index 7a8687a..6e23350 100644 --- a/rswag-specs/lib/rswag/specs/swagger_formatter.rb +++ b/rswag-specs/lib/rswag/specs/swagger_formatter.rb @@ -48,15 +48,16 @@ module Rswag def pretty_generate(doc) if @config.swagger_format == :yaml - YAML.dump(doc.deep_stringify_keys) + 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 deep_stringify_hash_keys(doc) - - doc + def yaml_prepare(doc) + json_doc = JSON.pretty_generate(doc) + clean_doc = JSON.parse(json_doc) end def metadata_to_swagger(metadata) From dc161fe27530de043e19f415d1f6dbce63bc4550 Mon Sep 17 00:00:00 2001 From: Greg Myers Date: Sat, 2 Nov 2019 13:57:47 +0000 Subject: [PATCH 7/9] Serve yaml files as yaml instead of converting them to json --- rswag-api/lib/rswag/api/middleware.rb | 19 +++++++++++++++---- rswag-api/spec/rswag/api/middleware_spec.rb | 4 ++-- .../rswag/ui/install/templates/rswag-ui.rb | 6 +++--- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/rswag-api/lib/rswag/api/middleware.rb b/rswag-api/lib/rswag/api/middleware.rb index 7256794..637d42d 100644 --- a/rswag-api/lib/rswag/api/middleware.rb +++ b/rswag-api/lib/rswag/api/middleware.rb @@ -1,9 +1,10 @@ require 'json' require 'yaml' +require 'rack/mime' module Rswag module Api - class Middleware + class Middleware def initialize(app, config) @app = app @@ -17,14 +18,16 @@ module Rswag if env['REQUEST_METHOD'] == 'GET' && File.file?(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' => mime }, + [ body ] ] end - + return @app.call(env) end @@ -45,6 +48,14 @@ module Rswag 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/rswag/api/middleware_spec.rb b/rswag-api/spec/rswag/api/middleware_spec.rb index f784f25..3ff0594 100644 --- a/rswag-api/spec/rswag/api/middleware_spec.rb +++ b/rswag-api/spec/rswag/api/middleware_spec.rb @@ -87,8 +87,8 @@ module Rswag it 'returns contents of the swagger file' do expect(response.length).to eql(3) - expect(response[1]).to include( 'Content-Type' => 'application/json') - expect(response[2].join).to include('"title":"API V1"') + expect(response[1]).to include( 'Content-Type' => 'text/yaml') + expect(response[2].join).to include('title: API V1') end end end 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 084a512..3a7fe3e 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 @@ 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 JSON or YAML 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 From 2d5da62bf1df7a0d9d4d681fc428ee0941c93643 Mon Sep 17 00:00:00 2001 From: Greg Myers Date: Sat, 2 Nov 2019 14:02:31 +0000 Subject: [PATCH 8/9] CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb1a888..b4a6374 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - New swagger_format config option for setting YAML output [#251](https://github.com/rswag/rswag/pull/251) ### Changed +- rswag-api will serve yaml files as yaml [#251](https://github.com/rswag/rswag/pull/251) ### Deprecated ### Removed ### Fixed From 4516ad4b7897a3234bc313334d786078a5c0fbb7 Mon Sep 17 00:00:00 2001 From: Greg Myers Date: Sat, 2 Nov 2019 14:52:37 +0000 Subject: [PATCH 9/9] Update changelog and tweak readme --- CHANGELOG.md | 12 ++++++++++-- README.md | 26 ++++++++++++++++++++------ 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4a6374..f084c97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,14 +6,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added -- New swagger_format config option for setting YAML output [#251](https://github.com/rswag/rswag/pull/251) ### Changed -- rswag-api will serve yaml files as yaml [#251](https://github.com/rswag/rswag/pull/251) ### Deprecated ### Removed ### Fixed ### Security +## [2.2.0] - 2019-11-01 +### Added +- New swagger_format config option for setting YAML output [#251](https://github.com/rswag/rswag/pull/251) +### Changed +- rswag-api will serve yaml files as yaml [#251](https://github.com/rswag/rswag/pull/251) + +## [2.1.1] - 2019-10-18 +### Fixed +- Fix incorrect require reference for swagger_generator [#248](https://github.com/rswag/rswag/issues/248) + ## [2.1.0] - 2019-10-17 ### Added - New Spec Generator [#75](https://github.com/rswag/rswag/pull/75) diff --git a/README.md b/README.md index bbf46e8..cfea9e4 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ rswag [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 youre 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 youre probably doing in some form already, to living documentation for your API consumers. And that's not all ... @@ -16,7 +16,7 @@ Once you have an API that can describe itself in Swagger, you've opened the trea |Rswag Version|Swagger (OpenAPI) Spec.|swagger-ui| |----------|----------|----------| |[master](https://github.com/rswag/rswag/tree/master)|2.0|3.18.2| -|[2.1.1](https://github.com/rswag/rswag/tree/2.1.1)|2.0|3.18.2| +|[2.2.0](https://github.com/rswag/rswag/tree/2.2.0)|2.0|3.18.2| |[1.6.0](https://github.com/rswag/rswag/tree/1.6.0)|2.0|2.2.5| ## Getting Started ## @@ -123,6 +123,9 @@ Once you have an API that can describe itself in Swagger, you've opened the trea end ``` + There is also a generator which can help get you started `rails generate rspec:swagger API::MyController` + + 4. Generate the Swagger JSON file(s) ```ruby @@ -190,7 +193,7 @@ describe 'Blogs API' do end end ``` -*Note:* the OAI v3 may be released soon(ish?) and include a nullable property. This may have an effect on the need/use of custom extension to the draft. Do not use this property if you don't understand the implications. +*Note:* OAI v3 has a nullable property. Rswag will work to support this soon. This may have an effect on the need/use of custom extension to the draft. Do not use this property if you don't understand the implications. ### Global Metadata ### @@ -213,8 +216,8 @@ RSpec.configure do |config| basePath: '/api/v1' }, - 'v2/swagger.json' => { - swagger: '2.0', + 'v2/swagger.yaml' => { + openapi: '3.0.0', info: { title: 'API V2', version: 'v2', @@ -231,7 +234,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 ... @@ -448,6 +451,17 @@ RSpec.configure do |config| config.swagger_dry_run = false end ``` + +### Running tests without documenting ### + +If you want to use Rswag for testing without adding it to you swagger docs, you can simply nullify the response metadata after the test run. + +```ruby +after do |example| + example.metadata[:response] = null +end +``` + ### Route Prefix for Swagger JSON Endpoints ### The functionality to expose Swagger files, such as those generated by rswag-specs, as JSON endpoints is implemented as a Rails Engine. As with any Engine, you can change it's mount prefix in _routes.rb_: