diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 8aa984dd..1def99a2 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -29,6 +29,5 @@ The version of are you using for: * Ruby: ## Relates to which version of OAS (OpenAPI Specification) -- [ ] OAS2 - [ ] OAS3 - [ ] OAS3.1 diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 6b7a05db..f28f99c9 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -24,6 +24,5 @@ A clear and concise description of any alternative solutions or features you've Add any other context or screenshots about the feature request here. ## Relates to which version of OAS (OpenAPI Specification) -- [ ] OAS2 - [ ] OAS3 - [ ] OAS3.1 diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 27a4a116..a672e031 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -9,7 +9,6 @@ A clear and concise description of what the solution is. * [ANOTHER LINK TO RELEVANT OPEN API SPECS PAGE](https://spec.openapis.org/oas/v3.1.0#schema) ### The changes I made are compatible with: -- [ ] OAS2 - [ ] OAS3 - [ ] OAS3.1 diff --git a/README.md b/README.md index 6fe34014..0c05bc79 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ rswag [![Build Status](https://github.com/rswag/rswag/actions/workflows/ruby.yml/badge.svg?branch=master)](https://github.com/rswag/rswag/actions/workflows/ruby.yml?query=branch%3Amaster+) [![Maintainability](https://api.codeclimate.com/v1/badges/1175b984edc4610f82ab/maintainability)](https://codeclimate.com/github/rswag/rswag/maintainability) -OpenApi 3.0 and Swagger 2.0 compatible! +OpenApi 3.0 compatible! Seeking maintainers! Got a pet-bug that needs fixing? Just let us know in your issue/pr that you'd like to step up to help. @@ -33,7 +33,7 @@ Once you have an API that can describe itself in Swagger, you've opened the trea - [Formatting the description literals:](#formatting-the-description-literals) - [Specifying/Testing API Security](#specifyingtesting-api-security) - [Configuration & Customization](#configuration--customization) - - [Output Location for Generated Swagger Files](#output-location-for-generated-swagger-files) + - [Output Location for Generated OpenAPI Files](#output-location-for-generated-openapi-files) - [Input Location for Rspec Tests](#input-location-for-rspec-tests) - [Referenced Parameters and Schema Definitions](#referenced-parameters-and-schema-definitions) - [Request examples](#request-examples) @@ -44,10 +44,10 @@ Once you have an API that can describe itself in Swagger, you've opened the trea - [Dry Run Option](#dry-run-option) - [Running tests without documenting](#running-tests-without-documenting) - [rswag helper methods](#rswag-helper-methods) - - [Route Prefix for Swagger JSON Endpoints](#route-prefix-for-swagger-json-endpoints) - - [Root Location for Swagger Files](#root-location-for-swagger-files) - - [Dynamic Values for Swagger JSON](#dynamic-values-for-swagger-json) - - [Custom Headers for Swagger Files](#custom-headers-for-swagger-files) + - [Route Prefix for OpenAPI JSON Endpoints](#route-prefix-for-openapi-json-endpoints) + - [Root Location for OpenAPI Files](#root-location-for-openapi-files) + - [Dynamic Values for OpenAPI JSON](#dynamic-values-for-openapi-json) + - [Custom Headers for OpenAPI Files](#custom-headers-for-openapi-files) - [Enable Swagger Endpoints for swagger-ui](#enable-swagger-endpoints-for-swagger-ui) - [Enable Simple Basic Auth for swagger-ui](#enable-simple-basic-auth-for-swagger-ui) - [Route Prefix for the swagger-ui](#route-prefix-for-the-swagger-ui) @@ -242,7 +242,7 @@ end ### Support for oneOf, anyOf or AllOf schemas ### -Open API 3.0 now supports more flexible schema validation with the ```oneOf```, ```anyOf``` and ```allOf``` directives. rswag will handle these definitions and validate them properly. +OpenAPI 3.0 supports more flexible schema validation with the ```oneOf```, ```anyOf``` and ```allOf``` directives. rswag will handle these definitions and validate them properly. Notice the ```schema``` inside the ```response``` section. Placing a ```schema``` method inside the response will validate (and fail the tests) @@ -277,7 +277,7 @@ This automatic schema validation is a powerful feature of rswag. ### Global Metadata ### -In addition to paths, operations and responses, Swagger also supports global API metadata. When you install rswag, a file called _swagger_helper.rb_ is added to your spec folder. This is where you define one or more Swagger documents and provide global metadata. Again, the format is based on Swagger so most of the global fields supported by the top level ["Swagger" object](http://swagger.io/specification/#swaggerObject) can be provided with each document definition. As an example, you could define a Swagger document for each version of your API and in each case specify a title, version string. In Open API 3.0 the pathing and server definitions have changed a bit [Swagger host/basePath](https://swagger.io/docs/specification/api-host-and-base-path/): +In addition to paths, operations and responses, Swagger also supports global API metadata. When you install rswag, a file called _swagger_helper.rb_ is added to your spec folder. This is where you define one or more Swagger documents and provide global metadata. Again, the format is based on Swagger so most of the global fields supported by the top level ["Swagger" object](http://swagger.io/specification/#swaggerObject) can be provided with each document definition. As an example, you could define a Swagger document for each version of your API and in each case specify a title, version string. ```ruby # spec/swagger_helper.rb @@ -364,7 +364,7 @@ you should use the following syntax, making sure there is no whitespace at the s Swagger allows for the specification of different security schemes and their applicability to operations in an API. To leverage this in rswag, you define the schemes globally in _swagger_helper.rb_ and then use the "security" attribute at the operation level to specify which schemes, if any, are applicable to that operation. -Swagger supports :basic, :bearer, :apiKey and :oauth2 and :openIdConnect scheme types. See [the spec](https://swagger.io/docs/specification/authentication/) for more info, as this underwent major changes between Swagger 2.0 and Open API 3.0 +Swagger supports :basic, :bearer, :apiKey and :oauth2 and :openIdConnect scheme types. See [the spec](https://swagger.io/docs/specification/authentication/) for more info. ```ruby # spec/swagger_helper.rb @@ -373,7 +373,6 @@ RSpec.configure do |config| config.swagger_docs = { 'v1/swagger.json' => { - ... # note the new Open API 3.0 compliant security structure here, under "components" components: { securitySchemes: { basic_auth: { @@ -453,7 +452,7 @@ The steps described above will get you up and running with minimal setup. Howeve | __rswag-api__ | Rails Engine that exposes your Swagger files as JSON endpoints | _config/initializers/rswag_api.rb, config/routes.rb_ | | __rswag-ui__ | Rails Engine that includes [swagger-ui](https://github.com/swagger-api/swagger-ui) and powers it from your Swagger endpoints | _config/initializers/rswag-ui.rb, config/routes.rb_ | -### Output Location for Generated Swagger Files ### +### Output Location for Generated OpenAPI Files ### You can adjust this in the _swagger_helper.rb_ that's installed with __rswag-specs__: @@ -601,7 +600,7 @@ end ### Response headers ### In Rswag, you could use `header` method inside the response block to specify header objects for this response. -Rswag will validate your response headers with those header objects and inject them into the generated swagger file: +Rswag will validate your response headers with those header objects and inject them into the generated OpenAPI spec: ```ruby # spec/requests/comments_spec.rb @@ -639,7 +638,7 @@ end ### Response examples ### -You can provide custom response examples to the generated swagger file by calling the method `examples` inside the response block: +You can provide custom response examples to the generated OpenAPI spec by calling the method `examples` inside the response block: However, auto generated example responses are now enabled by default in rswag. See below. ```ruby @@ -772,7 +771,7 @@ In the above example, we see methods ```request_body_json``` ```request_body_pla These methods can be used to describe json, plain text and xml body. They are just wrapper methods to setup posting JSON, plain text or xml into your endpoint. The simplest most common usage is for json formatted body to use the schema: to specify the location of the schema for the request body and the examples: :blog which will create a named example "blog" under the "requestBody / content / application/json / examples" section. -Again, documenting request response examples changed in Open API 3.0. The example above would generate a swagger.json snippet that looks like this: +Again, documenting request response examples changed in OpenAPI 3.0. The example above would generate a swagger.json snippet that looks like this: ```json ... @@ -782,7 +781,7 @@ Again, documenting request response examples changed in Open API 3.0. The exampl "application/json": { "examples": { "blog": { // takes the name from examples: :blog above - "value": { //this is open api 3.0 structure -> https://swagger.io/docs/specification/adding-examples/ + "value": { // this is OpenAPI 3.0 structure -> https://swagger.io/docs/specification/adding-examples/ "blog": { // here is the actual JSON payload that is submitted to the service, and shows up in swagger UI as an example "title": "foo", "content": "bar" @@ -815,9 +814,9 @@ This ```let``` value is used in the integration test to run the test AND capture ##### rswag response examples ##### -In the same way that requestBody examples can be captured and injected into the swagger output, response examples can also be captured. -Using the above example, when the integration test is run - the swagger would include the following snippet providing more useful real world examples -capturing the response from the execution of the integration test. Again 3.0 swagger changed the structure of how these are documented. +In the same way that requestBody examples can be captured and injected into the output, response examples can also be captured. +Using the above example, when the integration test is run - the OpenAPI spec would include the following snippet providing more useful real world examples +capturing the response from the execution of the integration test. ```json ... "responses": { @@ -857,9 +856,9 @@ capturing the response from the execution of the integration test. Again 3.0 swa } ``` --> -### Route Prefix for Swagger JSON Endpoints ### +### Route Prefix for OpenAPI 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_: +The functionality to expose OpenAPI 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_: ```ruby TestApp::Application.routes.draw do @@ -869,48 +868,48 @@ TestApp::Application.routes.draw do end ``` -Assuming a Swagger file exists at <swagger_root>/v1/swagger.json, this configuration would expose the file as the following JSON endpoint: +Assuming a OpenAPI file exists at <openapi_root>/v1/openapi.json, this configuration would expose the file as the following JSON endpoint: ``` -GET http:///your-custom-prefix/v1/swagger.json +GET http:///your-custom-prefix/v1/openapi.json ``` -### Root Location for Swagger Files ### +### Root Location for OpenAPI Files ### You can adjust this in the _rswag_api.rb_ initializer that's installed with __rspec-api__: ```ruby Rswag::Api.configure do |c| - c.swagger_root = Rails.root.to_s + '/your-custom-folder-name' + c.openapi_root = Rails.root.to_s + '/your-custom-folder-name' ... end ``` -__NOTE__: If you're using rswag-specs to generate Swagger files, you'll want to ensure they both use the same <swagger_root>. The reason for separate settings is to maintain independence between the two gems. For example, you could install rswag-api independently and create your Swagger files manually. +__NOTE__: If you're using rswag-specs to generate OpenAPI files, you'll want to ensure they both use the same <openapi_root>. The reason for separate settings is to maintain independence between the two gems. For example, you could install rswag-api independently and create your OpenAPI files manually. -### Dynamic Values for Swagger JSON ## +### Dynamic Values for OpenAPI JSON ## -There may be cases where you need to add dynamic values to the Swagger JSON that's returned by rswag-api. For example, you may want to provide an explicit host name. Rather than hardcoding it, you can configure a filter that's executed prior to serializing every Swagger document: +There may be cases where you need to add dynamic values to the OpenAPI JSON that's returned by rswag-api. For example, you may want to provide an explicit host name. Rather than hardcoding it, you can configure a filter that's executed prior to serializing every OpenAPI document: ```ruby Rswag::Api.configure do |c| ... - c.swagger_filter = lambda { |swagger, env| swagger['host'] = env['HTTP_HOST'] } + c.openapi_filter = lambda { |openapi, env| openapi['host'] = env['HTTP_HOST'] } end ``` Note how the filter is passed the rack env for the current request. This provides a lot of flexibility. For example, you can assign the "host" property (as shown) or you could inspect session information or an Authorization header and remove operations based on user permissions. -### Custom Headers for Swagger Files ### +### Custom Headers for OpenAPI Files ### -You can specify custom headers for serving your generated Swagger JSON. For example you may want to force a specific charset for the 'Content-Type' header. You can configure a hash of headers to be sent with the request: +You can specify custom headers for serving your generated OpenAPI JSON. For example you may want to force a specific charset for the 'Content-Type' header. You can configure a hash of headers to be sent with the request: ```ruby Rswag::Api.configure do |c| ... - c.swagger_headers = { 'Content-Type' => 'application/json; charset=UTF-8' } + c.openapi_headers = { 'Content-Type' => 'application/json; charset=UTF-8' } end ``` @@ -923,8 +922,8 @@ You can update the _rswag_ui.rb_ initializer, installed with rswag-ui, to specif ```ruby Rswag::Ui.configure do |c| - c.swagger_endpoint '/api-docs/v1/swagger.json', 'API V1 Docs' - c.swagger_endpoint '/api-docs/v2/swagger.json', 'API V2 Docs' + c.openapi_endpoint '/api-docs/v1/openapi.json', 'API V1 Docs' + c.openapi_endpoint '/api-docs/v2/openapi.json', 'API V2 Docs' end ``` @@ -982,4 +981,4 @@ docker pull swaggerapi/swagger-editor docker run -d -p 80:8080 swaggerapi/swagger-editor ``` This will run the swagger editor in the docker daemon and can be accessed -at ```http://localhost```. From here, you can use the UI to load the generated swagger.json to validate the output. +at ```http://localhost```. From here, you can use the UI to load the generated openapi.json to validate the output. diff --git a/rswag-api/lib/generators/rswag/api/install/templates/rswag_api.rb b/rswag-api/lib/generators/rswag/api/install/templates/rswag_api.rb index 4d72f687..3591a4aa 100644 --- a/rswag-api/lib/generators/rswag/api/install/templates/rswag_api.rb +++ b/rswag-api/lib/generators/rswag/api/install/templates/rswag_api.rb @@ -4,7 +4,7 @@ # This is used by the Swagger middleware to serve requests for API descriptions # NOTE: If you're using rswag-specs to generate Swagger, you'll need to ensure # that it's configured to generate files in the same folder - c.swagger_root = Rails.root.to_s + '/swagger' + c.openapi_root = Rails.root.to_s + '/openapi' # Inject a lambda function to alter the returned Swagger prior to serialization # The function will have access to the rack env for the current request diff --git a/rswag-api/lib/rswag/api/configuration.rb b/rswag-api/lib/rswag/api/configuration.rb index bf642290..6f286fb8 100644 --- a/rswag-api/lib/rswag/api/configuration.rb +++ b/rswag-api/lib/rswag/api/configuration.rb @@ -1,11 +1,11 @@ module Rswag module Api class Configuration - attr_accessor :swagger_root, :swagger_filter, :swagger_headers + attr_accessor :openapi_root, :openapi_filter, :openapi_headers - def resolve_swagger_root(env) + def resolve_openapi_root(env) path_params = env['action_dispatch.request.path_parameters'] || {} - path_params[:swagger_root] || swagger_root + path_params[:openapi_root] || openapi_root end end end diff --git a/rswag-api/lib/rswag/api/middleware.rb b/rswag-api/lib/rswag/api/middleware.rb index 77a3b01c..0c70eaa0 100644 --- a/rswag-api/lib/rswag/api/middleware.rb +++ b/rswag-api/lib/rswag/api/middleware.rb @@ -13,14 +13,14 @@ def initialize(app, config) def call(env) path = env['PATH_INFO'] - filename = "#{@config.resolve_swagger_root(env)}/#{path}" + filename = "#{@config.resolve_openapi_root(env)}/#{path}" if env['REQUEST_METHOD'] == 'GET' && File.file?(filename) - swagger = parse_file(filename) - @config.swagger_filter.call(swagger, env) unless @config.swagger_filter.nil? + openapi = parse_file(filename) + @config.openapi_filter.call(openapi, env) unless @config.openapi_filter.nil? mime = Rack::Mime.mime_type(::File.extname(path), 'text/plain') - headers = { 'Content-Type' => mime }.merge(@config.swagger_headers || {}) - body = unload_swagger(filename, swagger) + headers = { 'Content-Type' => mime }.merge(@config.openapi_headers || {}) + body = unload_openapi(filename, openapi) return [ '200', @@ -50,11 +50,11 @@ def load_json(filename) JSON.parse(File.read(filename)) end - def unload_swagger(filename, swagger) + def unload_openapi(filename, openapi) if /\.ya?ml$/ === filename - YAML.dump(swagger) + YAML.dump(openapi) else - JSON.dump(swagger) + JSON.dump(openapi) end end end diff --git a/rswag-api/spec/rswag/api/fixtures/swagger/v1/swagger.json b/rswag-api/spec/rswag/api/fixtures/openapi/v1/openapi.json similarity index 100% rename from rswag-api/spec/rswag/api/fixtures/swagger/v1/swagger.json rename to rswag-api/spec/rswag/api/fixtures/openapi/v1/openapi.json diff --git a/rswag-api/spec/rswag/api/fixtures/openapi/v1/openapi.yml b/rswag-api/spec/rswag/api/fixtures/openapi/v1/openapi.yml new file mode 100644 index 00000000..ba6019f1 --- /dev/null +++ b/rswag-api/spec/rswag/api/fixtures/openapi/v1/openapi.yml @@ -0,0 +1,12 @@ +openapi: 3.0.0 +info: + version: 1.0.0 + title: API V1 + description: A sample API to illustrate OpenAPI concepts +paths: + /list: + get: + description: Returns a list of stuff + responses: + '200': + description: Successful response diff --git a/rswag-api/spec/rswag/api/fixtures/swagger/v1/swagger.yml b/rswag-api/spec/rswag/api/fixtures/swagger/v1/swagger.yml deleted file mode 100644 index 0757e2a8..00000000 --- a/rswag-api/spec/rswag/api/fixtures/swagger/v1/swagger.yml +++ /dev/null @@ -1,5 +0,0 @@ -swagger: '2.0' -info: - title: API V1 - version: v1 -paths: {} diff --git a/rswag-api/spec/rswag/api/middleware_spec.rb b/rswag-api/spec/rswag/api/middleware_spec.rb index 6f907247..3162948f 100644 --- a/rswag-api/spec/rswag/api/middleware_spec.rb +++ b/rswag-api/spec/rswag/api/middleware_spec.rb @@ -6,9 +6,9 @@ module Api describe Middleware do let(:app) { double('app') } - let(:swagger_root) { File.expand_path('../fixtures/swagger', __FILE__) } + let(:openapi_root) { File.expand_path('../fixtures/openapi', __FILE__) } let(:config) do - Configuration.new.tap { |c| c.swagger_root = swagger_root } + Configuration.new.tap { |c| c.openapi_root = openapi_root } end subject { described_class.new(app, config) } @@ -22,27 +22,27 @@ module Api } end - context 'given a path that maps to an existing swagger file' do - let(:env) { env_defaults.merge('PATH_INFO' => 'v1/swagger.json') } + context 'given a path that maps to an existing openapi file' do + let(:env) { env_defaults.merge('PATH_INFO' => 'v1/openapi.json') } 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 + it 'returns contents of the openapi 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"') end end - context 'when swagger_headers is configured' do - let(:env) { env_defaults.merge('PATH_INFO' => 'v1/swagger.json') } + context 'when openapi_headers is configured' do + let(:env) { env_defaults.merge('PATH_INFO' => 'v1/openapi.json') } context 'replacing the default content type header' do before do - config.swagger_headers = { 'Content-Type' => 'application/json; charset=UTF-8' } + config.openapi_headers = { 'Content-Type' => 'application/json; charset=UTF-8' } end it 'returns a 200 status' do expect(response.length).to eql(3) @@ -56,7 +56,7 @@ module Api context 'adding an additional header' do before do - config.swagger_headers = { 'Access-Control-Allow-Origin' => '*' } + config.openapi_headers = { 'Access-Control-Allow-Origin' => '*' } end it 'returns a 200 status' do expect(response.length).to eql(3) @@ -73,7 +73,7 @@ module Api end end - context "given a path that doesn't map to any swagger file" do + context "given a path that doesn't map to any openapi file" do let(:env) { env_defaults.merge('PATH_INFO' => 'foobar.json') } before do allow(app).to receive(:call).and_return([ '500', {}, [] ]) @@ -84,28 +84,28 @@ module Api end end - context 'when the env contains a specific swagger_root' do + context 'when the env contains a specific openapi_root' do let(:env) do env_defaults.merge( - 'PATH_INFO' => 'v1/swagger.json', + 'PATH_INFO' => 'v1/openapi.json', 'action_dispatch.request.path_parameters' => { - swagger_root: swagger_root + openapi_root: openapi_root } ) end - it 'locates files at the provided swagger_root' do + it 'locates files at the provided openapi_root' do expect(response.length).to eql(3) expect(response[1]).to include( 'Content-Type' => 'application/json') expect(response[2].join).to include('"openapi":"3.0.1"') end end - context 'when a swagger_filter is configured' do + context 'when a openapi_filter is configured' do before do - config.swagger_filter = lambda { |swagger, env| swagger['host'] = env['HTTP_HOST'] } + config.openapi_filter = lambda { |openapi, env| openapi['host'] = env['HTTP_HOST'] } end - let(:env) { env_defaults.merge('PATH_INFO' => 'v1/swagger.json') } + let(:env) { env_defaults.merge('PATH_INFO' => 'v1/openapi.json') } it 'applies the filter prior to serialization' do expect(response.length).to eql(3) @@ -113,15 +113,15 @@ module Api end end - context 'when a path maps to a yaml swagger file' do - let(:env) { env_defaults.merge('PATH_INFO' => 'v1/swagger.yml') } + context 'when a path maps to a yaml openapi file' do + let(:env) { env_defaults.merge('PATH_INFO' => 'v1/openapi.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 + it 'returns contents of the openapi 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') diff --git a/rswag-specs/lib/generators/rspec/swagger_generator.rb b/rswag-specs/lib/generators/rspec/openapi_generator.rb similarity index 88% rename from rswag-specs/lib/generators/rspec/swagger_generator.rb rename to rswag-specs/lib/generators/rspec/openapi_generator.rb index 72991762..61ea1f5d 100644 --- a/rswag-specs/lib/generators/rspec/swagger_generator.rb +++ b/rswag-specs/lib/generators/rspec/openapi_generator.rb @@ -4,7 +4,7 @@ require 'rails/generators' module Rspec - class SwaggerGenerator < ::Rails::Generators::NamedBase + class OpenApiGenerator < ::Rails::Generators::NamedBase source_root File.expand_path('templates', __dir__) def setup diff --git a/rswag-specs/lib/generators/rspec/templates/spec.rb b/rswag-specs/lib/generators/rspec/templates/spec.rb index de3bac5e..f4afcbb7 100644 --- a/rswag-specs/lib/generators/rspec/templates/spec.rb +++ b/rswag-specs/lib/generators/rspec/templates/spec.rb @@ -1,4 +1,4 @@ -require 'swagger_helper' +require 'openapi_helper' RSpec.describe '<%= controller_path %>', type: :request do <% @routes.each do | template, path_item | %> diff --git a/rswag-specs/lib/generators/rswag/specs/install/USAGE b/rswag-specs/lib/generators/rswag/specs/install/USAGE index 38ec3b40..f79489e0 100644 --- a/rswag-specs/lib/generators/rswag/specs/install/USAGE +++ b/rswag-specs/lib/generators/rswag/specs/install/USAGE @@ -1,8 +1,8 @@ Description: - Adds swagger_helper to enable Swagger DSL in integration specs + Adds openapi_helper to enable Swagger DSL in integration specs Example: rails generate rswag:specs:install This will create: - spec/swagger_helper.rb + spec/openapi_helper.rb diff --git a/rswag-specs/lib/generators/rswag/specs/install/install_generator.rb b/rswag-specs/lib/generators/rswag/specs/install/install_generator.rb index 92f9dd86..10894b91 100644 --- a/rswag-specs/lib/generators/rswag/specs/install/install_generator.rb +++ b/rswag-specs/lib/generators/rswag/specs/install/install_generator.rb @@ -7,8 +7,8 @@ module Specs class InstallGenerator < Rails::Generators::Base source_root File.expand_path('templates', __dir__) - def add_swagger_helper - template('swagger_helper.rb', 'spec/swagger_helper.rb') + def add_openapi_helper + template('openapi_helper.rb', 'spec/openapi_helper.rb') 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/openapi_helper.rb similarity index 52% rename from rswag-specs/lib/generators/rswag/specs/install/templates/swagger_helper.rb rename to rswag-specs/lib/generators/rswag/specs/install/templates/openapi_helper.rb index 8f715605..b8c9c959 100644 --- a/rswag-specs/lib/generators/rswag/specs/install/templates/swagger_helper.rb +++ b/rswag-specs/lib/generators/rswag/specs/install/templates/openapi_helper.rb @@ -3,19 +3,19 @@ require 'rails_helper' RSpec.configure do |config| - # Specify a root folder where Swagger JSON files are generated + # Specify a root folder where OpenAPI JSON files are generated # NOTE: If you're using the rswag-api to serve API descriptions, you'll need - # to ensure that it's configured to serve Swagger from the same folder - config.swagger_root = Rails.root.join('swagger').to_s + # to ensure that it's configured to serve OpenAPI from the same folder + config.openapi_root = Rails.root.join('openapi').to_s - # Define one or more Swagger documents and provide global metadata for each one - # When you run the 'rswag:specs:swaggerize' rake task, the complete Swagger will - # be generated at the provided relative path under swagger_root + # Define one or more OpenAPI documents and provide global metadata for each one + # When you run the 'rswag:specs:swaggerize' rake task, the complete OpenAPI will + # be generated at the provided relative path under openapi_root # By default, the operations defined in spec files are added to the first - # 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.yaml' => { + # document below. You can override this behavior by adding a openapi_spec tag to the + # the root example_group in your specs, e.g. describe '...', openapi_spec: 'v2/openapi.json' + config.openapi_specs = { + 'v1/openapi.yaml' => { openapi: '3.0.1', info: { title: 'API V1', @@ -35,9 +35,9 @@ } } - # Specify the format of the output Swagger file when running 'rswag:specs:swaggerize'. - # The swagger_docs configuration option has the filename including format in + # Specify the format of the output OpenAPI file when running 'rswag:specs:swaggerize'. + # The openapi_specs 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 + config.openapi_format = :yaml end diff --git a/rswag-specs/lib/rswag/specs.rb b/rswag-specs/lib/rswag/specs.rb index 1db62a55..e5fa4bda 100644 --- a/rswag-specs/lib/rswag/specs.rb +++ b/rswag-specs/lib/rswag/specs.rb @@ -10,10 +10,10 @@ module Rswag module Specs # Extend RSpec with a swagger-based DSL ::RSpec.configure do |c| - c.add_setting :swagger_root - c.add_setting :swagger_docs + c.add_setting :openapi_root + c.add_setting :openapi_specs c.add_setting :swagger_dry_run - c.add_setting :swagger_format + c.add_setting :openapi_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 ab1317f8..deb86546 100644 --- a/rswag-specs/lib/rswag/specs/configuration.rb +++ b/rswag-specs/lib/rswag/specs/configuration.rb @@ -7,23 +7,23 @@ def initialize(rspec_config) @rspec_config = rspec_config end - def swagger_root - @swagger_root ||= begin - if @rspec_config.swagger_root.nil? - raise ConfigurationError, 'No swagger_root provided. See swagger_helper.rb' + def openapi_root + @openapi_root ||= begin + if @rspec_config.openapi_root.nil? + raise ConfigurationError, 'No openapi_root provided. See openapi_helper.rb' end - @rspec_config.swagger_root + @rspec_config.openapi_root end end - def swagger_docs - @swagger_docs ||= begin - if @rspec_config.swagger_docs.nil? || @rspec_config.swagger_docs.empty? - raise ConfigurationError, 'No swagger_docs defined. See swagger_helper.rb' + def openapi_specs + @openapi_specs ||= begin + if @rspec_config.openapi_specs.nil? || @rspec_config.openapi_specs.empty? + raise ConfigurationError, 'No openapi_specs defined. See openapi_helper.rb' end - @rspec_config.swagger_docs + @rspec_config.openapi_specs end end @@ -35,25 +35,24 @@ def swagger_dry_run @swagger_dry_run = @rspec_config.swagger_dry_run.nil? || @rspec_config.swagger_dry_run 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) + def openapi_format + @openapi_format ||= begin + if @rspec_config.openapi_format.nil? || @rspec_config.openapi_format.empty? + @rspec_config.openapi_format = :json + end + unless [:json, :yaml].include?(@rspec_config.openapi_format) + raise ConfigurationError, "Unknown openapi_format '#{@rspec_config.openapi_format}'" + end - @rspec_config.swagger_format + @rspec_config.openapi_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] - - swagger_docs[name] - end + def get_openapi_spec(name) + return openapi_specs.values.first if name.nil? + raise ConfigurationError, "Unknown openapi_spec '#{name}'" unless openapi_specs[name] - def get_swagger_doc_version(name) - doc = get_swagger_doc(name) - doc[:openapi] || doc[:swagger] + openapi_specs[name] end end diff --git a/rswag-specs/lib/rswag/specs/openapi_formatter.rb b/rswag-specs/lib/rswag/specs/openapi_formatter.rb new file mode 100644 index 00000000..bfd4bca4 --- /dev/null +++ b/rswag-specs/lib/rswag/specs/openapi_formatter.rb @@ -0,0 +1,172 @@ +# frozen_string_literal: true + +require 'active_support/core_ext/hash/deep_merge' +require 'rspec/core/formatters/base_text_formatter' +require 'openapi_helper' + +module Rswag + module Specs + class OpenApiFormatter < ::RSpec::Core::Formatters::BaseTextFormatter + ActiveSupport::Deprecation.warn('Rswag::Specs: WARNING: Support for Ruby 2.6 will be dropped in v3.0') if RUBY_VERSION.start_with? '2.6' + + if RSPEC_VERSION > 2 + ::RSpec::Core::Formatters.register self, :example_group_finished, :stop + else + ActiveSupport::Deprecation.warn('Rswag::Specs: WARNING: Support for RSpec 2.X will be dropped in v3.0') + end + + def initialize(output, config = Rswag::Specs.config) + @output = output + @config = config + + @output.puts 'Generating OpenAPI spec...' + end + + def example_group_finished(notification) + metadata = if RSPEC_VERSION > 2 + notification.group.metadata + else + notification.metadata + end + # metadata[:document] has to be explicitly false to skip generating docs + return if metadata[:document] == false + return unless metadata.key?(:response) + + openapi_spec = @config.get_openapi_spec(metadata[:openapi_spec]) + if !doc_version(openapi_spec).start_with?('3') + raise ConfigurationError, "Unsupported OpenAPI version" + end + + # This is called multiple times per file! + # metadata[:operation] is also re-used between examples within file + # therefore be careful NOT to modify its content here. + upgrade_request_type!(metadata) + upgrade_response_produces!(openapi_spec, metadata) + + openapi_spec.deep_merge!(metadata_to_openapi(metadata)) + end + + def stop(_notification = nil) + @config.openapi_specs.each do |url_path, doc| + doc[:paths]&.each_pair do |_k, v| + v.each_pair do |_verb, value| + is_hash = value.is_a?(Hash) + if is_hash && value[:parameters] + schema_param = value[:parameters]&.find { |p| (p[:in] == :body || p[:in] == :formData) && p[:schema] } + mime_list = value[:consumes] || doc[:consumes] + if value && schema_param && mime_list + value[:requestBody] = { content: {} } unless value.dig(:requestBody, :content) + value[:requestBody][:required] = true if schema_param[:required] + value[:requestBody][:description] = schema_param[:description] if schema_param[:description] + examples = value.dig(:request_examples) + mime_list.each do |mime| + value[:requestBody][:content][mime] = { schema: schema_param[:schema] } + if examples + value[:requestBody][:content][mime][:examples] ||= {} + examples.map do |example| + value[:requestBody][:content][mime][:examples][example[:name]] = { + summary: example[:summary] || value[:summary], + value: example[:value] + } + end + end + end + end + + value[:parameters].reject! { |p| p[:in] == :body || p[:in] == :formData } + end + remove_invalid_operation_keys!(value) + end + end + + file_path = File.join(@config.openapi_root, url_path) + dirname = File.dirname(file_path) + FileUtils.mkdir_p dirname unless File.exist?(dirname) + + File.open(file_path, 'w') do |file| + file.write(pretty_generate(doc)) + end + + @output.puts "OpenAPI doc generated at #{file_path}" + end + end + + private + + def pretty_generate(doc) + if @config.openapi_format == :yaml + clean_doc = yaml_prepare(doc) + YAML.dump(clean_doc) + else # config errors are thrown in 'def openapi_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_openapi(metadata) + response_code = metadata[:response][:code] + response = metadata[:response].reject { |k, _v| k == :code } + + verb = metadata[:operation][:verb] + operation = metadata[:operation] + .reject { |k, _v| k == :verb } + .merge(responses: { response_code => response }) + + path_template = metadata[:path_item][:template] + path_item = metadata[:path_item] + .reject { |k, _v| k == :template } + .merge(verb => operation) + + { paths: { path_template => path_item } } + end + + def doc_version(doc) + doc[:openapi] + end + + def upgrade_response_produces!(openapi_spec, metadata) + # Accept header + mime_list = Array(metadata[:operation][:produces] || openapi_spec[:produces]) + target_node = metadata[:response] + upgrade_content!(mime_list, target_node) + metadata[:response].delete(:schema) + end + + def upgrade_content!(mime_list, target_node) + schema = target_node[:schema] + return if mime_list.empty? || schema.nil? + + target_node[:content] ||= {} + mime_list.each do |mime_type| + # TODO: upgrade to have content-type specific schema + (target_node[:content][mime_type] ||= {}).merge!(schema: schema) + end + end + + def upgrade_request_type!(metadata) + # No deprecation here as it seems valid to allow type as a shorthand + operation_nodes = metadata[:operation][:parameters] || [] + path_nodes = metadata[:path_item][:parameters] || [] + header_node = metadata[:response][:headers] || {} + + (operation_nodes + path_nodes + [header_node]).each do |node| + if node && node[:type] && node[:schema].nil? + node[:schema] = { type: node[:type] } + node.delete(:type) + end + end + end + + def remove_invalid_operation_keys!(value) + is_hash = value.is_a?(Hash) + value.delete(:consumes) if is_hash && value[:consumes] + value.delete(:produces) if is_hash && value[:produces] + value.delete(:request_examples) if is_hash && value[:request_examples] + end + end + end +end diff --git a/rswag-specs/lib/rswag/specs/railtie.rb b/rswag-specs/lib/rswag/specs/railtie.rb index 0644f3c0..5dbea19b 100644 --- a/rswag-specs/lib/rswag/specs/railtie.rb +++ b/rswag-specs/lib/rswag/specs/railtie.rb @@ -8,7 +8,7 @@ class Railtie < ::Rails::Railtie end generators do - require 'generators/rspec/swagger_generator.rb' + require 'generators/rspec/openapi_generator.rb' end end end diff --git a/rswag-specs/lib/rswag/specs/request_factory.rb b/rswag-specs/lib/rswag/specs/request_factory.rb index 50de1dd7..d0ea439a 100644 --- a/rswag-specs/lib/rswag/specs/request_factory.rb +++ b/rswag-specs/lib/rswag/specs/request_factory.rb @@ -12,35 +12,35 @@ def initialize(config = ::Rswag::Specs.config) end def build_request(metadata, example) - swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc]) - parameters = expand_parameters(metadata, swagger_doc, example) + openapi_spec = @config.get_openapi_spec(metadata[:openapi_spec]) + parameters = expand_parameters(metadata, openapi_spec, example) {}.tap do |request| add_verb(request, metadata) - add_path(request, metadata, swagger_doc, parameters, example) - add_headers(request, metadata, swagger_doc, parameters, example) + add_path(request, metadata, openapi_spec, parameters, example) + add_headers(request, metadata, openapi_spec, parameters, example) add_payload(request, parameters, example) end end private - def expand_parameters(metadata, swagger_doc, example) + def expand_parameters(metadata, openapi_spec, example) operation_params = metadata[:operation][:parameters] || [] path_item_params = metadata[:path_item][:parameters] || [] - security_params = derive_security_params(metadata, swagger_doc) + security_params = derive_security_params(metadata, openapi_spec) # NOTE: Use of + instead of concat to avoid mutation of the metadata object (operation_params + path_item_params + security_params) - .map { |p| p['$ref'] ? resolve_parameter(p['$ref'], swagger_doc) : p } + .map { |p| p['$ref'] ? resolve_parameter(p['$ref'], openapi_spec) : p } .uniq { |p| p[:name] } .reject { |p| p[:required] == false && !example.respond_to?(p[:name]) } end - def derive_security_params(metadata, swagger_doc) - requirements = metadata[:operation][:security] || swagger_doc[:security] || [] + def derive_security_params(metadata, openapi_spec) + requirements = metadata[:operation][:security] || openapi_spec[:security] || [] scheme_names = requirements.flat_map(&:keys) - schemes = security_version(scheme_names, swagger_doc) + schemes = security_version(scheme_names, openapi_spec) schemes.map do |scheme| param = (scheme[:type] == :apiKey) ? scheme.slice(:name, :in) : { name: 'Authorization', in: :header } @@ -48,81 +48,43 @@ def derive_security_params(metadata, swagger_doc) end end - def security_version(scheme_names, swagger_doc) - if doc_version(swagger_doc).start_with?('2') - (swagger_doc[:securityDefinitions] || {}).slice(*scheme_names).values - else # Openapi3 - if swagger_doc.key?(:securityDefinitions) - ActiveSupport::Deprecation.warn('Rswag::Specs: WARNING: securityDefinitions is replaced in OpenAPI3! Rename to components/securitySchemes (in swagger_helper.rb)') - swagger_doc[:components] ||= { securitySchemes: swagger_doc[:securityDefinitions] } - swagger_doc.delete(:securityDefinitions) - end - components = swagger_doc[:components] || {} - (components[:securitySchemes] || {}).slice(*scheme_names).values - end + def security_version(scheme_names, openapi_spec) + components = openapi_spec[:components] || {} + (components[:securitySchemes] || {}).slice(*scheme_names).values end - def resolve_parameter(ref, swagger_doc) - key = key_version(ref, swagger_doc) - definitions = definition_version(swagger_doc) + def resolve_parameter(ref, openapi_spec) + key = key_version(ref, openapi_spec) + definitions = definition_version(openapi_spec) raise "Referenced parameter '#{ref}' must be defined" unless definitions && definitions[key] definitions[key] end - def key_version(ref, swagger_doc) - if doc_version(swagger_doc).start_with?('2') - ref.sub('#/parameters/', '').to_sym - else # Openapi3 - if ref.start_with?('#/parameters/') - ActiveSupport::Deprecation.warn('Rswag::Specs: WARNING: #/parameters/ refs are replaced in OpenAPI3! Rename to #/components/parameters/') - ref.sub('#/parameters/', '').to_sym - else - ref.sub('#/components/parameters/', '').to_sym - end - end + def key_version(ref, openapi_spec) + ref.sub('#/components/parameters/', '').to_sym end - def definition_version(swagger_doc) - if doc_version(swagger_doc).start_with?('2') - swagger_doc[:parameters] - else # Openapi3 - if swagger_doc.key?(:parameters) - ActiveSupport::Deprecation.warn('Rswag::Specs: WARNING: parameters is replaced in OpenAPI3! Rename to components/parameters (in swagger_helper.rb)') - swagger_doc[:parameters] - else - components = swagger_doc[:components] || {} - components[:parameters] - end - end + def definition_version(openapi_spec) + components = openapi_spec[:components] || {} + components[:parameters] end def add_verb(request, metadata) request[:verb] = metadata[:operation][:verb] end - def base_path_from_servers(swagger_doc, use_server = :default) - return '' if swagger_doc[:servers].nil? || swagger_doc[:servers].empty? - server = swagger_doc[:servers].first + def base_path_from_servers(openapi_spec, use_server = :default) + return '' if openapi_spec[:servers].nil? || openapi_spec[:servers].empty? + server = openapi_spec[:servers].first variables = {} server.fetch(:variables, {}).each_pair { |k,v| variables[k] = v[use_server] } base_path = server[:url].gsub(/\{(.*?)\}/) { |name| variables[name.to_sym] } URI(base_path).path end - def add_path(request, metadata, swagger_doc, parameters, example) - open_api_3_doc = doc_version(swagger_doc).start_with?('3') - uses_base_path = swagger_doc[:basePath].present? - - if open_api_3_doc && uses_base_path - ActiveSupport::Deprecation.warn('Rswag::Specs: WARNING: basePath is replaced in OpenAPI3! Update your swagger_helper.rb') - end - - if uses_base_path - template = (swagger_doc[:basePath] || '') + metadata[:path_item][:template] - else # OpenAPI 3 - template = base_path_from_servers(swagger_doc) + metadata[:path_item][:template] - end + def add_path(request, metadata, openapi_spec, parameters, example) + template = base_path_from_servers(openapi_spec) + metadata[:path_item][:template] request[:path] = template.tap do |path_template| parameters.select { |p| p[:in] == :path }.each do |p| @@ -135,16 +97,16 @@ def add_path(request, metadata, swagger_doc, parameters, example) parameters.select { |p| p[:in] == :query }.each_with_index do |p, i| path_template.concat(i.zero? ? '?' : '&') - path_template.concat(build_query_string_part(p, example.send(p[:name]), swagger_doc)) + path_template.concat(build_query_string_part(p, example.send(p[:name]), openapi_spec)) end end end - def build_query_string_part(param, value, swagger_doc) + def build_query_string_part(param, value, openapi_spec) name = param[:name] - # OAS 3: https://swagger.io/docs/specification/serialization/ - if swagger_doc && doc_version(swagger_doc).start_with?('3') && param[:schema] + # NOTE: https://swagger.io/docs/specification/serialization/ + if param[:schema] style = param[:style]&.to_sym || :form explode = param[:explode].nil? ? true : param[:explode] @@ -194,27 +156,27 @@ def build_query_string_part(param, value, swagger_doc) end end - def add_headers(request, metadata, swagger_doc, parameters, example) + def add_headers(request, metadata, openapi_spec, parameters, example) tuples = parameters .select { |p| p[:in] == :header } .map { |p| [p[:name], example.send(p[:name]).to_s] } # Accept header - produces = metadata[:operation][:produces] || swagger_doc[:produces] + produces = metadata[:operation][:produces] || openapi_spec[:produces] if produces accept = example.respond_to?(:Accept) ? example.send(:Accept) : produces.first tuples << ['Accept', accept] end # Content-Type header - consumes = metadata[:operation][:consumes] || swagger_doc[:consumes] + consumes = metadata[:operation][:consumes] || openapi_spec[:consumes] if consumes content_type = example.respond_to?(:'Content-Type') ? example.send(:'Content-Type') : consumes.first tuples << ['Content-Type', content_type] end # Host header - host = metadata[:operation][:host] || swagger_doc[:host] + host = metadata[:operation][:host] || openapi_spec[:host] if host.present? host = example.respond_to?(:'Host') ? example.send(:'Host') : host tuples << ['Host', host] @@ -270,7 +232,7 @@ def build_json_payload(parameters, example) end def doc_version(doc) - doc[:openapi] || doc[:swagger] || '3' + doc[:openapi] end end diff --git a/rswag-specs/lib/rswag/specs/response_validator.rb b/rswag-specs/lib/rswag/specs/response_validator.rb index 3c6ca3c7..835fa31e 100644 --- a/rswag-specs/lib/rswag/specs/response_validator.rb +++ b/rswag-specs/lib/rswag/specs/response_validator.rb @@ -13,11 +13,11 @@ def initialize(config = ::Rswag::Specs.config) end def validate!(metadata, response) - swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc]) + openapi_spec = @config.get_openapi_spec(metadata[:openapi_spec]) validate_code!(metadata, response) validate_headers!(metadata, response.headers) - validate_body!(metadata, swagger_doc, response.body) + validate_body!(metadata, openapi_spec, response.body) end private @@ -41,7 +41,7 @@ def validate_headers!(metadata, headers) is_nullable = nullable_attribute.nil? ? false : nullable_attribute is_required = required_attribute.nil? ? true : required_attribute - if headers.exclude?(name.to_s) && is_required + if !headers.include?(name.to_s) && is_required raise UnexpectedResponse, "Expected response header #{name} to be present" end @@ -51,12 +51,11 @@ def validate_headers!(metadata, headers) end end - def validate_body!(metadata, swagger_doc, body) + def validate_body!(metadata, openapi_spec, body) response_schema = metadata[:response][:schema] return if response_schema.nil? - version = @config.get_swagger_doc_version(metadata[:swagger_doc]) - schemas = definitions_or_component_schemas(swagger_doc, version) + schemas = { components: openapi_spec[:components] } validation_schema = response_schema .merge('$schema' => 'http://tempuri.org/rswag/specs/extended_schema') @@ -69,20 +68,6 @@ def validate_body!(metadata, swagger_doc, body) "Expected response body to match schema: #{errors.join("\n")}\n" \ "Response body: #{JSON.pretty_generate(JSON.parse(body))}" end - - def definitions_or_component_schemas(swagger_doc, version) - if version.start_with?('2') - swagger_doc.slice(:definitions) - else # Openapi3 - if swagger_doc.key?(:definitions) - ActiveSupport::Deprecation.warn('Rswag::Specs: WARNING: definitions is replaced in OpenAPI3! Rename to components/schemas (in swagger_helper.rb)') - swagger_doc.slice(:definitions) - else - components = swagger_doc[:components] || {} - { components: components } - end - end - end end class UnexpectedResponse < StandardError; end diff --git a/rswag-specs/lib/rswag/specs/swagger_formatter.rb b/rswag-specs/lib/rswag/specs/swagger_formatter.rb deleted file mode 100644 index 16bc03e3..00000000 --- a/rswag-specs/lib/rswag/specs/swagger_formatter.rb +++ /dev/null @@ -1,217 +0,0 @@ -# frozen_string_literal: true - -require 'active_support/core_ext/hash/deep_merge' -require 'rspec/core/formatters/base_text_formatter' -require 'swagger_helper' - -module Rswag - module Specs - class SwaggerFormatter < ::RSpec::Core::Formatters::BaseTextFormatter - ActiveSupport::Deprecation.warn('Rswag::Specs: WARNING: Support for Ruby 2.6 will be dropped in v3.0') if RUBY_VERSION.start_with? '2.6' - - if RSPEC_VERSION > 2 - ::RSpec::Core::Formatters.register self, :example_group_finished, :stop - else - ActiveSupport::Deprecation.warn('Rswag::Specs: WARNING: Support for RSpec 2.X will be dropped in v3.0') - end - - def initialize(output, config = Rswag::Specs.config) - @output = output - @config = config - - @output.puts 'Generating Swagger docs ...' - end - - def example_group_finished(notification) - metadata = if RSPEC_VERSION > 2 - notification.group.metadata - else - notification.metadata - end - - # !metadata[:document] won't work, since nil means we should generate - # docs. - return if metadata[:document] == false - return unless metadata.key?(:response) - - swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc]) - - unless doc_version(swagger_doc).start_with?('2') - # This is called multiple times per file! - # metadata[:operation] is also re-used between examples within file - # therefore be careful NOT to modify its content here. - upgrade_request_type!(metadata) - upgrade_servers!(swagger_doc) - upgrade_oauth!(swagger_doc) - upgrade_response_produces!(swagger_doc, metadata) - end - - swagger_doc.deep_merge!(metadata_to_swagger(metadata)) - end - - def stop(_notification = nil) - @config.swagger_docs.each do |url_path, doc| - unless doc_version(doc).start_with?('2') - doc[:paths]&.each_pair do |_k, v| - v.each_pair do |_verb, value| - is_hash = value.is_a?(Hash) - if is_hash && value[:parameters] - schema_param = value[:parameters]&.find { |p| (p[:in] == :body || p[:in] == :formData) && p[:schema] } - mime_list = value[:consumes] || doc[:consumes] - if value && schema_param && mime_list - value[:requestBody] = { content: {} } unless value.dig(:requestBody, :content) - value[:requestBody][:required] = true if schema_param[:required] - value[:requestBody][:description] = schema_param[:description] if schema_param[:description] - examples = value.dig(:request_examples) - mime_list.each do |mime| - value[:requestBody][:content][mime] = { schema: schema_param[:schema] } - if examples - value[:requestBody][:content][mime][:examples] ||= {} - examples.map do |example| - value[:requestBody][:content][mime][:examples][example[:name]] = { - summary: example[:summary] || value[:summary], - value: example[:value] - } - end - end - end - end - - value[:parameters].reject! { |p| p[:in] == :body || p[:in] == :formData } - end - remove_invalid_operation_keys!(value) - end - end - end - - file_path = File.join(@config.swagger_root, url_path) - dirname = File.dirname(file_path) - FileUtils.mkdir_p dirname unless File.exist?(dirname) - - File.open(file_path, 'w') do |file| - file.write(pretty_generate(doc)) - end - - @output.puts "Swagger doc generated at #{file_path}" - end - end - - 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 } - - verb = metadata[:operation][:verb] - operation = metadata[:operation] - .reject { |k, _v| k == :verb } - .merge(responses: { response_code => response }) - - path_template = metadata[:path_item][:template] - path_item = metadata[:path_item] - .reject { |k, _v| k == :template } - .merge(verb => operation) - - { paths: { path_template => path_item } } - end - - def doc_version(doc) - doc[:openapi] || doc[:swagger] || '3' - end - - def upgrade_response_produces!(swagger_doc, metadata) - # Accept header - mime_list = Array(metadata[:operation][:produces] || swagger_doc[:produces]) - target_node = metadata[:response] - upgrade_content!(mime_list, target_node) - metadata[:response].delete(:schema) - end - - def upgrade_content!(mime_list, target_node) - schema = target_node[:schema] - return if mime_list.empty? || schema.nil? - - target_node[:content] ||= {} - mime_list.each do |mime_type| - # TODO: upgrade to have content-type specific schema - (target_node[:content][mime_type] ||= {}).merge!(schema: schema) - end - end - - def upgrade_request_type!(metadata) - # No deprecation here as it seems valid to allow type as a shorthand - operation_nodes = metadata[:operation][:parameters] || [] - path_nodes = metadata[:path_item][:parameters] || [] - header_node = metadata[:response][:headers] || {} - - (operation_nodes + path_nodes + [header_node]).each do |node| - if node && node[:type] && node[:schema].nil? - node[:schema] = { type: node[:type] } - node.delete(:type) - end - end - end - - def upgrade_servers!(swagger_doc) - return unless swagger_doc[:servers].nil? && swagger_doc.key?(:schemes) - - ActiveSupport::Deprecation.warn('Rswag::Specs: WARNING: schemes, host, and basePath are replaced in OpenAPI3! Rename to array of servers[{url}] (in swagger_helper.rb)') - - swagger_doc[:servers] = { urls: [] } - swagger_doc[:schemes].each do |scheme| - swagger_doc[:servers][:urls] << scheme + '://' + swagger_doc[:host] + swagger_doc[:basePath] - end - - swagger_doc.delete(:schemes) - swagger_doc.delete(:host) - swagger_doc.delete(:basePath) - end - - def upgrade_oauth!(swagger_doc) - # find flow in securitySchemes (securityDefinitions will have been re-written) - schemes = swagger_doc.dig(:components, :securitySchemes) - return unless schemes&.any? { |_k, v| v.key?(:flow) } - - schemes.each do |name, v| - next unless v.key?(:flow) - - ActiveSupport::Deprecation.warn("Rswag::Specs: WARNING: securityDefinitions flow is replaced in OpenAPI3! Rename to components/securitySchemes/#{name}/flows[] (in swagger_helper.rb)") - flow = swagger_doc[:components][:securitySchemes][name].delete(:flow).to_s - if flow == 'accessCode' - ActiveSupport::Deprecation.warn("Rswag::Specs: WARNING: securityDefinitions accessCode is replaced in OpenAPI3! Rename to clientCredentials (in swagger_helper.rb)") - flow = 'authorizationCode' - end - if flow == 'application' - ActiveSupport::Deprecation.warn("Rswag::Specs: WARNING: securityDefinitions application is replaced in OpenAPI3! Rename to authorizationCode (in swagger_helper.rb)") - flow = 'clientCredentials' - end - flow_elements = swagger_doc[:components][:securitySchemes][name].except(:type).each_with_object({}) do |(k, _v), a| - a[k] = swagger_doc[:components][:securitySchemes][name].delete(k) - end - swagger_doc[:components][:securitySchemes][name].merge!(flows: { flow => flow_elements }) - end - end - - def remove_invalid_operation_keys!(value) - is_hash = value.is_a?(Hash) - value.delete(:consumes) if is_hash && value[:consumes] - value.delete(:produces) if is_hash && value[:produces] - value.delete(:request_examples) if is_hash && value[:request_examples] - end - end - end -end diff --git a/rswag-specs/spec/generators/rspec/swagger_generator_spec.rb b/rswag-specs/spec/generators/rspec/openapi_generator_spec.rb similarity index 89% rename from rswag-specs/spec/generators/rspec/swagger_generator_spec.rb rename to rswag-specs/spec/generators/rspec/openapi_generator_spec.rb index 1349230f..6944f89c 100644 --- a/rswag-specs/spec/generators/rspec/swagger_generator_spec.rb +++ b/rswag-specs/spec/generators/rspec/openapi_generator_spec.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true require 'generator_spec' -require 'generators/rspec/swagger_generator' +require 'generators/rspec/openapi_generator' require 'tmpdir' module Rspec - describe SwaggerGenerator do + describe OpenApiGenerator do include GeneratorSpec::TestCase destination Dir.mktmpdir @@ -18,7 +18,7 @@ module Rspec after(:all) do end - it 'installs the swagger_helper for rspec' do + it 'installs the openapi_helper for rspec' do allow_any_instance_of(Rswag::RouteParser).to receive(:routes).and_return(fake_routes) run_generator ['Posts::CommentsController'] diff --git a/rswag-specs/spec/generators/rswag/specs/install_generator_spec.rb b/rswag-specs/spec/generators/rswag/specs/install_generator_spec.rb index ffd7ddcc..7bc06675 100644 --- a/rswag-specs/spec/generators/rswag/specs/install_generator_spec.rb +++ b/rswag-specs/spec/generators/rswag/specs/install_generator_spec.rb @@ -17,8 +17,8 @@ module Specs run_generator end - it 'installs the swagger_helper for rspec' do - assert_file('spec/swagger_helper.rb') + it 'installs the openapi_helper for rspec' do + assert_file('spec/openapi_helper.rb') end end end diff --git a/rswag-specs/spec/swagger_helper.rb b/rswag-specs/spec/openapi_helper.rb similarity index 88% rename from rswag-specs/spec/swagger_helper.rb rename to rswag-specs/spec/openapi_helper.rb index 434e2371..2d918a53 100644 --- a/rswag-specs/spec/swagger_helper.rb +++ b/rswag-specs/spec/openapi_helper.rb @@ -1,4 +1,4 @@ # frozen_string_literal: true # NOTE: For the specs in this gem, all configuration is completely mocked out -# The file just needs to be present because it gets required by the swagger_formatter +# The file just needs to be present because it gets required by the openapi_formatter diff --git a/rswag-specs/spec/rswag/specs/configuration_spec.rb b/rswag-specs/spec/rswag/specs/configuration_spec.rb index 98345d48..364d66aa 100644 --- a/rswag-specs/spec/rswag/specs/configuration_spec.rb +++ b/rswag-specs/spec/rswag/specs/configuration_spec.rb @@ -8,91 +8,91 @@ module Specs subject { described_class.new(rspec_config) } let(:rspec_config) do - OpenStruct.new(swagger_root: swagger_root, swagger_docs: swagger_docs, swagger_format: swagger_format) + OpenStruct.new(openapi_root: openapi_root, openapi_specs: openapi_specs, openapi_format: openapi_format) end - let(:swagger_root) { 'foobar' } - let(:swagger_docs) do + let(:openapi_root) { 'foobar' } + let(:openapi_specs) do { - 'v1/swagger.json' => { info: { title: 'v1' } }, - 'v2/swagger.json' => { info: { title: 'v2' } } + 'v1/openapi.json' => { info: { title: 'v1' } }, + 'v2/openapi.json' => { info: { title: 'v2' } } } end - let(:swagger_format) { :yaml } + let(:openapi_format) { :yaml } - describe '#swagger_root' do - let(:response) { subject.swagger_root } + describe '#openapi_root' do + let(:response) { subject.openapi_root } context 'provided in rspec config' do it { expect(response).to eq('foobar') } end context 'not provided' do - let(:swagger_root) { nil } + let(:openapi_root) { nil } it { expect { response }.to raise_error ConfigurationError } end end - describe '#swagger_docs' do - let(:response) { subject.swagger_docs } + describe '#openapi_specs' do + let(:response) { subject.openapi_specs } context 'provided in rspec config' do it { expect(response).to be_an_instance_of(Hash) } end context 'not provided' do - let(:swagger_docs) { nil } + let(:openapi_specs) { nil } it { expect { response }.to raise_error ConfigurationError } end context 'provided but empty' do - let(:swagger_docs) { {} } + let(:openapi_specs) { {} } it { expect { response }.to raise_error ConfigurationError } end end - describe '#swagger_format' do - let(:response) { subject.swagger_format } + describe '#openapi_format' do + let(:response) { subject.openapi_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 } + let(:openapi_format) { :xml } it { expect { response }.to raise_error ConfigurationError } end context 'not provided' do - let(:swagger_format) { nil } + let(:openapi_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) } + describe '#get_openapi_spec(tag=nil)' do + let(:openapi_spec) { subject.get_openapi_spec(tag) } context 'no tag provided' do let(:tag) { nil } it 'returns the first doc in rspec config' do - expect(swagger_doc).to eq(info: { title: 'v1' }) + expect(openapi_spec).to eq(info: { title: 'v1' }) end end context 'tag provided' do context 'matching doc' do - let(:tag) { 'v2/swagger.json' } + let(:tag) { 'v2/openapi.json' } it 'returns the matching doc in rspec config' do - expect(swagger_doc).to eq(info: { title: 'v2' }) + expect(openapi_spec).to eq(info: { title: 'v2' }) end end context 'no matching doc' do let(:tag) { 'foobar' } - it { expect { swagger_doc }.to raise_error ConfigurationError } + it { expect { openapi_spec }.to raise_error ConfigurationError } end end end diff --git a/rswag-specs/spec/rswag/specs/example_helpers_spec.rb b/rswag-specs/spec/rswag/specs/example_helpers_spec.rb index d78bf454..62f180d5 100644 --- a/rswag-specs/spec/rswag/specs/example_helpers_spec.rb +++ b/rswag-specs/spec/rswag/specs/example_helpers_spec.rb @@ -10,18 +10,20 @@ module Specs before do subject.extend(ExampleHelpers) allow(Rswag::Specs).to receive(:config).and_return(config) - allow(config).to receive(:get_swagger_doc).and_return(swagger_doc) + allow(config).to receive(:get_openapi_spec).and_return(openapi_spec) stub_const('Rswag::Specs::RAILS_VERSION', 3) end let(:config) { double('config') } - let(:swagger_doc) do + let(:openapi_spec) do { - swagger: '2.0', - securityDefinitions: { - api_key: { - type: :apiKey, - name: 'api_key', - in: :query + openapi: '3.0', + components: { + securitySchemes: { + api_key: { + type: :apiKey, + name: 'api_key', + in: :query + } } } } diff --git a/rswag-specs/spec/rswag/specs/openapi_formatter_spec.rb b/rswag-specs/spec/rswag/specs/openapi_formatter_spec.rb new file mode 100644 index 00000000..a192fb18 --- /dev/null +++ b/rswag-specs/spec/rswag/specs/openapi_formatter_spec.rb @@ -0,0 +1,307 @@ +# frozen_string_literal: true + +require 'rswag/specs/openapi_formatter' +require 'ostruct' + +module Rswag + module Specs + RSpec.describe OpenApiFormatter do + subject { described_class.new(output, config) } + + # Mock out some infrastructure + before do + allow(config).to receive(:openapi_root).and_return(openapi_root) + + allow(ActiveSupport::Deprecation).to receive(:warn) # Silence deprecation output from specs + end + let(:config) { double('config') } + let(:output) { double('output').as_null_object } + let(:openapi_root) { File.expand_path('tmp/openapi', __dir__) } + + describe '#example_group_finished(notification)' do + before do + allow(config).to receive(:get_openapi_spec).and_return(openapi_spec) + subject.example_group_finished(notification) + end + let(:request_examples) { nil } + let(:notification) { OpenStruct.new(group: OpenStruct.new(metadata: api_metadata)) } + let(:api_metadata) do + operation = { verb: :post, summary: 'Creates a blog', parameters: [{ type: :string }] } + if request_examples + operation[:request_examples] = request_examples + end + { + path_item: { template: '/blogs', parameters: [{ type: :string }] }, + operation: operation, + response: response_metadata, + document: document + } + end + let(:response_metadata) { { code: '201', description: 'blog created', headers: { type: :string }, schema: { '$ref' => '#/definitions/blog' } } } + + context 'with the document tag set to false' do + let(:openapi_spec) { { openapi: '3.0' } } + let(:document) { false } + + it 'does not update the openapi doc' do + expect(openapi_spec).to match({ openapi: '3.0' }) + end + end + + context 'with the document tag set to anything but false' do + let(:openapi_spec) { { openapi: '3.0' } } + # anything works, including its absence when specifying responses. + let(:document) { nil } + + it 'converts to openapi and merges into the corresponding openapi doc' do + # TODO: verify, that :schema=>{"$ref"=>"#/definitions/blog"} should be removed + expect(openapi_spec).to match( + { + openapi: "3.0", + paths: { + '/blogs' => { + parameters: [{:schema=>{:type=>:string}}], + post: { + parameters: [{:schema=>{:type=>:string}}], + summary: "Creates a blog", + responses: { + '201' => { + description: "blog created", + headers: {:schema=>{:type=>:string}}}}}}}} + # openapi: '3.0', + # paths: { + # '/blogs' => { + # parameters: [{ type: :string }], + # post: { + # parameters: [{ type: :string }], + # summary: 'Creates a blog', + # responses: { + # '201' => { + # description: 'blog created', + # headers: { type: :string }, + # schema: { '$ref' => '#/definitions/blog'}}}}}} + ) + end + end + end + + describe '#stop' do + before do + FileUtils.rm_r(openapi_root) if File.exist?(openapi_root) + allow(config).to receive(:openapi_specs).and_return( + 'v1/openapi.json' => doc_1, + 'v2/openapi.json' => doc_2 + ) + allow(config).to receive(:openapi_format).and_return(openapi_format) + subject.stop(notification) + end + + let(:doc_1) { { info: { version: 'v1' } } } + let(:doc_2) { { info: { version: 'v2' } } } + let(:openapi_format) { :json } + + let(:notification) { double('notification') } + context 'with default format' do + it 'writes the openapi_spec(s) to file' do + expect(File).to exist("#{openapi_root}/v1/openapi.json") + expect(File).to exist("#{openapi_root}/v2/openapi.json") + expect { JSON.parse(File.read("#{openapi_root}/v2/openapi.json")) }.not_to raise_error + end + end + + context 'with yaml format' do + let(:openapi_format) { :yaml } + + it 'writes the openapi_spec(s) as yaml' do + expect(File).to exist("#{openapi_root}/v1/openapi.json") + expect { JSON.parse(File.read("#{openapi_root}/v1/openapi.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("#{openapi_root}/v1/openapi.json")) }.not_to raise_error + end + end + + context 'with oauth3 upgrades' do + let(:doc_2) do + { + paths: { + '/path/' => { + get: { + summary: 'Retrieve Nested Paths', + tags: ['nested Paths'], + produces: ['application/json'], + consumes: ['application/xml', 'application/json'], + parameters: [{ + in: :body, + schema: { foo: :bar } + }, { + in: :headers + }] + } + } + } + } + end + + it 'removes remaining consumes/produces' do + expect(doc_2[:paths]['/path/'][:get].keys).to eql([:summary, :tags, :parameters, :requestBody]) + end + + it 'duplicates params in: :body to requestBody from consumes list' do + expect(doc_2[:paths]['/path/'][:get][:parameters]).to eql([{ in: :headers }]) + expect(doc_2[:paths]['/path/'][:get][:requestBody]).to eql(content: { + 'application/xml' => { schema: { foo: :bar } }, + 'application/json' => { schema: { foo: :bar } } + }) + end + end + + context 'with oauth3 formData' do + let(:doc_2) do + { + paths: { + '/path/' => { + post: { + summary: 'Retrieve Nested Paths', + tags: ['nested Paths'], + produces: ['application/json'], + consumes: ['multipart/form-data'], + parameters: [{ + in: :formData, + schema: { type: :file } + },{ + in: :headers + }] + } + } + } + } + end + + it 'removes remaining consumes/produces' do + expect(doc_2[:paths]['/path/'][:post].keys).to eql([:summary, :tags, :parameters, :requestBody]) + end + + it 'duplicates params in: :formData to requestBody from consumes list' do + expect(doc_2[:paths]['/path/'][:post][:parameters]).to eql([{ in: :headers }]) + expect(doc_2[:paths]['/path/'][:post][:requestBody]).to eql(content: { + 'multipart/form-data' => { schema: { type: :file } } + }) + end + end + + context 'with descriptions on the body param' do + let(:doc_2) do + { + paths: { + '/path/' => { + post: { + produces: ['application/json'], + consumes: ['application/json'], + parameters: [{ + in: :body, + description: "description", + schema: { type: :number } + }] + } + } + } + } + end + + it 'puts the description in the doc' do + expect(doc_2[:paths]['/path/'][:post][:requestBody][:description]).to eql('description') + end + end + + after do + FileUtils.rm_r(openapi_root) if File.exist?(openapi_root) + end + + + context 'with request examples' do + let(:doc_2) do + { + paths: { + '/path/' => { + post: { + summary: 'Retrieve Nested Paths', + tags: ['nested Paths'], + produces: ['application/json'], + consumes: ['application/json'], + parameters: [{ + in: :body, + schema: { + '$ref': '#/components/schemas/BlogPost' + } + },{ + in: :headers + }], + request_examples: [ + { + name: 'basic', + value: { + some_field: 'Foo' + }, + summary: 'An example' + }, + { + name: 'another_basic', + value: { + some_field: 'Bar' + } + } + ], + } + } + }, + components: { + schemas: { + 'BlogPost' => { + type: 'object', + properties: { + some_field: { + type: 'string', + description: 'description' + } + } + } + } + } + } + end + + it 'removes remaining request_examples' do + expect(doc_2[:paths]['/path/'][:post].keys).to eql([:summary, :tags, :parameters, :requestBody]) + end + + it 'creates requestBody examples' do + expect(doc_2[:paths]['/path/'][:post][:parameters]).to eql([{ in: :headers }]) + expect(doc_2[:paths]['/path/'][:post][:requestBody]).to eql(content: { + 'application/json' => { + schema: { '$ref': '#/components/schemas/BlogPost' }, + examples: { + 'basic' => { + value: { + some_field: 'Foo' + }, + summary: 'An example' + }, + 'another_basic' => { + value: { + some_field: 'Bar' + }, + summary: 'Retrieve Nested Paths' + } + } + } + }) + end + end + + after do + FileUtils.rm_r(openapi_root) if File.exist?(openapi_root) + end + end + end + end +end diff --git a/rswag-specs/spec/rswag/specs/request_factory_spec.rb b/rswag-specs/spec/rswag/specs/request_factory_spec.rb index 9deba4d9..5d275544 100644 --- a/rswag-specs/spec/rswag/specs/request_factory_spec.rb +++ b/rswag-specs/spec/rswag/specs/request_factory_spec.rb @@ -10,10 +10,10 @@ module Specs subject { RequestFactory.new(config) } before do - allow(config).to receive(:get_swagger_doc).and_return(swagger_doc) + allow(config).to receive(:get_openapi_spec).and_return(openapi_spec) end let(:config) { double('config') } - let(:swagger_doc) { { swagger: '2.0' } } + let(:openapi_spec) { { openapi: '3.0' } } let(:example) { double('example') } let(:metadata) do { @@ -125,7 +125,7 @@ module Specs context "'query' parameters of type 'object'" do let(:things) { {'foo': 'bar'} } - let(:swagger_doc) { { swagger: '3.0' } } + let(:openapi_spec) { { openapi: '3.0' } } before do metadata[:operation][:parameters] = [ @@ -184,7 +184,7 @@ module Specs context "'query' parameters of type 'array'" do let(:id) { [3, 4, 5] } - let(:swagger_doc) { { swagger: '3.0' } } + let(:openapi_spec) { { openapi: '3.0' } } before do metadata[:operation][:parameters] = [ @@ -252,7 +252,7 @@ module Specs context "'query' parameters with schema reference" do let(:things) { 'foo' } - let(:swagger_doc) { { swagger: '3.0' } } + let(:openapi_spec) { { openapi: '3.0' } } before do metadata[:operation][:parameters] = [ @@ -406,22 +406,10 @@ module Specs end context 'basic auth' do - context 'swagger 2.0' do - before do - swagger_doc[:securityDefinitions] = { basic: { type: :basic } } - metadata[:operation][:security] = [basic: []] - allow(example).to receive(:Authorization).and_return('Basic foobar') - end - - it "sets 'HTTP_AUTHORIZATION' header to example value" do - expect(request[:headers]).to eq('HTTP_AUTHORIZATION' => 'Basic foobar') - end - end - context 'openapi 3.0.1' do - let(:swagger_doc) { { openapi: '3.0.1' } } + let(:openapi_spec) { { openapi: '3.0.1' } } before do - swagger_doc[:components] = { securitySchemes: { basic: { type: :basic } } } + openapi_spec[:components] = { securitySchemes: { basic: { type: :basic } } } metadata[:operation][:security] = [basic: []] allow(example).to receive(:Authorization).and_return('Basic foobar') end @@ -430,29 +418,12 @@ module Specs expect(request[:headers]).to eq('HTTP_AUTHORIZATION' => 'Basic foobar') end end - - context 'openapi 3.0.1 upgrade notice' do - let(:swagger_doc) { { openapi: '3.0.1' } } - before do - allow(ActiveSupport::Deprecation).to receive(:warn) - swagger_doc[:securityDefinitions] = { basic: { type: :basic } } - metadata[:operation][:security] = [basic: []] - allow(example).to receive(:Authorization).and_return('Basic foobar') - end - - it 'warns the user to upgrade' do - expect(request[:headers]).to eq('HTTP_AUTHORIZATION' => 'Basic foobar') - expect(ActiveSupport::Deprecation).to have_received(:warn) - .with('Rswag::Specs: WARNING: securityDefinitions is replaced in OpenAPI3! Rename to components/securitySchemes (in swagger_helper.rb)') - expect(swagger_doc[:components]).to have_key(:securitySchemes) - end - end end context 'apiKey' do before do - swagger_doc[:securityDefinitions] = { apiKey: { type: :apiKey, name: 'api_key', in: key_location } } - metadata[:operation][:security] = [apiKey: []] + openapi_spec[:components] = { securitySchemes: { api_key: { type: :apiKey, name: 'api_key', in: key_location } } } + metadata[:operation][:security] = [api_key: []] allow(example).to receive(:api_key).and_return('foobar') end @@ -492,7 +463,7 @@ module Specs context 'oauth2' do before do - swagger_doc[:securityDefinitions] = { oauth2: { type: :oauth2, scopes: ['read:blogs'] } } + openapi_spec[:components] = { securitySchemes: { oauth2: { type: :oauth2, flows: { implicit: { scopes: ['read:blogs'] } } } } } metadata[:operation][:security] = [oauth2: ['read:blogs']] allow(example).to receive(:Authorization).and_return('Bearer foobar') end @@ -504,9 +475,18 @@ module Specs context 'paired security requirements' do before do - swagger_doc[:securityDefinitions] = { - basic: { type: :basic }, - api_key: { type: :apiKey, name: 'api_key', in: :query } + openapi_spec[:components] = { + securitySchemes: { + basic: { + type: :http, + scheme: :basic + }, + api_key: { + type: :apiKey, + name: 'api_key', + in: :query + } + } } metadata[:operation][:security] = [{ basic: [], api_key: [] }] allow(example).to receive(:Authorization).and_return('Basic foobar') @@ -533,22 +513,10 @@ module Specs end context 'referenced parameters' do - context 'swagger 2.0' do - before do - swagger_doc[:parameters] = { q1: { name: 'q1', in: :query, type: :string } } - metadata[:operation][:parameters] = [{ '$ref' => '#/parameters/q1' }] - allow(example).to receive(:q1).and_return('foo') - end - - it 'uses the referenced metadata to build the request' do - expect(request[:path]).to eq('/blogs?q1=foo') - end - end - context 'openapi 3.0.1' do - let(:swagger_doc) { { openapi: '3.0.1' } } + let(:openapi_spec) { { openapi: '3.0.1' } } before do - swagger_doc[:components] = { parameters: { q1: { name: 'q1', in: :query, type: :string } } } + openapi_spec[:components] = { parameters: { q1: { name: 'q1', in: :query, type: :string } } } metadata[:operation][:parameters] = [{ '$ref' => '#/components/parameters/q1' }] allow(example).to receive(:q1).and_return('foo') end @@ -557,38 +525,12 @@ module Specs expect(request[:path]).to eq('/blogs?q1=foo') end end - - context 'openapi 3.0.1 upgrade notice' do - let(:swagger_doc) { { openapi: '3.0.1' } } - before do - allow(ActiveSupport::Deprecation).to receive(:warn) - swagger_doc[:parameters] = { q1: { name: 'q1', in: :query, type: :string } } - metadata[:operation][:parameters] = [{ '$ref' => '#/parameters/q1' }] - allow(example).to receive(:q1).and_return('foo') - end - - it 'warns the user to upgrade' do - expect(request[:path]).to eq('/blogs?q1=foo') - expect(ActiveSupport::Deprecation).to have_received(:warn) - .with('Rswag::Specs: WARNING: #/parameters/ refs are replaced in OpenAPI3! Rename to #/components/parameters/') - expect(ActiveSupport::Deprecation).to have_received(:warn) - .with('Rswag::Specs: WARNING: parameters is replaced in OpenAPI3! Rename to components/parameters (in swagger_helper.rb)') - end - end end context 'base path' do - context 'openapi 2.0' do - before { swagger_doc[:basePath] = '/api' } - - it 'prepends to the path' do - expect(request[:path]).to eq('/api/blogs') - end - end - context 'openapi 3.0' do before do - swagger_doc[:servers] = [{ + openapi_spec[:servers] = [{ :url => "https://{defaultHost}", :variables => { :defaultHost => { @@ -602,24 +544,10 @@ module Specs expect(request[:path]).to eq('/blogs') end end - - context 'openapi 3.0 with old config' do - let(:swagger_doc) { {:openapi => '3.0', :basePath => '/blogs' } } - - before do - allow(ActiveSupport::Deprecation).to receive(:warn) - end - - it 'generates the path' do - expect(request[:headers]).to eq({}) - expect(ActiveSupport::Deprecation).to have_received(:warn) - .with('Rswag::Specs: WARNING: basePath is replaced in OpenAPI3! Update your swagger_helper.rb') - end - end end context 'global consumes' do - before { swagger_doc[:consumes] = ['application/xml'] } + before { openapi_spec[:consumes] = ['application/xml'] } it "defaults 'CONTENT_TYPE' to global value(s)" do expect(request[:headers]).to eq('CONTENT_TYPE' => 'application/xml') @@ -628,8 +556,8 @@ module Specs context 'global security requirements' do before do - swagger_doc[:securityDefinitions] = { apiKey: { type: :apiKey, name: 'api_key', in: :query } } - swagger_doc[:security] = [apiKey: []] + openapi_spec[:components] = { securitySchemes: { api_key: { type: :apiKey, name: 'api_key', in: :query } } } + openapi_spec[:security] = [api_key: []] allow(example).to receive(:api_key).and_return('foobar') end diff --git a/rswag-specs/spec/rswag/specs/response_validator_spec.rb b/rswag-specs/spec/rswag/specs/response_validator_spec.rb index b8cffe19..7773a10d 100644 --- a/rswag-specs/spec/rswag/specs/response_validator_spec.rb +++ b/rswag-specs/spec/rswag/specs/response_validator_spec.rb @@ -8,11 +8,10 @@ module Specs subject { ResponseValidator.new(config) } before do - allow(config).to receive(:get_swagger_doc).and_return(swagger_doc) - allow(config).to receive(:get_swagger_doc_version).and_return('2.0') + allow(config).to receive(:get_openapi_spec).and_return(openapi_spec) end let(:config) { double('config') } - let(:swagger_doc) { {} } + let(:openapi_spec) { {} } let(:example) { double('example') } let(:metadata) do { @@ -110,29 +109,11 @@ module Specs end context 'referenced schemas' do - context 'swagger 2.0' do - before do - swagger_doc[:definitions] = { - 'blog' => { - type: :object, - properties: { foo: { type: :string } }, - required: ['foo'] - } - } - metadata[:response][:schema] = { '$ref' => '#/definitions/blog' } - end - - it 'uses the referenced schema to validate the response body' do - expect { call }.to raise_error(/Expected response body/) - end - end - context 'openapi 3.0.1' do context 'components/schemas' do before do allow(ActiveSupport::Deprecation).to receive(:warn) - allow(config).to receive(:get_swagger_doc_version).and_return('3.0.1') - swagger_doc[:components] = { + openapi_spec[:components] = { schemas: { 'blog' => { type: :object, @@ -234,27 +215,6 @@ module Specs end end end - - context 'deprecated definitions' do - before do - allow(ActiveSupport::Deprecation).to receive(:warn) - allow(config).to receive(:get_swagger_doc_version).and_return('3.0.1') - swagger_doc[:definitions] = { - 'blog' => { - type: :object, - properties: { foo: { type: :string } }, - required: ['foo'] - } - } - metadata[:response][:schema] = { '$ref' => '#/definitions/blog' } - end - - it 'warns the user to upgrade' do - expect { call }.to raise_error(/Expected response body/) - expect(ActiveSupport::Deprecation).to have_received(:warn) - .with('Rswag::Specs: WARNING: definitions is replaced in OpenAPI3! Rename to components/schemas (in swagger_helper.rb)') - end - end end end end diff --git a/rswag-specs/spec/rswag/specs/swagger_formatter_spec.rb b/rswag-specs/spec/rswag/specs/swagger_formatter_spec.rb deleted file mode 100644 index 4bb1aff7..00000000 --- a/rswag-specs/spec/rswag/specs/swagger_formatter_spec.rb +++ /dev/null @@ -1,491 +0,0 @@ -# frozen_string_literal: true - -require 'rswag/specs/swagger_formatter' -require 'ostruct' - -module Rswag - module Specs - RSpec.describe SwaggerFormatter do - subject { described_class.new(output, config) } - - # Mock out some infrastructure - before do - allow(config).to receive(:swagger_root).and_return(swagger_root) - - allow(ActiveSupport::Deprecation).to receive(:warn) # Silence deprecation output from specs - end - let(:config) { double('config') } - let(:output) { double('output').as_null_object } - let(:swagger_root) { File.expand_path('tmp/swagger', __dir__) } - - describe '#example_group_finished(notification)' do - before do - allow(config).to receive(:get_swagger_doc).and_return(swagger_doc) - subject.example_group_finished(notification) - end - let(:request_examples) { nil } - let(:notification) { OpenStruct.new(group: OpenStruct.new(metadata: api_metadata)) } - let(:api_metadata) do - operation = { verb: :post, summary: 'Creates a blog', parameters: [{ type: :string }] } - if request_examples - operation[:request_examples] = request_examples - end - { - path_item: { template: '/blogs', parameters: [{ type: :string }] }, - operation: operation, - response: response_metadata, - document: document - } - end - let(:response_metadata) { { code: '201', description: 'blog created', headers: { type: :string }, schema: { '$ref' => '#/definitions/blog' } } } - - context 'with the document tag set to false' do - let(:swagger_doc) { { swagger: '2.0' } } - let(:document) { false } - - it 'does not update the swagger doc' do - expect(swagger_doc).to match({ swagger: '2.0' }) - end - end - - context 'with the document tag set to anything but false' do - let(:swagger_doc) { { swagger: '2.0' } } - # anything works, including its absence when specifying responses. - let(:document) { nil } - - it 'converts to swagger and merges into the corresponding swagger doc' do - expect(swagger_doc).to match( - swagger: '2.0', - paths: { - '/blogs' => { - parameters: [{ type: :string }], - post: { - parameters: [{ type: :string }], - summary: 'Creates a blog', - responses: { - '201' => { - description: 'blog created', - headers: { type: :string }, - schema: { '$ref' => '#/definitions/blog' } - } - } - } - } - } - ) - end - end - - context 'with metadata upgrades for 3.0' do - let(:swagger_doc) do - { - openapi: '3.0.1', - basePath: '/foo', - schemes: ['http', 'https'], - host: 'api.example.com', - produces: ['application/vnd.my_mime', 'application/json'], - components: { - securitySchemes: { - myClientCredentials: { - type: :oauth2, - flow: :application, - token_url: :somewhere - }, - myAuthorizationCode: { - type: :oauth2, - flow: :accessCode, - token_url: :somewhere - }, - myImplicit: { - type: :oauth2, - flow: :implicit, - token_url: :somewhere - } - } - } - } - end - let(:document) { nil } - - it 'converts query and path params, type: to schema: { type: }' do - expect(swagger_doc.slice(:paths)).to match( - paths: { - '/blogs' => { - parameters: [{ schema: { type: :string } }], - post: { - parameters: [{ schema: { type: :string } }], - summary: 'Creates a blog', - responses: { - '201' => { - content: { - 'application/vnd.my_mime' => { - schema: { '$ref' => '#/definitions/blog' } - }, - 'application/json' => { - schema: { '$ref' => '#/definitions/blog' } - } - }, - description: 'blog created', - headers: { schema: { type: :string } } - } - } - } - } - } - ) - end - - context 'with response example' do - let(:response_metadata) do - { - code: '201', - description: 'blog created', - headers: { type: :string }, - content: { 'application/json' => { example: { foo: :bar } } }, - schema: { '$ref' => '#/definitions/blog' } - } - end - - it 'adds example to definition' do - expect(swagger_doc.slice(:paths)).to match( - paths: { - '/blogs' => { - parameters: [{ schema: { type: :string } }], - post: { - parameters: [{ schema: { type: :string } }], - summary: 'Creates a blog', - responses: { - '201' => { - content: { - 'application/vnd.my_mime' => { - schema: { '$ref' => '#/definitions/blog' } - }, - 'application/json' => { - schema: { '$ref' => '#/definitions/blog' }, - example: { foo: :bar } - } - }, - description: 'blog created', - headers: { schema: { type: :string } } - } - } - } - } - } - ) - end - end - - context 'with empty content' do - let(:swagger_doc) do - { - openapi: '3.0.1', - basePath: '/foo', - schemes: ['http', 'https'], - host: 'api.example.com', - components: { - securitySchemes: { - myClientCredentials: { - type: :oauth2, - flow: :application, - token_url: :somewhere - }, - myAuthorizationCode: { - type: :oauth2, - flow: :accessCode, - token_url: :somewhere - }, - myImplicit: { - type: :oauth2, - flow: :implicit, - token_url: :somewhere - } - } - } - } - end - - it 'converts query and path params, type: to schema: { type: }' do - expect(swagger_doc.slice(:paths)).to match( - paths: { - '/blogs' => { - parameters: [{ schema: { type: :string } }], - post: { - parameters: [{ schema: { type: :string } }], - summary: 'Creates a blog', - responses: { - '201' => { - description: 'blog created', - headers: { schema: { type: :string } } - } - } - } - } - } - ) - end - end - - it 'converts basePath, schemas and host to urls' do - expect(swagger_doc.slice(:servers)).to match( - servers: { - urls: ['http://api.example.com/foo', 'https://api.example.com/foo'] - } - ) - end - - it 'upgrades oauth flow to flows' do - expect(swagger_doc.slice(:components)).to match( - components: { - securitySchemes: { - myClientCredentials: { - type: :oauth2, - flows: { - 'clientCredentials' => { - token_url: :somewhere - } - } - }, - myAuthorizationCode: { - type: :oauth2, - flows: { - 'authorizationCode' => { - token_url: :somewhere - } - } - }, - myImplicit: { - type: :oauth2, - flows: { - 'implicit' => { - token_url: :somewhere - } - } - } - } - } - ) - end - end - end - - describe '#stop' do - before do - FileUtils.rm_r(swagger_root) if File.exist?(swagger_root) - allow(config).to receive(:swagger_docs).and_return( - 'v1/swagger.json' => doc_1, - 'v2/swagger.json' => doc_2 - ) - allow(config).to receive(:swagger_format).and_return(swagger_format) - subject.stop(notification) - end - - let(:doc_1) { { info: { version: 'v1' } } } - let(:doc_2) { { info: { version: 'v2' } } } - let(:swagger_format) { :json } - - let(:notification) { double('notification') } - context 'with default format' do - 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 - - context 'with oauth3 upgrades' do - let(:doc_2) do - { - paths: { - '/path/' => { - get: { - summary: 'Retrieve Nested Paths', - tags: ['nested Paths'], - produces: ['application/json'], - consumes: ['application/xml', 'application/json'], - parameters: [{ - in: :body, - schema: { foo: :bar } - }, { - in: :headers - }] - } - } - } - } - end - - it 'removes remaining consumes/produces' do - expect(doc_2[:paths]['/path/'][:get].keys).to eql([:summary, :tags, :parameters, :requestBody]) - end - - it 'duplicates params in: :body to requestBody from consumes list' do - expect(doc_2[:paths]['/path/'][:get][:parameters]).to eql([{ in: :headers }]) - expect(doc_2[:paths]['/path/'][:get][:requestBody]).to eql(content: { - 'application/xml' => { schema: { foo: :bar } }, - 'application/json' => { schema: { foo: :bar } } - }) - end - end - - context 'with oauth3 formData' do - let(:doc_2) do - { - paths: { - '/path/' => { - post: { - summary: 'Retrieve Nested Paths', - tags: ['nested Paths'], - produces: ['application/json'], - consumes: ['multipart/form-data'], - parameters: [{ - in: :formData, - schema: { type: :file } - },{ - in: :headers - }] - } - } - } - } - end - - it 'removes remaining consumes/produces' do - expect(doc_2[:paths]['/path/'][:post].keys).to eql([:summary, :tags, :parameters, :requestBody]) - end - - it 'duplicates params in: :formData to requestBody from consumes list' do - expect(doc_2[:paths]['/path/'][:post][:parameters]).to eql([{ in: :headers }]) - expect(doc_2[:paths]['/path/'][:post][:requestBody]).to eql(content: { - 'multipart/form-data' => { schema: { type: :file } } - }) - end - end - - context 'with descriptions on the body param' do - let(:doc_2) do - { - paths: { - '/path/' => { - post: { - produces: ['application/json'], - consumes: ['application/json'], - parameters: [{ - in: :body, - description: "description", - schema: { type: :number } - }] - } - } - } - } - end - - it 'puts the description in the doc' do - expect(doc_2[:paths]['/path/'][:post][:requestBody][:description]).to eql('description') - end - end - - after do - FileUtils.rm_r(swagger_root) if File.exist?(swagger_root) - end - - - context 'with request examples' do - let(:doc_2) do - { - paths: { - '/path/' => { - post: { - summary: 'Retrieve Nested Paths', - tags: ['nested Paths'], - produces: ['application/json'], - consumes: ['application/json'], - parameters: [{ - in: :body, - schema: { - '$ref': '#/components/schemas/BlogPost' - } - },{ - in: :headers - }], - request_examples: [ - { - name: 'basic', - value: { - some_field: 'Foo' - }, - summary: 'An example' - }, - { - name: 'another_basic', - value: { - some_field: 'Bar' - } - } - ], - } - } - }, - components: { - schemas: { - 'BlogPost' => { - type: 'object', - properties: { - some_field: { - type: 'string', - description: 'description' - } - } - } - } - } - } - end - - it 'removes remaining request_examples' do - expect(doc_2[:paths]['/path/'][:post].keys).to eql([:summary, :tags, :parameters, :requestBody]) - end - - it 'creates requestBody examples' do - expect(doc_2[:paths]['/path/'][:post][:parameters]).to eql([{ in: :headers }]) - expect(doc_2[:paths]['/path/'][:post][:requestBody]).to eql(content: { - 'application/json' => { - schema: { '$ref': '#/components/schemas/BlogPost' }, - examples: { - 'basic' => { - value: { - some_field: 'Foo' - }, - summary: 'An example' - }, - 'another_basic' => { - value: { - some_field: 'Bar' - }, - summary: 'Retrieve Nested Paths' - } - } - } - }) - end - end - - after do - FileUtils.rm_r(swagger_root) if File.exist?(swagger_root) - end - end - 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 0a768c17..2c4d4388 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 @@ -5,10 +5,10 @@ # host) to the corresponding 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 + # (under openapi_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.yaml', 'API V1 Docs' + c.openapi_endpoint '/api-docs/v1/openapi.yaml', 'API V1 Docs' # Add Basic Auth in case your API is private # c.basic_auth_enabled = true diff --git a/rswag-ui/lib/rswag/ui/configuration.rb b/rswag-ui/lib/rswag/ui/configuration.rb index fe4acf0f..6fa107e9 100644 --- a/rswag-ui/lib/rswag/ui/configuration.rb +++ b/rswag-ui/lib/rswag/ui/configuration.rb @@ -25,7 +25,7 @@ def initialize @basic_auth_enabled = false end - def swagger_endpoint(url, name) + def openapi_endpoint(url, name) @config_object[:urls] ||= [] @config_object[:urls] << { url: url, name: name } end diff --git a/rswag-ui/spec/rswag/ui/configuration_spec.rb b/rswag-ui/spec/rswag/ui/configuration_spec.rb index 6e32590a..0d2c3e95 100644 --- a/rswag-ui/spec/rswag/ui/configuration_spec.rb +++ b/rswag-ui/spec/rswag/ui/configuration_spec.rb @@ -3,7 +3,7 @@ require_relative '../../spec_helper' RSpec.describe Rswag::Ui::Configuration do - describe '#swagger_endpoints' + describe '#openapi_endpoints' describe '#basic_auth_enabled' do context 'when unspecified' do diff --git a/rswag/lib/generators/rswag/install/USAGE b/rswag/lib/generators/rswag/install/USAGE index dbaf32e3..6bb148aa 100644 --- a/rswag/lib/generators/rswag/install/USAGE +++ b/rswag/lib/generators/rswag/install/USAGE @@ -1,10 +1,10 @@ Description: - Adds a Swagger to your Rails API + Adds a OpenAPI to your Rails API Example: rails generate rswag:install This will create: - spec/swagger_helper.rb + spec/openapi_helper.rb config/initializers/rswag_api.rb config/initializers/rswag_ui.rb diff --git a/rswag/spec/generators/rswag/specs/install_generator_spec.rb b/rswag/spec/generators/rswag/specs/install_generator_spec.rb index 3a5dfb39..d97307fc 100644 --- a/rswag/spec/generators/rswag/specs/install_generator_spec.rb +++ b/rswag/spec/generators/rswag/specs/install_generator_spec.rb @@ -20,7 +20,7 @@ module Specs end it 'installs spec helper rswag-specs' do - assert_file('spec/swagger_helper.rb') + assert_file('spec/openapi_helper.rb') end it 'installs initializer for rswag-api' do diff --git a/test-app/config/initializers/rswag-api.rb b/test-app/config/initializers/rswag-api.rb index 4d72f687..a0771717 100644 --- a/test-app/config/initializers/rswag-api.rb +++ b/test-app/config/initializers/rswag-api.rb @@ -1,14 +1,14 @@ Rswag::Api.configure do |c| - # Specify a root folder where Swagger JSON files are located - # This is used by the Swagger middleware to serve requests for API descriptions - # NOTE: If you're using rswag-specs to generate Swagger, you'll need to ensure + # Specify a root folder where OpenAPI JSON files are located + # This is used by the OpenAPI middleware to serve requests for API descriptions + # NOTE: If you're using rswag-specs to generate OpenAPI, you'll need to ensure # that it's configured to generate files in the same folder - c.swagger_root = Rails.root.to_s + '/swagger' + c.openapi_root = Rails.root.to_s + '/openapi' - # Inject a lambda function to alter the returned Swagger prior to serialization + # Inject a lambda function to alter the returned OpenAPI prior to serialization # The function will have access to the rack env for the current request # For example, you could leverage this to dynamically assign the "host" property # - #c.swagger_filter = lambda { |swagger, env| swagger['host'] = env['HTTP_HOST'] } + #c.openapi_filter = lambda { |openapi, env| openapi['host'] = env['HTTP_HOST'] } end diff --git a/test-app/config/initializers/rswag-ui.rb b/test-app/config/initializers/rswag-ui.rb index 084a5123..40916636 100644 --- a/test-app/config/initializers/rswag-ui.rb +++ b/test-app/config/initializers/rswag-ui.rb @@ -1,10 +1,10 @@ Rswag::Ui.configure do |c| - # List the Swagger endpoints that you want to be documented through the swagger-ui + # List the OpenAPI 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, + # NOTE: If you're using rspec-api to expose OpenAPI files (under openapi_root) as 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.openapi_endpoint '/api-docs/v1/openapi.json', 'API V1 Docs' end diff --git a/test-app/spec/integration/auth_tests_spec.rb b/test-app/spec/integration/auth_tests_spec.rb index dfe59cdc..5dba3c94 100644 --- a/test-app/spec/integration/auth_tests_spec.rb +++ b/test-app/spec/integration/auth_tests_spec.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -require 'swagger_helper' +require 'openapi_helper' -RSpec.describe 'Auth Tests API', type: :request, swagger_doc: 'v1/swagger.json' do +RSpec.describe 'Auth Tests API', type: :request, openapi_spec: 'v1/openapi.json' do before do allow(ActiveSupport::Deprecation).to receive(:warn) # Silence deprecation output from specs end diff --git a/test-app/spec/integration/blogs_spec.rb b/test-app/spec/integration/blogs_spec.rb index 684c0419..406dd58b 100644 --- a/test-app/spec/integration/blogs_spec.rb +++ b/test-app/spec/integration/blogs_spec.rb @@ -1,6 +1,6 @@ -require 'swagger_helper' +require 'openapi_helper' -RSpec.describe 'Blogs API', type: :request, swagger_doc: 'v1/swagger.json' do +RSpec.describe 'Blogs API', type: :request, openapi_spec: 'v1/openapi.json' do let(:api_key) { 'fake_key' } before do diff --git a/test-app/spec/integration/openapi3_spec.rb b/test-app/spec/integration/openapi3_spec.rb index 6a265e5d..129f3425 100644 --- a/test-app/spec/integration/openapi3_spec.rb +++ b/test-app/spec/integration/openapi3_spec.rb @@ -1,24 +1,23 @@ -require 'swagger_helper' -require 'rswag/specs/swagger_formatter' +require 'openapi_helper' +require 'rswag/specs/openapi_formatter' # This spec file validates OpenAPI output generated by spec metadata. -# Specifically here, we look at OpenApi 3 as documented at # https://swagger.io/docs/specification/about/ -RSpec.describe 'Generated OpenApi', type: :request, swagger_doc: 'v3/openapi.json' do +RSpec.describe 'Generated OpenApi', type: :request, openapi_spec: 'v3/openapi.json' do before do |example| output = double('output').as_null_object - swagger_root = File.expand_path('tmp/swagger', __dir__) - config = double('config', swagger_root: swagger_root, get_swagger_doc: swagger_doc ) - formatter = Rswag::Specs::SwaggerFormatter.new(output, config) + openapi_root = File.expand_path('tmp/openapi', __dir__) + config = double('config', openapi_root: openapi_root, get_openapi_spec: openapi_spec) + formatter = Rswag::Specs::OpenApiFormatter.new(output, config) example_group = OpenStruct.new(group: OpenStruct.new(metadata: example.metadata)) formatter.example_group_finished(example_group) end # Framework definition, to be overridden for contexts - let(:swagger_doc) do - { # That which would be defined in swagger_helper.rb + let(:openapi_spec) do + { # That which would be defined in openapi_helper.rb openapi: api_openapi, info: {}, servers: api_servers, @@ -42,7 +41,7 @@ run_test! it 'lists server' do - tree = swagger_doc.dig(:servers) + tree = openapi_spec.dig(:servers) expect(tree).to eq([ { url: "https://api.example.com/foo" } ]) @@ -55,7 +54,7 @@ ]} it 'lists servers' do - tree = swagger_doc.dig(:servers) + tree = openapi_spec.dig(:servers) expect(tree).to eq([ { url: "https://api.example.com/foo" }, { url: "http://api.example.com/foo" } @@ -74,7 +73,7 @@ }]} it 'lists server and variables' do - tree = swagger_doc.dig(:servers) + tree = openapi_spec.dig(:servers) expect(tree).to eq([{ url: "https://{defaultHost}/foo", variables: { @@ -102,7 +101,7 @@ it 'declares output as application/json' do pending "Not yet implemented?" - tree = swagger_doc.dig(:paths, "/stubs", :get, :responses, '200', :content) + tree = openapi_spec.dig(:paths, "/stubs", :get, :responses, '200', :content) expect(tree).to have_key('application/json') end end @@ -129,13 +128,13 @@ run_test! it 'declares parameter in path' do - tree = swagger_doc.dig(:paths, "/stubs/{a_param}", :get, :parameters) + tree = openapi_spec.dig(:paths, "/stubs/{a_param}", :get, :parameters) expect(tree.first[:name]).to eq('a_param') expect(tree.first[:in]).to eq(:path) end it 'declares path parameters as required' do - tree = swagger_doc.dig(:paths, "/stubs/{a_param}", :get, :parameters) + tree = openapi_spec.dig(:paths, "/stubs/{a_param}", :get, :parameters) expect(tree.first[:required]).to eq(true) end end @@ -159,7 +158,7 @@ run_test! it 'declares parameter in query string' do - tree = swagger_doc.dig(:paths, "/stubs", :get, :parameters) + tree = openapi_spec.dig(:paths, "/stubs", :get, :parameters) expect(tree.first[:name]).to eq('a_param') expect(tree.first[:in]).to eq(:query) end @@ -195,7 +194,7 @@ it 'declares requestBody is required' do pending "This output is massaged in SwaggerFormatter#stop, and isn't quite ready here to assert" - tree = swagger_doc.dig(:paths, "/stubs", :post, :requestBody) + tree = openapi_spec.dig(:paths, "/stubs", :post, :requestBody) expect(tree[:required]).to eq(true) end end diff --git a/test-app/spec/swagger_helper.rb b/test-app/spec/openapi_helper.rb similarity index 86% rename from test-app/spec/swagger_helper.rb rename to test-app/spec/openapi_helper.rb index cadb5927..0a5e2e0f 100644 --- a/test-app/spec/swagger_helper.rb +++ b/test-app/spec/openapi_helper.rb @@ -3,19 +3,19 @@ require 'rails_helper' RSpec.configure do |config| - # Specify a root folder where Swagger JSON files are generated + # Specify a root folder where OpenAPI JSON files are generated # NOTE: If you're using the rswag-api to serve API descriptions, you'll need - # to ensure that it's configured to serve Swagger from the same folder - config.swagger_root = Rails.root.to_s + '/swagger' + # to ensure that it's configured to serve OpenAPI from the same folder + config.openapi_root = Rails.root.to_s + '/openapi' config.swagger_dry_run = false - # Define one or more Swagger documents and provide global metadata for each one - # When you run the 'rswag:specs:to_swagger' rake task, the complete Swagger will - # be generated at the provided relative path under swagger_root + # Define one or more OpenAPI documents and provide global metadata for each one + # When you run the 'rswag:specs:to_swagger' rake task, the complete OpenAPI will + # be generated at the provided relative path under openapi_root # By default, the operations defined in spec files are added to the first - # 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' => { + # document below. You can override this behavior by adding a openapi_spec tag to the + # the root example_group in your specs, e.g. describe '...', openapi_spec: 'v2/openapi.json' + config.openapi_specs = { + 'v1/openapi.json' => { openapi: '3.0.0', info: { title: 'API V1', diff --git a/test-app/spec/rake/rswag_specs_swaggerize_spec.rb b/test-app/spec/rake/rswag_specs_swaggerize_spec.rb index dab7da2f..af47b5b7 100644 --- a/test-app/spec/rake/rswag_specs_swaggerize_spec.rb +++ b/test-app/spec/rake/rswag_specs_swaggerize_spec.rb @@ -4,14 +4,14 @@ require 'rake' RSpec.describe 'rswag:specs:swaggerize' do - let(:swagger_root) { Rails.root.to_s + '/swagger' } + let(:openapi_root) { Rails.root.to_s + '/openapi' } before do TestApp::Application.load_tasks - FileUtils.rm_r(swagger_root) if File.exist?(swagger_root) + FileUtils.rm_r(openapi_root) if File.exist?(openapi_root) end - it 'generates Swagger JSON files from integration specs' do + it 'generates OpenAPI JSON files from integration specs' do Rake::Task['rswag:specs:swaggerize'].invoke - expect(File).to exist("#{swagger_root}/v1/swagger.json") + expect(File).to exist("#{openapi_root}/v1/openapi.json") end end diff --git a/test-app/swagger/v1/swagger.json b/test-app/swagger/v1/openapi.json similarity index 100% rename from test-app/swagger/v1/swagger.json rename to test-app/swagger/v1/openapi.json