diff --git a/CHANGELOG.md b/CHANGELOG.md index fa57b8b..f084c97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 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_: 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-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/lib/rswag/specs/swagger_formatter.rb b/rswag-specs/lib/rswag/specs/swagger_formatter.rb index 794a9d9..6e23350 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,20 @@ module Rswag 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) + clean_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 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) } diff --git a/rswag-specs/spec/rswag/specs/swagger_formatter_spec.rb b/rswag-specs/spec/rswag/specs/swagger_formatter_spec.rb index b5b7ad8..6561f8a 100644 --- a/rswag-specs/spec/rswag/specs/swagger_formatter_spec.rb +++ b/rswag-specs/spec/rswag/specs/swagger_formatter_spec.rb @@ -53,14 +53,30 @@ 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) + # 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 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