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/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"
+ ]
+ }
+ }
+ }
+}
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_open_api.rb b/lib/rspec/rails/swagger/formatter_open_api.rb
new file mode 100644
index 0000000..c284fd3
--- /dev/null
+++ b/lib/rspec/rails/swagger/formatter_open_api.rb
@@ -0,0 +1,90 @@
+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 FormatterOpenApi < 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]
+
+ content_type = operation[:consumes] && operation[:consumes][0] || 'application/json'
+ operation.delete(:consumes)
+
+ operation[:responses][status] ||= {}
+ operation[:responses][status].tap do |response|
+ prepare_response_contents(response, swagger_response, content_type)
+ end
+ end
+
+ def prepare_response_contents(response, swagger_response, content_type)
+ 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: swagger_response[:schema]}}
+ end
+
+ response.merge!(swagger_response.slice(:description, :headers))
+ 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] = parameters.values.map do |param|
+ param.slice(:in, :name, :required, :schema, :description, :style,
+ :explode, :allowEmptyValue, :example, :examples, :deprecated)
+ 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..d3a202a 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(:openapi) do |t|
+ t.verbose = false
+ 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..163286d
--- /dev/null
+++ b/spec/rspec/rails/swagger/formatter_openapi_spec.rb
@@ -0,0 +1,273 @@
+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
+
+ 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
+ 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