From abf808e009815ae20beefe7dfb44ed594e6f6c47 Mon Sep 17 00:00:00 2001 From: Pavel Evstigneev Date: Wed, 24 Apr 2019 12:32:10 +0800 Subject: [PATCH 1/9] Add v3 formatter --- lib/rspec/rails/swagger/formatter_v3.rb | 38 ++++++++++++++++++++++ lib/rspec/rails/swagger/tasks/swagger.rake | 5 +++ 2 files changed, 43 insertions(+) create mode 100644 lib/rspec/rails/swagger/formatter_v3.rb diff --git a/lib/rspec/rails/swagger/formatter_v3.rb b/lib/rspec/rails/swagger/formatter_v3.rb new file mode 100644 index 0000000..f365884 --- /dev/null +++ b/lib/rspec/rails/swagger/formatter_v3.rb @@ -0,0 +1,38 @@ +RSpec::Support.require_rspec_core "formatters/base_text_formatter" +RSpec::Support.require_rspec_core "formatters/console_codes" + +require_relative 'formatter' + +module RSpec + module Rails + module Swagger + class Formatter_V3 < RSpec::Rails::Swagger::Formatter + RSpec::Core::Formatters.register self, :example_group_started, + :example_passed, :example_pending, :example_failed, :example_finished, + :close + + def response_for(operation, swagger_response) + status = swagger_response[:status_code] + + operation[:responses][status] ||= {} + operation[:responses][status].tap do |response| + + if swagger_response[:examples] + schema = swagger_response[:schema] || {} + response[:content] ||= {} + swagger_response[:examples].each_pair do |format, resp| + formatted = ResponseFormatters[format].call(resp) + response[:content][format] ||= {schema: schema.merge(example: formatted)} + end + elsif swagger_response[:schema] + response[:content] = {'application/json' => {schema: schema}} + end + + response.merge!(swagger_response.slice(:description, :headers)) + end + end + + end + end + end +end diff --git a/lib/rspec/rails/swagger/tasks/swagger.rake b/lib/rspec/rails/swagger/tasks/swagger.rake index 02840da..27b562a 100644 --- a/lib/rspec/rails/swagger/tasks/swagger.rake +++ b/lib/rspec/rails/swagger/tasks/swagger.rake @@ -5,3 +5,8 @@ RSpec::Core::RakeTask.new(:swagger) do |t| t.verbose = false t.rspec_opts = "-f RSpec::Rails::Swagger::Formatter --order defined -t swagger_object" end + +RSpec::Core::RakeTask.new(:swagger_v3) do |t| + t.verbose = false + t.rspec_opts = "-f RSpec::Rails::Swagger::Formatter_V3 --order defined -t swagger_object" +end From ee3e89298495b04985303ddf328fabfa2fa011fc Mon Sep 17 00:00:00 2001 From: Pavel Evstigneev Date: Wed, 24 Apr 2019 21:28:08 +0800 Subject: [PATCH 2/9] V3 formatter add request body --- lib/rspec/rails/swagger/formatter_v3.rb | 42 +++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/lib/rspec/rails/swagger/formatter_v3.rb b/lib/rspec/rails/swagger/formatter_v3.rb index f365884..92b2ca0 100644 --- a/lib/rspec/rails/swagger/formatter_v3.rb +++ b/lib/rspec/rails/swagger/formatter_v3.rb @@ -32,6 +32,48 @@ def response_for(operation, swagger_response) end end + def path_item_for(document, swagger_path_item) + name = swagger_path_item[:path] + + document[:paths] ||= {} + document[:paths][name] ||= {} + if swagger_path_item[:parameters] + apply_params(document[:paths][name], swagger_path_item[:parameters].dup) + end + document[:paths][name] + end + + def operation_for(path, swagger_operation) + method = swagger_operation[:method] + + path[method] ||= {responses: {}} + path[method].tap do |operation| + if swagger_operation[:parameters] + apply_params(operation, swagger_operation[:parameters].dup) + end + operation.merge!(swagger_operation.slice( + :tags, :summary, :description, :externalDocs, :operationId, + :consumes, :produces, :schemes, :deprecated, :security + )) + end + end + + def apply_params(object, parameters) + body = parameters.delete('body&body') + if body + object[:requestBody] = { + required: body[:required], + content: { + 'application/json': { + schema: body[:schema], + examples: body[:examples] + } + } + } + end + object[:parameters] = prepare_parameters(parameters) + end + end end end From 7b85437ad9f18f1bb4e947795aa49f23c1d07af9 Mon Sep 17 00:00:00 2001 From: Pavel Evstigneev Date: Thu, 25 Apr 2019 04:32:24 +0800 Subject: [PATCH 3/9] Remove consumes from v3 formatter, remove type from parameters --- lib/rspec/rails/swagger/formatter_v3.rb | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/rspec/rails/swagger/formatter_v3.rb b/lib/rspec/rails/swagger/formatter_v3.rb index 92b2ca0..f50aede 100644 --- a/lib/rspec/rails/swagger/formatter_v3.rb +++ b/lib/rspec/rails/swagger/formatter_v3.rb @@ -14,6 +14,9 @@ class Formatter_V3 < RSpec::Rails::Swagger::Formatter def response_for(operation, swagger_response) status = swagger_response[:status_code] + content_type = operation[:consumes] && operation[:consumes][0] || 'application/json' + operation.delete(:consumes) + operation[:responses][status] ||= {} operation[:responses][status].tap do |response| @@ -25,7 +28,7 @@ def response_for(operation, swagger_response) response[:content][format] ||= {schema: schema.merge(example: formatted)} end elsif swagger_response[:schema] - response[:content] = {'application/json' => {schema: schema}} + response[:content] = {content_type => {schema: schema}} end response.merge!(swagger_response.slice(:description, :headers)) @@ -66,12 +69,16 @@ def apply_params(object, parameters) content: { 'application/json': { schema: body[:schema], - examples: body[:examples] + examples: body[:examples] || {} } } } end - object[:parameters] = prepare_parameters(parameters) + + object[:parameters] = parameters.values.map do |param| + param.slice(:in, :name, :required, :schema, :description, :style, + :explode, :allowEmptyValue, :example, :examples, :deprecated) + end end end From 2c08dd89dbc768fafa029b3922179d06a0f778fe Mon Sep 17 00:00:00 2001 From: Pavel Evstigneev Date: Mon, 27 May 2019 18:48:30 +0800 Subject: [PATCH 4/9] Rename formatter_v3 to formatter_open_api --- README.md | 7 + lib/rspec/rails/swagger.rb | 1 + ...{formatter_v3.rb => formatter_open_api.rb} | 2 +- lib/rspec/rails/swagger/tasks/swagger.rake | 4 +- scripts/run_tests.sh | 1 + .../rails/swagger/formatter_openapi_spec.rb | 236 ++++++++++++++++++ spec/swagger_helper.rb | 7 + 7 files changed, 255 insertions(+), 3 deletions(-) rename lib/rspec/rails/swagger/{formatter_v3.rb => formatter_open_api.rb} (97%) create mode 100644 spec/rspec/rails/swagger/formatter_openapi_spec.rb diff --git a/README.md b/README.md index 41a511e..73d2164 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,13 @@ To create the Swagger files use the rake task: bundle exec rake swagger ``` +For OpenAPI use: + +``` +bundle exec rake openapi +``` + + Now you can use Swagger UI or the renderer of your choice to display the formatted documentation. [swagger_engine](https://github.com/batdevis/swagger_engine) works pretty well and supports multiple documents. diff --git a/lib/rspec/rails/swagger.rb b/lib/rspec/rails/swagger.rb index 489ab79..932186e 100644 --- a/lib/rspec/rails/swagger.rb +++ b/lib/rspec/rails/swagger.rb @@ -2,6 +2,7 @@ require 'rspec/rails/swagger/configuration' require 'rspec/rails/swagger/document' require 'rspec/rails/swagger/formatter' +require 'rspec/rails/swagger/formatter_open_api' require 'rspec/rails/swagger/helpers' require 'rspec/rails/swagger/response_formatters' require 'rspec/rails/swagger/request_builder' diff --git a/lib/rspec/rails/swagger/formatter_v3.rb b/lib/rspec/rails/swagger/formatter_open_api.rb similarity index 97% rename from lib/rspec/rails/swagger/formatter_v3.rb rename to lib/rspec/rails/swagger/formatter_open_api.rb index f50aede..ce15530 100644 --- a/lib/rspec/rails/swagger/formatter_v3.rb +++ b/lib/rspec/rails/swagger/formatter_open_api.rb @@ -6,7 +6,7 @@ module RSpec module Rails module Swagger - class Formatter_V3 < RSpec::Rails::Swagger::Formatter + class FormatterOpenApi < RSpec::Rails::Swagger::Formatter RSpec::Core::Formatters.register self, :example_group_started, :example_passed, :example_pending, :example_failed, :example_finished, :close diff --git a/lib/rspec/rails/swagger/tasks/swagger.rake b/lib/rspec/rails/swagger/tasks/swagger.rake index 27b562a..d3a202a 100644 --- a/lib/rspec/rails/swagger/tasks/swagger.rake +++ b/lib/rspec/rails/swagger/tasks/swagger.rake @@ -6,7 +6,7 @@ RSpec::Core::RakeTask.new(:swagger) do |t| t.rspec_opts = "-f RSpec::Rails::Swagger::Formatter --order defined -t swagger_object" end -RSpec::Core::RakeTask.new(:swagger_v3) do |t| +RSpec::Core::RakeTask.new(:openapi) do |t| t.verbose = false - t.rspec_opts = "-f RSpec::Rails::Swagger::Formatter_V3 --order defined -t swagger_object" + t.rspec_opts = "-f RSpec::Rails::Swagger::FormatterOpenApi --order defined -t swagger_object" end diff --git a/scripts/run_tests.sh b/scripts/run_tests.sh index 2456ae2..a36bcb1 100755 --- a/scripts/run_tests.sh +++ b/scripts/run_tests.sh @@ -4,3 +4,4 @@ set -x -e bundle exec rspec # Duplicating the body of the rake task. Need to figure out how to call it directly. bundle exec rspec -f RSpec::Rails::Swagger::Formatter --order defined -t swagger_object +bundle exec rspec -f RSpec::Rails::Swagger::FormatterOpenApi --order defined -t swagger_object diff --git a/spec/rspec/rails/swagger/formatter_openapi_spec.rb b/spec/rspec/rails/swagger/formatter_openapi_spec.rb new file mode 100644 index 0000000..a668d24 --- /dev/null +++ b/spec/rspec/rails/swagger/formatter_openapi_spec.rb @@ -0,0 +1,236 @@ +require 'swagger_helper' + +RSpec.describe RSpec::Rails::Swagger::FormatterOpenApi do + let(:output) { StringIO.new } + let(:formatter) { described_class.new(output) } + let(:documents) { {'minimal.json' => minimal} } + # Make this a method to bypass rspec's memoization. + def minimal + { + openapi: '3.0.0', + info: { + version: '0.0.0', + title: 'Simple API' + } + } + end + + before do + RSpec.configure {|c| c.swagger_docs = documents } + end + + describe "#example_finished" do + let(:example_notification) { double('Notification', example: double('Example', metadata: metadata)) } + let(:metadata) { {} } + + context "with a single document" do + let(:metadata) do + { + swagger_object: :response, + swagger_path_item: {path: "/ping"}, + swagger_operation: {method: :put}, + swagger_response: {status_code: 200, description: "OK"}, + } + end + + it "copies the requests into the document" do + formatter.example_finished(example_notification) + + expect(formatter.documents.values.first[:paths]).to eq({ + '/ping' => { + put: { + responses: {200 => {description: 'OK'}} + } + } + }) + end + end + + context "with multiple documents" do + let(:documents) { {'doc1.json' => minimal, 'doc2.json' => minimal} } + let(:metadata) do + { + swagger_object: :response, + swagger_doc: 'doc2.json', + swagger_path_item: {path: "/ping"}, + swagger_operation: {method: :put}, + swagger_response: {status_code: 200, description: "OK"}, + } + end + + it "puts the response on the right document" do + formatter.example_finished(example_notification) + + expect(formatter.documents['doc1.json'][:paths]).to be_blank + expect(formatter.documents['doc2.json'][:paths].length).to eq(1) + end + end + + context "with a response examples" do + let(:metadata_examples) { {'application/json' => JSON.dump({foo: :bar})} } + let(:metadata) do + { + swagger_object: :response, + swagger_path_item: {path: "/ping"}, + swagger_operation: {method: :put}, + swagger_response: {status_code: 200, description: "OK", examples: metadata_examples}, + } + end + + shared_examples 'response example formatter' do + it "copies the requests into the document" do + formatter.example_finished(example_notification) + expected_paths = { + '/ping' => { + put: { + responses: { + 200 => { + content: { + "application/json" => { + schema: { + example: output_example + } + } + }, + description: "OK" + } + } + } + } + } + expect(formatter.documents.values.first[:paths]).to eq(expected_paths) + end + end + + context "with a default formatter" do + before(:example) do + RSpec::Rails::Swagger::ResponseFormatters.register( + 'application/json', + RSpec::Rails::Swagger::ResponseFormatters::JSON.new + ) + end + + let(:output_example) { {"foo" => "bar"} } + include_examples 'response example formatter' + end + + context "custom application/json formatter" do + before(:example) do + RSpec::Rails::Swagger::ResponseFormatters.register('application/json', ->(resp) { resp }) + end + + let(:output_example) { JSON.dump({"foo" => "bar"}) } + include_examples 'response example formatter' + end + end + end + + describe "#close" do + let(:blank_notification) { double('Notification') } + + context "no relevant examples" do + it "writes document with no changes" do + expect(formatter).to receive(:write_file).with(documents.keys.first, documents.values.first) + formatter.close(blank_notification) + end + end + + context "with a relevant example" do + let(:example_notification) { double(example: double(metadata: metadata)) } + let(:metadata) do + { + swagger_object: :response, + swagger_path_item: {path: "/ping"}, + swagger_operation: {method: :get, produces: ["application/json"]}, + swagger_response: {status_code: 200, description: 'all good'}, + } + end + + it "writes a document with the request" do + formatter.example_finished(example_notification) + + expect(formatter).to receive(:write_file).with( + documents.keys.first, + documents.values.first.merge({ + paths: { + '/ping' => { + get: { + responses: {200 => {description: 'all good'}}, + produces: ["application/json"] + } + } + } + }) + ) + + formatter.close(blank_notification) + end + + describe 'output formats' do + let(:documents) { {file_name => minimal} } + + subject do + formatter.example_finished(example_notification) + formatter.close(blank_notification) + Pathname(file_name).expand_path(::RSpec.configuration.swagger_root).read + end + + %w(yaml yml).each do |extension| + context "with a name that ends in .#{extension}" do + let(:file_name) { "minimal.#{extension}" } + + it 'outputs YAML' do + expect(subject).to eq < { + openapi: '3.0.0', + info: { + title: 'API V1', + version: 'v1' + } } } end From 365557068550d0262720c1a693528406458718e4 Mon Sep 17 00:00:00 2001 From: Pavel Evstigneev Date: Mon, 27 May 2019 18:48:40 +0800 Subject: [PATCH 5/9] Add simple html example --- examples/README.md | 6 +++ examples/index.html | 22 +++++++++ examples/swagger.json | 106 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 134 insertions(+) create mode 100644 examples/README.md create mode 100644 examples/index.html create mode 100644 examples/swagger.json diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..6e77343 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,6 @@ +Example of standalone swagger UI with assets loaded from CDN + +How to run: + +1. `cd examples && ruby -run -e httpd . -p 4000` +2. Visit http://localhost:4000/`` diff --git a/examples/index.html b/examples/index.html new file mode 100644 index 0000000..06a844a --- /dev/null +++ b/examples/index.html @@ -0,0 +1,22 @@ + + + + + + Example API + + + + +
+ + + + + diff --git a/examples/swagger.json b/examples/swagger.json new file mode 100644 index 0000000..e930600 --- /dev/null +++ b/examples/swagger.json @@ -0,0 +1,106 @@ +{ + "swagger": "2.0", + "info": { + "title": "API V1", + "version": "v1" + }, + "paths": { + "/posts": { + "get": { + "responses": { + "200": { + "description": "successful" + } + }, + "tags": [ + "context_tag", + "path_tag", + "operation_tag" + ], + "summary": "fetch list", + "produces": [ + "application/json" + ] + }, + "post": { + "responses": { + "201": { + "description": "successfully created", + "content": { + "application/json": { + "schema": { + "example": { + "id": 1, + "title": "asdf", + "body": "blah", + "created_at": "2019-05-27T09:54:55.606Z", + "updated_at": "2019-05-27T09:54:55.606Z" + } + } + } + } + } + }, + "requestBody": { + "required": null, + "content": { + "application/json": { + "schema": { + "foo": "bar" + }, + "examples": { + } + } + } + }, + "parameters": [ + + ], + "tags": [ + "context_tag", + "path_tag" + ], + "summary": "create", + "produces": [ + "application/json" + ] + } + }, + "/posts/{post_id}": { + "parameters": [ + { + "in": "path", + "name": "post_id", + "required": true + } + ], + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "example": { + "id": 1, + "title": null, + "body": null, + "created_at": "2019-05-27T09:54:55.611Z", + "updated_at": "2019-05-27T09:54:55.611Z" + } + } + } + }, + "description": "success" + } + }, + "tags": [ + "context_tag" + ], + "summary": "fetch item", + "produces": [ + "application/json" + ] + } + } + } +} From 0325fc6c3092d79268adcaada3c0a9ef5b2eefb6 Mon Sep 17 00:00:00 2001 From: Pavel Evstigneev Date: Mon, 27 May 2019 18:52:55 +0800 Subject: [PATCH 6/9] Try to fix climate --- lib/rspec/rails/swagger/formatter_open_api.rb | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/lib/rspec/rails/swagger/formatter_open_api.rb b/lib/rspec/rails/swagger/formatter_open_api.rb index ce15530..c5550fa 100644 --- a/lib/rspec/rails/swagger/formatter_open_api.rb +++ b/lib/rspec/rails/swagger/formatter_open_api.rb @@ -19,20 +19,23 @@ def response_for(operation, swagger_response) operation[:responses][status] ||= {} operation[:responses][status].tap do |response| + prepare_response_contents(response, swagger_response) + end + end - if swagger_response[:examples] - schema = swagger_response[:schema] || {} - response[:content] ||= {} - swagger_response[:examples].each_pair do |format, resp| - formatted = ResponseFormatters[format].call(resp) - response[:content][format] ||= {schema: schema.merge(example: formatted)} - end - elsif swagger_response[:schema] - response[:content] = {content_type => {schema: schema}} + def prepare_response_contents(response, swagger_response) + if swagger_response[:examples] + schema = swagger_response[:schema] || {} + response[:content] ||= {} + swagger_response[:examples].each_pair do |format, resp| + formatted = ResponseFormatters[format].call(resp) + response[:content][format] ||= {schema: schema.merge(example: formatted)} end - - response.merge!(swagger_response.slice(:description, :headers)) + elsif swagger_response[:schema] + response[:content] = {content_type => {schema: schema}} end + + response.merge!(swagger_response.slice(:description, :headers)) end def path_item_for(document, swagger_path_item) From 9173e38af8a3f5b8320aa1e6a5c2152a50624e02 Mon Sep 17 00:00:00 2001 From: Pavel Evstigneev Date: Mon, 27 May 2019 19:31:28 +0800 Subject: [PATCH 7/9] Try to fix climate --- lib/rspec/rails/swagger/formatter_open_api.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rspec/rails/swagger/formatter_open_api.rb b/lib/rspec/rails/swagger/formatter_open_api.rb index c5550fa..c699a73 100644 --- a/lib/rspec/rails/swagger/formatter_open_api.rb +++ b/lib/rspec/rails/swagger/formatter_open_api.rb @@ -70,7 +70,7 @@ def apply_params(object, parameters) object[:requestBody] = { required: body[:required], content: { - 'application/json': { + 'application/json' => { schema: body[:schema], examples: body[:examples] || {} } From 4f008089952baa97c7a4b971159bf16e1f450076 Mon Sep 17 00:00:00 2001 From: Pavel Evstigneev Date: Mon, 27 May 2019 19:46:39 +0800 Subject: [PATCH 8/9] Add test for openapi response schema --- lib/rspec/rails/swagger/formatter_open_api.rb | 6 +-- .../rails/swagger/formatter_openapi_spec.rb | 37 +++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/lib/rspec/rails/swagger/formatter_open_api.rb b/lib/rspec/rails/swagger/formatter_open_api.rb index c699a73..c284fd3 100644 --- a/lib/rspec/rails/swagger/formatter_open_api.rb +++ b/lib/rspec/rails/swagger/formatter_open_api.rb @@ -19,11 +19,11 @@ def response_for(operation, swagger_response) operation[:responses][status] ||= {} operation[:responses][status].tap do |response| - prepare_response_contents(response, swagger_response) + prepare_response_contents(response, swagger_response, content_type) end end - def prepare_response_contents(response, swagger_response) + def prepare_response_contents(response, swagger_response, content_type) if swagger_response[:examples] schema = swagger_response[:schema] || {} response[:content] ||= {} @@ -32,7 +32,7 @@ def prepare_response_contents(response, swagger_response) response[:content][format] ||= {schema: schema.merge(example: formatted)} end elsif swagger_response[:schema] - response[:content] = {content_type => {schema: schema}} + response[:content] = {content_type => {schema: swagger_response[:schema]}} end response.merge!(swagger_response.slice(:description, :headers)) diff --git a/spec/rspec/rails/swagger/formatter_openapi_spec.rb b/spec/rspec/rails/swagger/formatter_openapi_spec.rb index a668d24..732cabc 100644 --- a/spec/rspec/rails/swagger/formatter_openapi_spec.rb +++ b/spec/rspec/rails/swagger/formatter_openapi_spec.rb @@ -123,6 +123,43 @@ def minimal include_examples 'response example formatter' end end + + context "with a response schema" do + let(:metadata) do + { + swagger_object: :response, + swagger_path_item: {path: "/ping"}, + swagger_operation: {method: :put}, + swagger_response: { + status_code: 200, + description: "OK", + schema: { '$ref': "#/components/schemas/BasicRequest" } + } + } + end + + it "copies the requests into the document" do + formatter.example_finished(example_notification) + expected_paths = { + '/ping' => { + put: { + responses: { + 200 => { + content: { + "application/json" => { + schema: { '$ref': "#/components/schemas/BasicRequest" } + } + }, + description: "OK" + } + } + } + } + } + + expect(formatter.documents.values.first[:paths]).to eq(expected_paths) + end + end end describe "#close" do From 42970f9af27c0683e0b9c85a604fa04cf204e6d3 Mon Sep 17 00:00:00 2001 From: Pavel Evstigneev Date: Mon, 27 May 2019 19:49:26 +0800 Subject: [PATCH 9/9] Try to fix climate --- spec/rspec/rails/swagger/formatter_openapi_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/rspec/rails/swagger/formatter_openapi_spec.rb b/spec/rspec/rails/swagger/formatter_openapi_spec.rb index 732cabc..163286d 100644 --- a/spec/rspec/rails/swagger/formatter_openapi_spec.rb +++ b/spec/rspec/rails/swagger/formatter_openapi_spec.rb @@ -133,7 +133,7 @@ def minimal swagger_response: { status_code: 200, description: "OK", - schema: { '$ref': "#/components/schemas/BasicRequest" } + schema: { :'$ref' => "#/components/schemas/BasicRequest" } } } end @@ -147,7 +147,7 @@ def minimal 200 => { content: { "application/json" => { - schema: { '$ref': "#/components/schemas/BasicRequest" } + schema: { :'$ref' => "#/components/schemas/BasicRequest" } } }, description: "OK"