diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 000000000..b844b143d
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1 @@
+Gemfile.lock
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 57df306a5..8aa984dd1 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -1,3 +1,11 @@
+---
+name: 🐛 Bug
+about: File a bug/issue with Rswag that is not working
+title: '[BUG]
'
+labels: Bug, Needs Triage
+assignees: ''
+---
+
## Describe the bug
A clear and concise description of what the bug is.
@@ -13,5 +21,14 @@ If applicable, add screenshots to help explain your problem.
## Additional context
Add any other context about the problem here.
-## Rswag Version
-The version of rswag are you using.
+## Dependency versions
+The version of are you using for:
+* Rswag:
+* RSpec:
+* Rails:
+* Ruby:
+
+## Relates to which version of OAS (OpenAPI Specification)
+- [ ] OAS2
+- [ ] OAS3
+- [ ] OAS3.1
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 000000000..ec4bb386b
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1 @@
+blank_issues_enabled: false
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
index bde1cd080..6b7a05db9 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -1,11 +1,29 @@
+---
+name: 🙏 Request
+about: Request something from the community
+title: '[REQUEST] '
+labels: Request, Needs Triage
+assignees: ''
+---
+
## Is your feature request related to a problem? Please describe.
A clear and concise description of what the problem is.
## Describe the solution you'd like
A clear and concise description of what you want to happen.
+## What support could we give you, so you could implement this yourself?
+Please tell us how could we help you implement this feature, so you could
+become a contributor. **Our time is limited, so it's unlikely that we will
+be able to implement this ourselves, but we will try to help you**
+
## Describe alternatives you've considered
A clear and concise description of any alternative solutions or features you've considered.
## Additional context
-Add any other context or screenshots about the feature request here.
\ No newline at end of file
+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/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md
deleted file mode 100644
index 73c44a1db..000000000
--- a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md
+++ /dev/null
@@ -1,15 +0,0 @@
-## Problem
-A clear and concise description of what the problem is.
-
-## Solution
-A clear and concise description of what the solution is.
-
-### Related Issues
-Links to any related issues.
-
-### Checklist
-- [ ] Added tests
-- [ ] Changelog updated
-
-### Steps to Test or Reproduce
-Outline the steps to test or reproduce the PR here.
\ No newline at end of file
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 000000000..5ace4600a
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,6 @@
+version: 2
+updates:
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "weekly"
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 000000000..a46a1221e
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,26 @@
+## Problem
+A clear and concise description of what the problem is.
+
+## Solution
+A clear and concise description of what the solution is.
+
+### This concerns this parts of the OpenAPI Specification:
+* [EXAMPLE_LINK TO RELEVANT OPEN API SPECS PAGE](https://spec.openapis.org/oas/v3.1.0#data-types)
+* [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
+
+### Related Issues
+Links to any related issues.
+
+### Checklist
+- [ ] Added tests
+- [ ] Changelog updated
+- [ ] Added documentation to README.md
+- [ ] Added example of using the enhancement into [test-app](https://github.com/rswag/rswag/tree/master/test-app)
+
+### Steps to Test or Reproduce
+Outline the steps to test or reproduce the PR here.
diff --git a/.github/stale.yml b/.github/stale.yml
new file mode 100644
index 000000000..45fcf69fd
--- /dev/null
+++ b/.github/stale.yml
@@ -0,0 +1,19 @@
+# Number of days of inactivity before an issue becomes stale
+# ToDo: reduce to 90 days once issue backlog is under control
+daysUntilStale: 365
+# Number of days of inactivity before a stale issue is closed
+daysUntilClose: 14
+# Issues with these labels will never be considered stale
+exemptLabels:
+ - pinned
+ - security
+# Label to use when marking an issue as stale
+staleLabel: wontfix
+# Comment to post when marking an issue as stale. Set to `false` to disable
+markComment: >
+ This issue has been automatically marked as stale because it has not had
+ recent activity. It will be closed if no further activity occurs. If the issue is
+ still relevant to you, please leave a comment stating so to keep the issue
+ from being closed. Thank you for your contributions.
+# Comment to post when closing a stale issue. Set to `false` to disable
+closeComment: false
diff --git a/.github/workflows/deploy-rswag.yml b/.github/workflows/deploy-rswag.yml
new file mode 100644
index 000000000..dba56e3e7
--- /dev/null
+++ b/.github/workflows/deploy-rswag.yml
@@ -0,0 +1,78 @@
+name: Publish Rswag Gems
+
+on:
+ workflow_dispatch:
+ inputs:
+ version:
+ description: "gem version to publish"
+ type: string
+ required: true
+
+jobs:
+ build:
+ name: Build + Publish
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up Ruby 3.3
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+
+ - name: Publish rswag-api
+ run: |
+ mkdir -p $HOME/.gem
+ touch $HOME/.gem/credentials
+ chmod 0600 $HOME/.gem/credentials
+ printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
+ cd rswag-api
+ gem build rswag-api.gemspec
+ gem push *.gem
+ env:
+ GEM_HOST_API_KEY: "${{secrets.BOOKOFGREG_RUBYGEMS_API_KEY}}"
+ RUBYGEMS_VERSION: "${{github.event.inputs.version}}"
+
+ - name: Publish rswag-specs
+ run: |
+ mkdir -p $HOME/.gem
+ touch $HOME/.gem/credentials
+ chmod 0600 $HOME/.gem/credentials
+ printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
+ cd rswag-specs
+ gem build rswag-specs.gemspec
+ gem push *.gem
+ env:
+ GEM_HOST_API_KEY: "${{secrets.BOOKOFGREG_RUBYGEMS_API_KEY}}"
+ RUBYGEMS_VERSION: "${{github.event.inputs.version}}"
+
+ - name: Publish rswag-ui
+ run: |
+ mkdir -p $HOME/.gem
+ touch $HOME/.gem/credentials
+ chmod 0600 $HOME/.gem/credentials
+ printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
+ cd rswag-ui
+ npm install
+ gem build rswag-ui.gemspec
+ gem push *.gem
+ env:
+ GEM_HOST_API_KEY: "${{secrets.BOOKOFGREG_RUBYGEMS_API_KEY}}"
+ RUBYGEMS_VERSION: "${{github.event.inputs.version}}"
+
+ - name: Publish rswag
+ run: |
+ mkdir -p $HOME/.gem
+ touch $HOME/.gem/credentials
+ chmod 0600 $HOME/.gem/credentials
+ printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
+ cd rswag
+ gem build rswag.gemspec
+ echo $RUBYGEMS_VERSION
+ gem push *.gem
+ env:
+ GEM_HOST_API_KEY: "${{secrets.BOOKOFGREG_RUBYGEMS_API_KEY}}"
+ RUBYGEMS_VERSION: "${{github.event.inputs.version}}"
diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml
index beb315c6a..36b8538cc 100644
--- a/.github/workflows/ruby.yml
+++ b/.github/workflows/ruby.yml
@@ -10,18 +10,71 @@ jobs:
test:
runs-on: ubuntu-latest
strategy:
+ fail-fast: false
matrix:
- ruby: [2.6, 2.7, truffleruby-head]
- rails: [5.2.4.4, 6.0.3.4]
+ # Ruby 2.6 became EOL on 2022-03-31; kept for information only
+ # Ruby 2.7 became EOL on 2023-03-31; kept for information only
+ # Ruby 3.0 became EOL on 2024-04-23; kept for information only
+ ruby: ['2.6', '2.7', '3.0', '3.1', '3.2', '3.3']
+
+ # Rails 5.2 became EOL on 2022-06-01; kept for information only
+ # Rails 6.0 became EOL on 2023-06-01; kept for information only
+ # Rails 6.1 became EOL on 2024-10-01; kept for information only
+ rails: ['5.2.8.1', '6.0.6.1', '6.1.7.8', '7.0.8.4', '7.1.3.4', '7.2.0', '8.0.0']
+
+ exclude:
+ # Excludes Rails 5.2 on Ruby 3.0+
+ # Excludes Rails 6.0 on Ruby 3.0+
+ # Excludes Rails 7.0 on Ruby 2.6
+ # Excludes Rails 7.1 on Ruby 2.6
+ # Excludes Rails 7.2 on Ruby 2.6, 2.7, 3.0
+ # Excludes Rails 8.0 on Ruby 2.6, 2.7, 3.0, 3.1
+ - rails: '5.2.8.1'
+ ruby: '3.0'
+ - rails: '5.2.8.1'
+ ruby: '3.1'
+ - rails: '5.2.8.1'
+ ruby: '3.2'
+ - rails: '5.2.8.1'
+ ruby: '3.3'
+ - rails: '6.0.6.1'
+ ruby: '3.0'
+ - rails: '6.0.6.1'
+ ruby: '3.1'
+ - rails: '6.0.6.1'
+ ruby: '3.2'
+ - rails: '6.0.6.1'
+ ruby: '3.3'
+ - rails: '7.0.8.4'
+ ruby: '2.6'
+ - rails: '7.1.3.4'
+ ruby: '2.6'
+ - rails: '7.2.0'
+ ruby: '2.6'
+ - rails: '7.2.0'
+ ruby: '2.7'
+ - rails: '7.2.0'
+ ruby: '3.0'
+ - rails: '8.0.0'
+ ruby: '2.6'
+ - rails: '8.0.0'
+ ruby: '2.7'
+ - rails: '8.0.0'
+ ruby: '3.0'
+ - rails: '8.0.0'
+ ruby: '3.1'
+ name: Ruby ${{ matrix.ruby }}, Rails ${{ matrix.rails }}
env:
RAILS_VERSION: ${{ matrix.rails }}
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
- with: { ruby-version: 2.6 }
+ with:
+ ruby-version: ${{ matrix.ruby }}
+ bundler-cache: true # runs 'bundle install' and caches installed gems automatically
- - uses: actions/cache@v2
+ - uses: actions/cache@v4
id: cache
with:
path: |
@@ -31,6 +84,11 @@ jobs:
- name: Install dependencies
run: |
+ if [ $(cut -d '.' -f 1 <<< "${{ matrix.ruby }}") -lt 3 ]; then
+ gem update --system 3.4.22
+ else
+ gem update --system
+ fi
bundle install
cd rswag-ui && npm install
diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/spellcheck.yml
new file mode 100644
index 000000000..008cdc04e
--- /dev/null
+++ b/.github/workflows/spellcheck.yml
@@ -0,0 +1,18 @@
+name: Spellchecking
+
+on:
+ workflow_dispatch:
+ pull_request:
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ spellchecking:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: streetsidesoftware/cspell-action@v5
+ with:
+ config: "./cspell.json"
diff --git a/.gitignore b/.gitignore
index 181022d5b..87f26358b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,4 @@
Gemfile.lock
/.idea/
**/.byebug_history
+**/coverage
diff --git a/.ruby-version b/.ruby-version
index 37c2961c2..49cdd668e 100644
--- a/.ruby-version
+++ b/.ruby-version
@@ -1 +1 @@
-2.7.2
+2.7.6
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index f987c52d0..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,73 +0,0 @@
-language: ruby
-
-dist: bionic
-services:
- - xvfb
-
-rvm:
- - 2.6.3
-
-env:
- - RAILS_VERSION=6.0.0
- - RAILS_VERSION=5.2.0
-
-addons:
- apt:
- packages:
- - libqtwebkit-dev
- - libqtwebkit4
-
-cache:
- directories:
- - /home/travis/.rvm/gems/ruby-2.6.3
-
-install: ./ci/build.sh
-
-script: ./ci/test.sh
-
-jobs:
- include:
- - stage: publish components
- script: 'cd rswag-api'
- if: tag IS present
- deploy:
- gemspec: rswag-api.gemspec
- provider: rubygems
- api_key: $RUBYGEMS_API_KEY
- on:
- branch: master
- tags: true
-
- - stage: publish components
- script: 'cd rswag-specs'
- if: tag IS present
- deploy:
- gemspec: rswag-specs.gemspec
- provider: rubygems
- api_key: $RUBYGEMS_API_KEY
- on:
- branch: master
- tags: true
-
- - stage: publish components
- script: 'cd rswag-ui'
- if: tag IS present
- deploy:
- gemspec: rswag-ui.gemspec
- provider: rubygems
- api_key: $RUBYGEMS_API_KEY
- skip_cleanup: true
- on:
- branch: master
- tags: true
-
- - stage: publish rswag
- script: 'cd rswag'
- if: tag IS present
- deploy:
- gemspec: rswag.gemspec
- provider: rubygems
- api_key: $RUBYGEMS_API_KEY
- on:
- branch: master
- tags: true
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 19df54dde..0e4c1a4e6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,72 +1,333 @@
# rswag
+
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
+## Added
+
+## Changed
+
+## Fixed
+
+## [2.16.0] - 2024-11-13
+
+## Changed
+
+- Update rails dependency in gemspec to support Rails 8.0 (https://github.com/rswag/rswag/pull/797)
+
+## [2.15.0] - 2024-10-04
+
+### Added
+
+- Define extra options for properties (https://github.com/rswag/rswag/pull/783)
+
+### Changed
+
+- Relaxed the dependency on json-schema to make v5 available.
+
+### Fixed
+
+- Suppress deprecation warning when strict setting is not set (https://github.com/rswag/rswag/pull/785)
+- Allow vendor-specific MIME types for JSON payloads (https://github.com/rswag/rswag/pull/769)
+- Fix escaping of schema in path parameters for openapi spec >= 3.0.0 (https://github.com/rswag/rswag/pull/725)
+
+## [2.14.0] - 2024-08-13
+
+### Added
+
+- Tell Dependabot to update GitHub Actions (https://github.com/rswag/rswag/pull/707)
+- Rails 7.2 support [#765](https://github.com/rswag/rswag/pull/765)
+- Add support for support per-enum-value descriptions (https://github.com/rswag/rswag/pull/429)
+- Add support for text/plain body format (https://github.com/rswag/rswag/pull/639)
+
+### Fixed
+
+- Update README to fix broken link to the JSON-Schema website [#715](https://github.com/rswag/rswag/pull/715)
+- fix: rubygems-update version for Docker (https://github.com/rswag/rswag/pull/724)
+- fix: rubygems-update for github actions (https://github.com/rswag/rswag/pull/730)
+
+### Documentation
+
+- Added more details about config swagger format [#698](https://github.com/rswag/rswag/pull/698)
+
+## [2.13.0] - 2023-11-29
+
+### Added
+
+- Add deprecation warnings for `Rswag::Api` configuration (https://github.com/rswag/rswag/pull/702)
+
+### Fixed
+
+- Fix deprecation warnings for `Rswag::Specs` configuration (https://github.com/rswag/rswag/pull/702)
+
+## [2.12.0] - 2023-11-25
+
+### Changed
+
+- Relaxed the dependency on json-schema, allowing for updates including support for allPropertiesRequired and noPropertiesRequired options (https://github.com/rswag/rswag/pull/659)
+
+### Fixed
+
+- Add missing link to Content Security Policy (https://github.com/rswag/rswag/pull/619)
+- Fix it's vs its typo (https://github.com/rswag/rswag/pull/689)
+
+### Added
+
+- Add warning about methods renaming (https://github.com/rswag/rswag/pull/688)
+
+### Changed
+
+- Bump "swagger-ui-dist" to "5.9.4" in rswag-ui (https://github.com/rswag/rswag/pull/670)
+
+### Documentation
+
+## [2.11.0] - 2023-10-11
+
+### Added
+
+- Rails 7.1 support (https://github.com/rswag/rswag/pull/677)
+
+## [2.10.1] - 2023-07-13
+
+### Fixed
+
+- Fix path expansion (https://github.com/rswag/rswag/pull/660)
+
+## [2.10.0] - 2023-07-13
+
+### Fixed
+
+- Sanitize directory traversal in middleware (https://github.com/rswag/rswag/pull/654)
+- Fix encoding of query params (https://github.com/rswag/rswag/pull/621)
+- Fix support for string body params (https://github.com/rswag/rswag/pull/639)
+
+### Added
+
+- Allow passing metadata to HTTP verb methods (https://github.com/rswag/rswag/pull/628)
+- Added configuration for RuboCop RSpec to improve detection of RSpec examples and example groups (https://github.com/rswag/rswag/pull/632)
+
+### Changed
+
+### Fixed
+
+### Documentation
+
+## [2.9.0] - 2023-04-24
+
+### Added
+
+- Added option --spec_path to the generator command with requests as default value (https://github.com/rswag/rswag/pull/607)
+- Add support for `:getter` parameter option to explicitly define custom parameter getter method and avoid RSpec conflicts with `include` matcher and `status` method (https://github.com/rswag/rswag/pull/605)
+- Added support strict schema validation and allow to pass metadata to run_test! (https://github.com/rswag/rswag/pull/604)
+- Add support for passing a custom specification description to `run_test!` (https://github.com/rswag/rswag/pull/622)
+
+### Changed
+
+- Remove commented code (https://github.com/rswag/rswag/pull/576)
+
+### Fixed
+
+- Invalid URI error when specifying protocol within server configuration (https://github.com/rswag/rswag/pull/591)
+- Fix ADDITIONAL_RSPEC_OPTS to always apply (https://github.com/rswag/rswag/pull/584)
+
+### Documentation
+
+- Ask for dependency versions in issue template (https://github.com/rswag/rswag/pull/575)
+
+## [2.8.0] - 2022-11-16
+
+### Added
+
+- Add support for nullable & required on header parameters (https://github.com/rswag/rswag/pull/527)
+- Add option to set `Host` in header (https://github.com/rswag/rswag/pull/570)
+- Add Support for Request body examples (https://github.com/rswag/rswag/pull/555)
+
+### Changed
+
+### Fixed
+
+- Fix support for referenced parameter schema https://github.com/rswag/rswag/pull/564)
+
+### Documentation
+
+- Correct method name in ReadMe (https://github.com/rswag/rswag/pull/566)
+
+## [2.7.0] - 2022-10-19
+
+### Added
+
+- Add tooling for measuring test coverage so that changes are safer (https://github.com/rswag/rswag/pull/551)
+- Add CSP compatible with rswag in case the Rails one is not compatible (https://github.com/rswag/rswag/pull/263)
+- Add ADDITIONAL_RSPEC_OPTS env variable (https://github.com/rswag/rswag/pull/556)
+- Add option to set Host header (https://github.com/rswag/rswag/pull/184)
+
+### Changed
+
+- Change default dev tooling setup to Ruby 2.7 and Rails 6 (https://github.com/rswag/rswag/pull/542)
+- Make the development docker user non-root for easier volume sharing (https://github.com/rswag/rswag/pull/550)
+- Update `json-schema` dependency version constraint (https://github.com/rswag/rswag/pull/517)
+- Add deprecation notice for intent to drop support for Ruby 2.6 and RSpec 2 (https://github.com/rswag/rswag/pull/552)
+
+### Fixed
+
+- Fix request body examples (https://github.com/rswag/rswag/pull/555)
+- Corrected method name in README example (https://github.com/rswag/rswag/pull/566)
+- Fix Style/SingleArgumentDig issue in `swagger_formatter` (https://github.com/rswag/rswag/pull/486)
+- Make dependency on rspec-core explicit instead of implied (https://github.com/rswag/rswag/pull/554)
+- Fix base path for OAS3 specification (https://github.com/rswag/rswag/pull/547)
+- Fix ResponseValidator adding support for nullable and required headers (https://github.com/rswag/rswag/pull/527)
+
+### Documentation
+
+## [2.6.0] - 2022-09-09
+
+### Added
+
+- Examples generated with `run_test!` now have the rspec tag `rswag`
+- Add query parameter serialization styles (OAS3) (https://github.com/rswag/rswag/pull/507)
+- Support for adding descriptions in body params (https://github.com/rswag/rswag/pull/422)
+- Display all validation errors instead of only the first (https://github.com/rswag/rswag/pull/461)
+
+## Fixed
+
+- Fixes examples for OAS3 specification, allowing multiple examples (https://github.com/rswag/rswag/pull/501)
+- Fix array parameter serialization on OAS3 (https://github.com/rswag/rswag/pull/507)
+- Fix assorted spelling errors (https://github.com/rswag/rswag/pull/535)
+- Fix null-checking when using a referenced property (https://github.com/rswag/rswag/pull/515)
+
### Changed
-- Update swagger-ui to 3.52.5
+
+- Rename generated `rswag-ui.rb` file to match Ruby style (https://github.com/rswag/rswag/pull/508)
+- Code comment formatting changes (https://github.com/rswag/rswag/pull/487)
+
+### Documentation
+
+- Add Syntax Highlighting to ReadMe (https://github.com/rswag/rswag/pull/525/files)
+- Fix ReadMe response headers example for OpenApi3.0 (https://github.com/rswag/rswag/pull/518)
+- Update TOC in the ReadMe (https://github.com/rswag/rswag/pull/536/files)
+- Fix incorrect sample code for example generation (https://github.com/rswag/rswag/pull/513)
+
+## [2.5.1] - 2022-02-10
+
+### Fixed
+
+- Fixed missing assets in rswag-ui [#493](https://github.com/rswag/rswag/pull/493)
+
+## [2.5.0] - 2022-02-08
+
+### Added
+
+- Update swagger-ui to 3.52.5 [#453](https://github.com/rswag/rswag/pull/453)
+- Added specs print failed body [#406](https://github.com/rswag/rswag/pull/406)
+- Added ability to specify multiple params in short form [#300](https://github.com/rswag/rswag/pull/300)
+- REVERTS #300, help wanted! [#407](https://github.com/rswag/rswag/pull/407)
+- Added better messages for missing lets [#441](https://github.com/rswag/rswag/pull/441)
+- Added Rails 7.0 support [#450](https://github.com/rswag/rswag/pull/450)
+
+### Fixed
+
+- Fixed allowed $refs in components [#404](https://github.com/rswag/rswag/pull/404)
+
+### Documentation
+
+- Documents support for multiple tags [#416](https://github.com/rswag/rswag/pull/416)
+- Documents libv8 troubleshooting [#426](https://github.com/rswag/rswag/pull/426)
+
+### Development
+
+- Development - Replaces TheRubyRacer with mini_racer [#442](https://github.com/rswag/rswag/pull/442)
+- Development - Migrate to GH Action for tests [#475](https://github.com/rswag/rswag/pull/475)
+- Development - Test improvements[#481](https://github.com/rswag/rswag/pull/481)
## [2.4.0] - 2021-02-09
+
### Added
+
- Added `SWAGGER_DRY_RUN` env variable [#274](https://github.com/rswag/rswag/pull/274)
## [2.3.3] - 2021-02-07
### Fixed
+
- Include response examples [#394](https://github.com/rswag/rswag/pull/394)
### Changed
+
- Update swagger-ui to 3.42.0
## [2.3.2] - 2021-01-27
+
### Added
+
- RequestBody now supports the `required` flag [#342](https://github.com/rswag/rswag/pull/342)
+
### Fixed
+
- Fix response example rendering [#330](https://github.com/rswag/rswag/pull/330)
- Fix empty content block [#347](https://github.com/rswag/rswag/pull/347)
## [2.3.1] - 2020-04-08
+
### Fixed
+
- Remove require for byebug [#295](https://github.com/rswag/rswag/issues/295)
## [2.3.0] - 2020-04-05
+
### Added
+
- Support for OpenAPI 3.0 ! [#286](https://github.com/rswag/rswag/pull/286)
- Custom headers in rswag-api [#187](https://github.com/rswag/rswag/pull/187)
-- Allow document: false rspec metatag [#255](https://github.com/rswag/rswag/pull/255)
+- Allow document: false rspec meta-tag [#255](https://github.com/rswag/rswag/pull/255)
- Add parameterized pattern for spec files [#254](https://github.com/rswag/rswag/pull/254)
- Support Basic Auth on rswag-ui [#167](https://github.com/rswag/rswag/pull/167)
### Changed
+
- Update swagger-ui version to 3.23.11 [#239](https://github.com/rswag/rswag/pull/239)
- Rails constraint moved from < 6.1 to < 7 [#253](https://github.com/rswag/rswag/pull/253)
- Swaggerize now outputs base RSpec text on completion to avoid silent failures [#293](https://github.com/rswag/rswag/pull/293)
- Update swagger-ui version to 3.28.0
## [2.2.0] - 2019-11-01
+
### Added
+
- New swagger_format config option for setting YAML output [#251](https://github.com/rswag/rswag/pull/251)
+
### Changed
+
- rswag-api will serve yaml files as yaml [#251](https://github.com/rswag/rswag/pull/251)
## [2.1.1] - 2019-10-18
+
### Fixed
+
- Fix incorrect require reference for swagger_generator [#248](https://github.com/rswag/rswag/issues/248)
## [2.1.0] - 2019-10-17
+
### Added
+
- New Spec Generator [#75](https://github.com/rswag/rswag/pull/75)
- Support for Options and Trace verbs; You must use a framework that supports this, for Options Rails 6.1+ Rails 6 does not support Trace. [#237](https://github.com/rswag/rswag/pull/75)
+
### Changed
+
- Update swagger-ui to 3.18.2 [#240](https://github.com/rswag/rswag/pull/240)
## [2.0.6] - 2019-10-03
+
### Added
+
- Support for Rails 6 [#228](https://github.com/rswag/rswag/pull/228)
- Support for Windows paths [#176](https://github.com/rswag/rswag/pull/176)
+
### Changed
+
- Show response body when error code is not expected [#117](https://github.com/rswag/rswag/pull/177)
## [2.0.5] - 2018-07-10
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index dc7b71936..f614bfdcc 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -7,17 +7,22 @@ We put forward the philosophy put forward by the [react community](https://react
We also strive to achieve [semantic versioning](https://semver.org/) for this repo.
## Fork, then clone the repo:
+
```
git clone git@github.com:rswag/rswag.git
cd rswag
```
## Build
+
Set up your machine:
+
```
./ci/build.sh
```
+
Or manually
+
```
bundle
cd test-app
@@ -30,16 +35,21 @@ cd -
```
## Test
+
Initialize the rswag-ui repo with assets.
+
```
ci/build.sh
```
Make sure the tests pass:
+
```
./ci/test.sh
```
+
or manually
+
```
cd test-app
bundle exec rspec
@@ -61,14 +71,15 @@ Find the latest versions of swagger-ui here:
https://github.com/swagger-api/swagger-ui/releases
Update the swagger-ui-dist version in the rswag-ui dependencies
+
```
./rswag-ui/package.json
```
Navigate to the rswag-ui folder and run npm install to update the package-lock.json
-
## Release
+
(for maintainers)
Update the changelog.md, putting the new version number in and moving the Unreleased marker.
@@ -76,8 +87,9 @@ Update the changelog.md, putting the new version number in and moving the Unrele
Merge the changes into master you wish to release.
Add and push a new git tag, annotated tags preferred:
+
```
git tag -s 2.0.6 -m 'v2.0.6'
```
-Travis will detect the tag and release all gems with that tag version number.
+Then run the GH Action "Publish Rswag Gems" with the correct version number.
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 000000000..c554602b7
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,22 @@
+FROM ruby:2.7
+ENV RAILS_VERSION 7.0.3.1
+
+RUN apt-get update -qq && apt-get install -y nodejs npm
+# Bugfix for https://github.com/rubyjs/mini_racer/issues/220#issuecomment-1010724771
+RUN gem update --system 3.4.22
+
+# Run docker as a non-root user to avoid having to chown generated files while developing
+ENV APP_PATH=/rswag/
+ENV BUNDLE_PATH=/usr/local/bundle
+ARG USER_ID=1000
+RUN useradd -md ${APP_PATH} appuser -u ${USER_ID} && chown appuser:appuser ${APP_PATH}
+USER appuser
+WORKDIR ${APP_PATH}
+
+COPY --chown=appuser:appuser . ${APP_PATH}
+RUN "./ci/build.sh"
+
+# Configure the main process to run when running the image
+EXPOSE 3000
+WORKDIR /rswag/test-app
+CMD ["rails", "server", "-b", "0.0.0.0"]
diff --git a/Gemfile b/Gemfile
index f7ce082a4..de40a2b85 100644
--- a/Gemfile
+++ b/Gemfile
@@ -2,24 +2,26 @@
source 'https://rubygems.org'
-# Allow the rails version to come from an ENV setting so Travis can test multiple versions.
+# Allow the rails version to come from an ENV setting so CI can test multiple versions.
# See http://www.schneems.com/post/50991826838/testing-against-multiple-rails-versions/
-rails_version = ENV['RAILS_VERSION'] || '5.2.4.2'
+rails_version = ENV['RAILS_VERSION'] || '8.0.0'
gem 'rails', rails_version.to_s
-case rails_version.split('.').first
-when '3'
- gem 'strong_parameters'
-when '4', '5', '6'
- gem 'responders'
-end
+gem 'responders'
case rails_version.split('.').first
-when '3', '4', '5'
+when '5'
gem 'sqlite3', '~> 1.3.6'
-when '6'
- gem 'sqlite3', '~> 1.4.1'
+when '6', '7'
+ gem 'sqlite3', '~> 1.4'
+when '8'
+ gem 'sqlite3', '~> 2.2'
+end
+
+case RUBY_VERSION.split('.').first
+when '3'
+ gem 'net-smtp', require: false
end
gem 'rswag-api', path: './rswag-api'
@@ -31,11 +33,13 @@ end
group :test do
gem 'capybara'
+ gem 'climate_control'
gem 'geckodriver-helper'
gem 'generator_spec'
gem 'rspec-rails'
gem 'selenium-webdriver'
gem 'test-unit'
+ gem 'simplecov', '=0.21.2'
end
group :development do
@@ -43,7 +47,6 @@ group :development do
end
group :assets do
- gem 'mini_racer'
gem 'uglifier'
end
diff --git a/Makefile b/Makefile
new file mode 100644
index 000000000..37c208911
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,4 @@
+default: build
+
+build:
+ docker compose build --build-arg=USER_ID=$(shell id -u)
diff --git a/README.md b/README.md
index 1cd4d5f5d..460d4fccd 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,15 @@
+
+
rswag
=========
-[![Build Status](https://travis-ci.org/rswag/rswag.svg?branch=master)](https://travis-ci.org/rswag/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!
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.
-Rswag extends rspec-rails "request specs" with a Swagger-based DSL for describing and testing API operations. You describe your API operations with a succinct, intuitive syntax, and it automaticaly runs the tests. Once you have green tests, run a rake task to auto-generate corresponding Swagger files and expose them as YAML or JSON endpoints. Rswag also provides an embedded version of the awesome [swagger-ui](https://github.com/swagger-api/swagger-ui) that's powered by the exposed file. This toolchain makes it seamless to go from integration specs, which youre probably doing in some form already, to living documentation for your API consumers.
+Rswag extends rspec-rails "request specs" with a Swagger-based DSL for describing and testing API operations. You describe your API operations with a succinct, intuitive syntax, and it automatically runs the tests. Once you have green tests, run a rake task to auto-generate corresponding Swagger files and expose them as YAML or JSON endpoints. Rswag also provides an embedded version of the awesome [swagger-ui](https://github.com/swagger-api/swagger-ui) that's powered by the exposed file. This toolchain makes it seamless to go from integration specs, which you're probably doing in some form already, to living documentation for your API consumers.
Api Rswag creates [Swagger](http://swagger.io) tooling for Rails API's. Generate beautiful API documentation, including a UI to explore and test operations, directly from your rspec integration tests.
@@ -16,21 +18,10 @@ And that's not all ...
Once you have an API that can describe itself in Swagger, you've opened the treasure chest of Swagger-based tools including a client generator that can be targeted to a wide range of popular platforms. See [swagger-codegen](https://github.com/swagger-api/swagger-codegen) for more details.
-## Compatibility ##
-
-|Rswag Version|Swagger (OpenAPI) Spec.|swagger-ui|
-|----------|----------|----------|
-|[master](https://github.com/rswag/rswag/tree/master)|3.0.3|3.52.5|
-|[2.3.0](https://github.com/rswag/rswag/tree/2.3.0)|3.0.3|3.23.11|
-|[2.2.0](https://github.com/rswag/rswag/tree/2.2.0)|2.0|3.18.2|
-|[1.6.0](https://github.com/rswag/rswag/tree/1.6.0)|2.0|2.2.5|
-
-
**Table of Contents**
- [rswag](#rswag)
- - [Compatibility](#compatibility)
- [Getting Started](#getting-started)
- [The rspec DSL](#the-rspec-dsl)
- [Paths, Operations and Responses](#paths-operations-and-responses)
@@ -44,12 +35,14 @@ Once you have an API that can describe itself in Swagger, you've opened the trea
- [Output Location for Generated Swagger Files](#output-location-for-generated-swagger-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)
- [Response headers](#response-headers)
+ - [Nullable or Optional Response Headers](#nullable-or-optional-response-headers)
- [Response examples](#response-examples)
- [Enable auto generation examples from responses](#enable-auto-generation-examples-from-responses)
+ - [Dry Run Option](#dry-run-option)
- [Running tests without documenting](#running-tests-without-documenting)
- [rswag helper methods](#rswag-helper-methods)
- - [rswag response examples](#rswag-response-examples)
- [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)
@@ -104,7 +97,7 @@ Once you have an API that can describe itself in Swagger, you've opened the trea
There is also a generator which can help get you started `rails generate rspec:swagger API::MyController`
```ruby
- # spec/integration/blogs_spec.rb
+ # spec/requests/blogs_spec.rb
require 'swagger_helper'
describe 'Blogs API' do
@@ -141,6 +134,7 @@ There is also a generator which can help get you started `rails generate rspec:s
tags 'Blogs', 'Another Tag'
produces 'application/json', 'application/xml'
parameter name: :id, in: :path, type: :string
+ request_body_example value: { some_field: 'Foo' }, name: 'basic', summary: 'Request example description'
response '200', 'blog found' do
schema type: :object,
@@ -168,7 +162,8 @@ There is also a generator which can help get you started `rails generate rspec:s
end
end
```
-
+By default, the above command will create spec under _spec/requests_ folder. You can pass an option to change this default path as in `rails generate rspec:swagger API::BlogsController --spec_path integration`.
+This will create the spec file _spec/integration/blogs_spec.rb_
4. Generate the Swagger JSON file(s)
@@ -191,7 +186,29 @@ There is also a generator which can help get you started `rails generate rspec:s
If you've used [Swagger](http://swagger.io/specification) before, then the syntax should be very familiar. To describe your API operations, start by specifying a path and then list the supported operations (i.e. HTTP verbs) for that path. Path parameters must be surrounded by curly braces ({}). Within an operation block (see "post" or "get" in the example above), most of the fields supported by the [Swagger "Operation" object](http://swagger.io/specification/#operationObject) are available as methods on the example group. To list (and test) the various responses for an operation, create one or more response blocks. Again, you can reference the [Swagger "Response" object](http://swagger.io/specification/#responseObject) for available fields.
-Take special note of the __run_test!__ method that's called within each response block. This tells rswag to create and execute a corresponding example. It builds and submits a request based on parameter descriptions and corresponding values that have been provided using the rspec "let" syntax. For example, the "post" description in the example above specifies a "body" parameter called "blog". It also lists 2 different responses. For the success case (i.e. the 201 response), notice how "let" is used to set the blog parameter to a value that matches the provided schema. For the failure case (i.e. the 422 response), notice how it's set to a value that does not match the provided schema. When the test is executed, rswag also validates the actual response code and, where applicable, the response body against the provided [JSON Schema](http://json-schema.org/documentation.html).
+Take special note of the __run_test!__ method that's called within each response block. This tells rswag to create and execute a corresponding example. It builds and submits a request based on parameter descriptions and corresponding values that have been provided using the rspec "let" syntax. For example, the "post" description in the example above specifies a "body" parameter called "blog". It also lists 2 different responses. For the success case (i.e. the 201 response), notice how "let" is used to set the blog parameter to a value that matches the provided schema. For the failure case (i.e. the 422 response), notice how it's set to a value that does not match the provided schema. When the test is executed, rswag also validates the actual response code and, where applicable, the response body against the provided [JSON Schema](https://json-schema.org/specification).
+
+If you want to add metadata to the example, you can pass keyword arguments to the __run_test!__ method:
+
+```ruby
+# to run particular test case
+response '201', 'blog created' do
+ run_test! focus: true
+end
+
+# to write vcr cassette
+response '201', 'blog created' do
+ run_test! vcr: true
+end
+```
+
+If you want to customize the description of the generated specification, a description can be passed to **run_test!**
+
+```ruby
+response '201', 'blog created' do
+ run_test! "custom spec description"
+end
+```
If you want to do additional validation on the response, pass a block to the __run_test!__ method:
@@ -220,6 +237,131 @@ response '201', 'blog created' do
end
```
+Also note that the examples generated with __run_test!__ are tagged with the `:rswag` so they can easily be filtered. E.g. `rspec --tag rswag`
+
+### date-time in query parameters
+
+Input sent in queries of Rspec tests is HTML safe, including date-time strings.
+
+```ruby
+parameter name: :date_time, in: :query, type: :string
+
+response '200', 'blog found' do
+ let(:date_time) { DateTime.new(2001, 2, 3, 4, 5, 6, '-7').to_s }
+
+ run_test! do
+ expect(request[:path]).to eq('/blogs?date_time=2001-02-03T04%3A05%3A06-07%3A00')
+ end
+end
+```
+
+### Enum description ###
+If you want to output a description of each enum value, the description can be passed to each value:
+```ruby
+parameter name: :status, in: :query, getter: :blog_status,
+ enum: { 'draft': 'Retrieves draft blogs', 'published': 'Retrieves published blogs', 'archived': 'Retrieves archived blogs' },
+ description: 'Filter by status'
+
+response '200', 'success' do
+ let(:blog_status) { 'published' }
+
+ run_test!
+end
+```
+
+### Schema validations
+
+#### Strict (deprecated)
+It validates required properties and disallows additional properties in response body.
+To enable, you can set the option `openapi_strict_schema_validation` to true.
+It is equal to `openapi_no_additional_properties: true` and `openapi_all_properties_required: true`
+**Important** If you would like to keep validation of required properties but allow additional properties, you can set the `openapi_strict_schema_validation` option to `false` and set `openapi_all_properties_required` to `true` and `openapi_no_additional_properties` to `false`.
+
+```ruby
+# spec/swagger_helper.rb
+RSpec.configure do |config|
+ config.openapi_strict_schema_validation = true # default false
+end
+```
+
+or set the option per individual example:
+
+```ruby
+# using in run_test!
+describe 'Blogs API' do
+ path '/blogs' do
+ post 'Creates a blog' do
+ ...
+ response '201', 'blog created' do
+ let(:blog) { { title: 'foo', content: 'bar' } }
+
+ run_test!(openapi_strict_schema_validation: true)
+ end
+ end
+ end
+end
+
+# using in response block
+describe 'Blogs API' do
+ path '/blogs' do
+ post 'Creates a blog' do
+ ...
+
+ response '201', 'blog created', openapi_strict_schema_validation: true do
+ let(:blog) { { title: 'foo', content: 'bar' } }
+
+ run_test!
+ end
+ end
+ end
+end
+
+# using in an explicit example
+describe 'Blogs API' do
+ path '/blogs' do
+ post 'Creates a blog' do
+ ...
+ response '201', 'blog created' do
+ let(:blog) { { title: 'foo', content: 'bar' } }
+
+ before do |example|
+ submit_request(example.metadata)
+ end
+
+ it 'returns a valid 201 response', openapi_strict_schema_validation: true do |example|
+ assert_response_matches_metadata(example.metadata)
+ end
+ end
+ end
+ end
+end
+```
+
+#### Additional properties
+If you want to disallow additional properties in response body, you can set the option `openapi_no_additional_properties` to true:
+
+```ruby
+# spec/swagger_helper.rb
+RSpec.configure do |config|
+ config.openapi_no_additional_properties = true # default false
+end
+```
+
+You can set similarly the option per individual example as shown in Strict (deprecated) sections.
+
+#### All required properties
+If you want to disallow missing required properties in response body, you can set the `openapi_all_properties_required` option to true:
+**Important** it will allow the additional properties
+
+```ruby
+# spec/swagger_helper.rb
+RSpec.configure do |config|
+ config.openapi_all_properties_required = true # default false
+end
+```
+
+You can set similarly the option per individual example as shown in Strict (deprecated) sections.
+
### Null Values ###
This library is currently using JSON::Draft4 for validation of response models. Nullable properties can be supported with the non-standard property 'x-nullable' to a definition to allow null/nil values to pass. Or you can add the new standard ```nullable``` property to a definition.
@@ -285,9 +427,9 @@ In addition to paths, operations and responses, Swagger also supports global API
```ruby
# spec/swagger_helper.rb
RSpec.configure do |config|
- config.swagger_root = Rails.root.to_s + '/swagger'
+ config.openapi_root = Rails.root.to_s + '/swagger'
- config.swagger_docs = {
+ config.openapi_specs = {
'v1/swagger.json' => {
openapi: '3.0.1',
info: {
@@ -307,7 +449,7 @@ RSpec.configure do |config|
]
},
- 'v2/swagger.yaml' => {
+ 'v2/swagger.json' => {
openapi: '3.0.1',
info: {
title: 'API V2',
@@ -316,8 +458,11 @@ RSpec.configure do |config|
},
servers: [
{
- url: 'https://{defaultHost}',
+ url: '{protocol}://{defaultHost}',
variables: {
+ protocol: {
+ default: :https
+ },
defaultHost: {
default: 'www.example.com'
}
@@ -330,11 +475,11 @@ end
```
#### Supporting multiple versions of API ####
-By default, the paths, operations and responses defined in your spec files will be associated with the first Swagger document in _swagger_helper.rb_. If your API has multiple versions, you should be using separate documents to describe each of them. In order to assign a file with a given version of API, you'll need to add the ```swagger_doc``` tag to each spec specifying its target document name:
+By default, the paths, operations and responses defined in your spec files will be associated with the first Swagger document in _swagger_helper.rb_. If your API has multiple versions, you should be using separate documents to describe each of them. In order to assign a file with a given version of API, you'll need to add the ```openapi_spec``` tag to each spec specifying its target document name:
```ruby
-# spec/integration/v2/blogs_spec.rb
-describe 'Blogs API', swagger_doc: 'v2/swagger.yaml' do
+# spec/requests/v2/blogs_spec.rb
+describe 'Blogs API', openapi_spec: 'v2/swagger.yaml' do
path '/blogs' do
...
@@ -344,21 +489,59 @@ describe 'Blogs API', swagger_doc: 'v2/swagger.yaml' do
end
```
+#### Supporting YAML format ####
+
+By default, the swagger docs are generated in JSON format. If you want to generate them in YAML format, you can specify the swagger format in the swagger_helper.rb file:
+
+```ruby
+# spec/swagger_helper.rb
+RSpec.configure do |config|
+ config.openapi_root = Rails.root.to_s + '/swagger'
+
+ # Use if you want to see which test is running
+ # config.formatter = :documentation
+
+ # Generate swagger docs in YAML format
+ config.openapi_format = :yaml
+
+ config.openapi_specs = {
+ 'v1/swagger.yaml' => {
+ openapi: '3.0.1',
+ info: {
+ title: 'API V1',
+ version: 'v1',
+ description: 'This is the first version of my API'
+ },
+ servers: [
+ {
+ url: 'https://{defaultHost}',
+ variables: {
+ defaultHost: {
+ default: 'www.example.com'
+ }
+ }
+ }
+ ]
+ },
+ }
+end
+```
+
#### Formatting the description literals: ####
Swagger supports the Markdown syntax to format strings. This can be especially handy if you were to provide a long description of a given API version or endpoint. Use [this guide](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet) for reference.
__NOTE:__ There is one difference between the official Markdown syntax and Swagger interpretation, namely tables. To create a table like this:
-| Column1 | Collumn2 |
-| ------- | -------- |
-| cell1 | cell2 |
+| Column1 | Column2 |
+| ------- | ------- |
+| cell1 | cell2 |
-you should use the folowing syntax, making sure there are no whitespaces at the start of any of the lines:
+you should use the following syntax, making sure there is no whitespace at the start of any of the lines:
```
-| Column1 | Collumn2 |
-| ------- | -------- |
+| Column1 | Column2 |
|
+| ------- | ------- |
| cell1 | cell2 |
```
@@ -372,9 +555,9 @@ Swagger supports :basic, :bearer, :apiKey and :oauth2 and :openIdConnect scheme
```ruby
# spec/swagger_helper.rb
RSpec.configure do |config|
- config.swagger_root = Rails.root.to_s + '/swagger'
+ config.openapi_root = Rails.root.to_s + '/swagger'
- config.swagger_docs = {
+ config.openapi_specs = {
'v1/swagger.json' => {
... # note the new Open API 3.0 compliant security structure here, under "components"
components: {
@@ -394,7 +577,7 @@ RSpec.configure do |config|
}
end
-# spec/integration/blogs_spec.rb
+# spec/requests/blogs_spec.rb
describe 'Blogs API' do
path '/blogs' do
@@ -433,7 +616,7 @@ describe 'Auth examples API' do
response '401', 'Invalid credentials' do
let(:Authorization) { "Basic #{::Base64.strict_encode64('jsmith:jspass')}" }
- let(:api_key) { 'barfoo' }
+ let(:api_key) { 'bar-foo' }
run_test!
end
end
@@ -448,13 +631,13 @@ For example, :basic auth is required above and so the :Authorization (header) pa
## Configuration & Customization ##
-The steps described above will get you up and running with minimal setup. However, rswag offers a lot of flexibility to customize as you see fit. Before exploring the various options, you'll need to be aware of it's different components. The following table lists each of them and the files that get added/updated as part of a standard install.
+The steps described above will get you up and running with minimal setup. However, rswag offers a lot of flexibility to customize as you see fit. Before exploring the various options, you'll need to be aware of its different components. The following table lists each of them and the files that get added/updated as part of a standard install.
-|Gem|Description|Added/Updated|
-|---------|-----------|-------------|
-|__rswag-specs__|Swagger-based DSL for rspec & accompanying rake task for generating Swagger files|_spec/swagger_helper.rb_|
-|__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_|
+| Gem | Description | Added/Updated |
+| --------------- | ---------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
+| __rswag-specs__ | Swagger-based DSL for rspec & accompanying rake task for generating Swagger files | _spec/swagger_helper.rb_ |
+| __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 ###
@@ -463,7 +646,7 @@ You can adjust this in the _swagger_helper.rb_ that's installed with __rswag-spe
```ruby
# spec/swagger_helper.rb
RSpec.configure do |config|
- config.swagger_root = Rails.root.to_s + '/your-custom-folder-name'
+ config.openapi_root = Rails.root.to_s + '/your-custom-folder-name'
...
end
```
@@ -479,6 +662,15 @@ By default, rswag will search for integration tests in _spec/requests_, _spec/ap
rake rswag:specs:swaggerize PATTERN="spec/swagger/**/*_spec.rb"
```
+### Additional rspec options
+
+You can add additional rspec parameters using the ADDITIONAL_RSPEC_OPTS env variable:
+
+```ruby
+# Only include tests tagged "rswag"
+rake rswag:specs:swaggerize ADDITIONAL_RSPEC_OPTS="--tag rswag"
+```
+
### Referenced Parameters and Schema Definitions ###
Swagger allows you to describe JSON structures inline with your operation descriptions OR as referenced globals.
@@ -488,7 +680,7 @@ Rather than repeating the schema in every operation spec, you can define it glob
```ruby
# spec/swagger_helper.rb
-config.swagger_docs = {
+config.openapi_specs = {
'v1/swagger.json' => {
openapi: '3.0.0',
info: {
@@ -533,7 +725,7 @@ config.swagger_docs = {
}
}
-# spec/integration/blogs_spec.rb
+# spec/requests/blogs_spec.rb
describe 'Blogs API' do
path '/blogs' do
@@ -547,7 +739,7 @@ describe 'Blogs API' do
...
end
-# spec/integration/comments_spec.rb
+# spec/requests/comments_spec.rb
describe 'Blogs API' do
path '/blogs/{blog_id}/comments' do
@@ -560,13 +752,45 @@ describe 'Blogs API' do
end
```
+### Request examples ###
+
+```ruby
+# spec/integration/blogs_spec.rb
+describe 'Blogs API' do
+
+ path '/blogs/{blog_id}' do
+
+ get 'Retrieves a blog' do
+
+ request_body_example value: { some_field: 'Foo' }, name: 'request_example_1', summary: 'A request example'
+
+ response 200, 'blog found' do
+ ...
+```
+
+to use the actual request from the spec as the example:
+
+```ruby
+config.after(:each, operation: true, use_as_request_example: true) do |spec|
+ spec.metadata[:operation][:request_examples] ||= []
+
+ example = {
+ value: JSON.parse(request.body.string, symbolize_names: true),
+ name: 'request_example_1',
+ summary: 'A request example'
+ }
+
+ spec.metadata[:operation][:request_examples] << example
+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:
```ruby
-# spec/integration/comments_spec.rb
+# spec/requests/comments_spec.rb
describe 'Blogs API' do
path '/blogs/{blog_id}/comments' do
@@ -574,8 +798,27 @@ describe 'Blogs API' do
post 'Creates a comment' do
response 422, 'invalid request' do
- header 'X-Rate-Limit-Limit', type: :integer, description: 'The number of allowed requests in the current period'
- header 'X-Rate-Limit-Remaining', type: :integer, description: 'The number of remaining requests in the current period'
+ header 'X-Rate-Limit-Limit', schema: { type: :integer }, description: 'The number of allowed requests in the current period'
+ header 'X-Rate-Limit-Remaining', schema: { type: :integer }, description: 'The number of remaining requests in the current period'
+ ...
+end
+```
+
+#### Nullable or Optional Response Headers ####
+
+You can include `nullable` or `required` to specify whether a response header must be present or may be null. When `nullable` is not included, the headers validation validates that the header response is non-null. When `required` is not included, the headers validation validates the the header response is passed.
+
+```ruby
+# spec/integration/comments_spec.rb
+describe 'Blogs API' do
+
+ path '/blogs/{blog_id}/comments' do
+
+ get 'Gets a list of comments' do
+
+ response 200, 'blog found' do
+ header 'X-Cursor', schema: { type: :string, nullable: true }, description: 'The cursor to get the next page of comments.'
+ header 'X-Per-Page', schema: { type: :integer }, required: false, description: 'The number of comments per page.'
...
end
```
@@ -584,8 +827,9 @@ end
You can provide custom response examples to the generated swagger file by calling the method `examples` inside the response block:
However, auto generated example responses are now enabled by default in rswag. See below.
+
```ruby
-# spec/integration/blogs_spec.rb
+# spec/requests/blogs_spec.rb
describe 'Blogs API' do
path '/blogs/{blog_id}' do
@@ -593,11 +837,16 @@ describe 'Blogs API' do
get 'Retrieves a blog' do
response 200, 'blog found' do
- examples 'application/json' => {
+ example 'application/json', :example_key, {
id: 1,
title: 'Hello world!',
content: '...'
}
+ example 'application/json', :example_key_2, {
+ id: 1,
+ title: 'Hello world!',
+ content: '...'
+ }, "Summary of the example", "Longer description of the example"
...
end
```
@@ -608,25 +857,31 @@ end
To enable examples generation from responses add callback above run_test! like:
-```
+```ruby
after do |example|
- example.metadata[:response][:content] = {
- 'application/json' => {
- example: JSON.parse(response.body, symbolize_names: true)
+ content = example.metadata[:response][:content] || {}
+ example_spec = {
+ "application/json"=>{
+ examples: {
+ test_example: {
+ value: JSON.parse(response.body, symbolize_names: true)
+ }
+ }
}
}
+ example.metadata[:response][:content] = content.deep_merge(example_spec)
end
```
#### Dry Run Option ####
The `--dry-run` option is enabled by default for Rspec 3, but if you need to
-disable it you can use the environment varible `SWAGGER_DRY_RUN=0` during the
+disable it you can use the environment variable `RSWAG_DRY_RUN=0` during the
generation command or add the following to your `config/environments/test.rb`:
```ruby
RSpec.configure do |config|
- config.swagger_dry_run = false
+ config.rswag_dry_run = false
end
```
@@ -662,7 +917,7 @@ describe 'Blogs API', document: false do
### Route Prefix for Swagger JSON Endpoints ###
-The functionality to expose Swagger files, such as those generated by rswag-specs, as JSON endpoints is implemented as a Rails Engine. As with any Engine, you can change it's mount prefix in _routes.rb_:
+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 its mount prefix in _routes.rb_:
```ruby
TestApp::Application.routes.draw do
@@ -800,7 +1055,7 @@ 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 Swagger file exists at <openapi_root>/v1/swagger.json, this configuration would expose the file as the following JSON endpoint:
```
GET http:///your-custom-prefix/v1/swagger.json
@@ -812,12 +1067,12 @@ You can adjust this in the _rswag_api.rb_ initializer that's installed with __rs
```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 Swagger 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 Swagger files manually.
### Dynamic Values for Swagger JSON ##
@@ -831,7 +1086,7 @@ Rswag::Api.configure do |c|
end
```
-Note how the filter is passed the rack env for the current request. This provides a lot of flexibilty. 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.
+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 ###
@@ -850,18 +1105,18 @@ Take care when overriding Content-Type if you serve both YAML and JSON files as
### Enable Swagger Endpoints for swagger-ui ###
-You can update the _rswag-ui.rb_ initializer, installed with rswag-ui, to specify which Swagger endpoints should be available to power the documentation UI. If you're using rswag-api, these should correspond to the Swagger endpoints it exposes. When the UI is rendered, you'll see these listed in a drop-down to the top right of the page:
+You can update the _rswag_ui.rb_ initializer, installed with rswag-ui, to specify which Swagger endpoints should be available to power the documentation UI. If you're using rswag-api, these should correspond to the Swagger endpoints it exposes. When the UI is rendered, you'll see these listed in a drop-down to the top right of the page:
```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/swagger.json', 'API V1 Docs'
+ c.openapi_endpoint '/api-docs/v2/swagger.json', 'API V2 Docs'
end
```
### Enable Simple Basic Auth for swagger-ui
-You can also update the _rswag-ui.rb_ initializer, installed with rswag-ui to specify a username and password should you want to keep your documentation private.
+You can also update the _rswag_ui.rb_ initializer, installed with rswag-ui to specify a username and password should you want to keep your documentation private.
```ruby
Rswag::Ui.configure do |c|
@@ -872,7 +1127,7 @@ end
### Route Prefix for the swagger-ui ###
-Similar to rswag-api, you can customize the swagger-ui path by changing it's mount prefix in _routes.rb_:
+Similar to rswag-api, you can customize the swagger-ui path by changing its mount prefix in _routes.rb_:
```ruby
TestApp::Application.routes.draw do
@@ -885,14 +1140,33 @@ end
### Customizing the swagger-ui ###
-The swagger-ui provides several options for customizing it's behavior, all of which are documented here https://github.com/swagger-api/swagger-ui/tree/2.x#swaggerui. If you need to tweak these or customize the overall look and feel of your swagger-ui, then you'll need to provide your own version of index.html. You can do this with the following generator.
+The swagger-ui provides several options for customizing its behavior, all of which are documented here https://github.com/swagger-api/swagger-ui/tree/2.x#swaggerui. If you need to tweak these or customize the overall look and feel of your swagger-ui, then you'll need to provide your own version of index.html. You can do this with the following generator.
```ruby
rails g rswag:ui:custom
```
-This will add a local version that you can modify at _app/views/rswag/ui/home/index.html.erb_
+This will add a local version that you can modify at _app/views/rswag/ui/home/index.html.erb_. For example, it will let you to add your own `` and favicon.
+
+To replace the *"Swagger sponsored by"* brand image, you can add the following script to the generated file:
+
+```html
+
+```
+
+The above script would expect to find an image named `favicon.png` in the public folder.
### Serve UI Assets Directly from your Web Server
@@ -914,3 +1188,31 @@ 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.
+
+### Custom :getter option for parameter
+
+To avoid conflicts with Rspec [`include`](https://github.com/rspec/rspec-rails/blob/40261bb72875c00a6e4a0ca2ac697b660d4e8d9c/spec/support/generators.rb#L18) matcher and other possible intersections like `status` method:
+
+```
+...
+parameter name: :status,
+ getter: :filter_status,
+ in: :query,
+ schema: {
+ type: :string,
+ enum: %w[one two three],
+ }, required: false
+
+let(:status) { nil } # will not be used in query string
+let(:filter_status) { 'one' } # `&status=one` will be provided in final query
+```
+
+### Linting with RuboCop RSpec
+
+When you lint your RSpec spec files with `rubocop-rspec`, it will fail to detect RSpec aliases that Rswag defines.
+Make sure to use `rubocop-rspec` 2.0 or newer and add the following to your `.rubocop.yml`:
+
+```yaml
+inherit_gem:
+ rswag-specs: .rubocop_rspec_alias_config.yml
+```
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 000000000..e35bf94ec
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,17 @@
+# Security Policy
+
+## Supported Versions
+
+Only the latest version is supported due to the small size of the team.
+[Follow this link to see the latest release](https://github.com/rswag/rswag/releases/latest)
+
+## Reporting a Vulnerability
+
+
+
+The Rswag team currently does not have a mailbox for security vulnerabilities. Please report discovered issues in the GitHub issues section, tagging public members.
+
+
+We will get back to the issue as soon as possible.
diff --git a/cspell.json b/cspell.json
new file mode 100644
index 000000000..442c19a79
--- /dev/null
+++ b/cspell.json
@@ -0,0 +1,57 @@
+{
+ "version": "0.2",
+ "language": "en",
+ "useGitignore": true,
+ "patterns": [
+ {
+ "name": "Url",
+ "pattern": "/\\b\\w+\\.(com|org)(\\/[\\w.-]+)*?/g"
+ },
+ {
+ "name": "QueryParam",
+ "pattern": "/[?&][a-zA-z0-9_|-]+=[a-zA-Z0-9_%-]*/g"
+ }
+ ],
+ "ignoreRegExpList": [
+ "Url",
+ "QueryParam"
+ ],
+ "words": [
+ "actionpack",
+ "appuser",
+ "autoload",
+ "autoloadable",
+ "byebug",
+ "cacher",
+ "codegen",
+ "danielian",
+ "datetime",
+ "doctoc",
+ "domaindrivendev",
+ "fdescribe",
+ "fcontext",
+ "jsmith",
+ "jspass",
+ "geckodriver",
+ "libv8",
+ "openapi",
+ "pathing",
+ "railtie",
+ "railties",
+ "Rakefile",
+ "rdoc",
+ "rdoctask",
+ "rswag",
+ "rubygems",
+ "srand",
+ "scrollbars",
+ "stylesheet",
+ "swaggerapi",
+ "swaggerize",
+ "tempuri",
+ "tmpdir",
+ "xlink",
+ "rubocop",
+ "RuboCop"
+ ]
+}
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 000000000..213bf3577
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,14 @@
+version: '3.8'
+services:
+ test-app: &base-test
+ container_name: rswag
+ image: rswag
+ ports:
+ - 3000:3000
+ build:
+ context: .
+ volumes:
+ - .:/rswag
+ test:
+ <<: *base-test
+ command: sh -c 'cd .. && ./ci/test.sh'
\ No newline at end of file
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 5f3ddc40f..c4462b277 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,9 +4,9 @@
# 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 + '/swagger'
- # Inject a lamda function to alter the returned Swagger prior to serialization
+ # 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
# For example, you could leverage this to dynamically assign the "host" property
#
diff --git a/rswag-api/lib/rswag/api.rb b/rswag-api/lib/rswag/api.rb
index 894072b0b..0dd38aa65 100644
--- a/rswag-api/lib/rswag/api.rb
+++ b/rswag-api/lib/rswag/api.rb
@@ -1,8 +1,14 @@
+require 'active_support/deprecation'
require 'rswag/api/configuration'
-require 'rswag/api/engine'
+require 'rswag/api/engine' if defined?(Rails::Engine)
module Rswag
module Api
+ RENAMED_METHODS = {
+ swagger_root: :openapi_root
+ }.freeze
+ private_constant :RENAMED_METHODS
+
def self.configure
yield(config)
end
@@ -10,5 +16,22 @@ def self.configure
def self.config
@config ||= Configuration.new
end
+
+ def self.deprecator
+ @deprecator ||= ActiveSupport::Deprecation.new('3.0', 'rswag-api')
+ end
+
+ Configuration.class_eval do
+ RENAMED_METHODS.each do |old_name, new_name|
+ define_method("#{old_name}=") do |*args, &block|
+ public_send("#{new_name}=", *args, &block)
+ end
+ end
+ end
+
+ Api.deprecator.deprecate_methods(
+ Configuration,
+ RENAMED_METHODS.to_h { |old_name, new_name| ["#{old_name}=".to_sym, "#{new_name}=".to_sym] }
+ )
end
end
diff --git a/rswag-api/lib/rswag/api/configuration.rb b/rswag-api/lib/rswag/api/configuration.rb
index bf6422901..94d4dd2a6 100644
--- a/rswag-api/lib/rswag/api/configuration.rb
+++ b/rswag-api/lib/rswag/api/configuration.rb
@@ -1,11 +1,19 @@
module Rswag
module Api
class Configuration
- attr_accessor :swagger_root, :swagger_filter, :swagger_headers
+ attr_accessor :openapi_root, :swagger_filter, :swagger_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
+
+ if path_params.key?(:swagger_root)
+ Rswag::Api.deprecator.warn(
+ 'swagger_root is deprecated and will be removed from rswag-api 3.0 (use openapi_root instead)'
+ )
+ return path_params[:swagger_root]
+ end
+
+ 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 77a3b01c2..dfa2c18a9 100644
--- a/rswag-api/lib/rswag/api/middleware.rb
+++ b/rswag-api/lib/rswag/api/middleware.rb
@@ -5,7 +5,6 @@
module Rswag
module Api
class Middleware
-
def initialize(app, config)
@app = app
@config = config
@@ -13,7 +12,11 @@ def initialize(app, config)
def call(env)
path = env['PATH_INFO']
- filename = "#{@config.resolve_swagger_root(env)}/#{path}"
+ # Sanitize the filename for directory traversal by expanding, and ensuring
+ # its starts with the root directory.
+ openapi_root = @config.resolve_openapi_root(env)
+ filename = File.expand_path(File.join(openapi_root, path))
+ return @app.call(env) unless filename.start_with? openapi_root.to_s
if env['REQUEST_METHOD'] == 'GET' && File.file?(filename)
swagger = parse_file(filename)
@@ -25,11 +28,11 @@ def call(env)
return [
'200',
headers,
- [ body ]
+ [body]
]
end
- return @app.call(env)
+ @app.call(env)
end
private
diff --git a/rswag-api/rswag-api.gemspec b/rswag-api/rswag-api.gemspec
index 0cbfd9915..d4cdab653 100644
--- a/rswag-api/rswag-api.gemspec
+++ b/rswag-api/rswag-api.gemspec
@@ -5,7 +5,7 @@ $LOAD_PATH.push File.expand_path('../lib', __FILE__)
# Describe your gem and declare its dependencies:
Gem::Specification.new do |s|
s.name = 'rswag-api'
- s.version = ENV['TRAVIS_TAG'] || '0.0.0'
+ s.version = ENV['RUBYGEMS_VERSION'] || '0.0.0'
s.authors = ['Richie Morris', 'Greg Myers', 'Jay Danielian']
s.email = ['domaindrivendev@gmail.com']
s.homepage = 'https://github.com/rswag/rswag'
@@ -15,5 +15,8 @@ Gem::Specification.new do |s|
s.files = Dir['{lib}/**/*'] + ['MIT-LICENSE', 'Rakefile']
- s.add_dependency 'railties', '>= 3.1', '< 7.1'
+ s.add_dependency 'activesupport', '>= 5.2', '< 8.1'
+ s.add_dependency 'railties', '>= 5.2', '< 8.1'
+
+ s.add_development_dependency 'simplecov', '=0.21.2'
end
diff --git a/rswag-api/spec/generators/rswag/api/install_generator_spec.rb b/rswag-api/spec/generators/rswag/api/install_generator_spec.rb
index 6e983fcd8..a83dbc4c7 100644
--- a/rswag-api/spec/generators/rswag/api/install_generator_spec.rb
+++ b/rswag-api/spec/generators/rswag/api/install_generator_spec.rb
@@ -20,9 +20,6 @@ module Api
it 'installs the Rails initializer' do
assert_file('config/initializers/rswag_api.rb')
end
-
- # Don't know how to test this
- #it 'wires up routes'
end
end
end
diff --git a/rswag-api/spec/lib/rswag/api/configuration_spec.rb b/rswag-api/spec/lib/rswag/api/configuration_spec.rb
new file mode 100644
index 000000000..e7391dba4
--- /dev/null
+++ b/rswag-api/spec/lib/rswag/api/configuration_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Rswag::Api::Configuration do
+ subject(:configuration) { described_class.new }
+
+ describe '#swagger_root=' do
+ it 'is deprecated' do
+ allow(Rswag::Api.deprecator).to receive(:warn)
+ configuration.swagger_root = 'foobar'
+ expect(subject.openapi_root).to eq('foobar')
+ expect(Rswag::Api.deprecator).to(
+ have_received(:warn)
+ .with('swagger_root= is deprecated and will be removed from rswag-api 3.0 (use openapi_root= instead)',
+ any_args)
+ )
+ end
+ end
+end
diff --git a/rswag-api/spec/lib/rswag/api_spec.rb b/rswag-api/spec/lib/rswag/api_spec.rb
new file mode 100644
index 000000000..0e2ca3221
--- /dev/null
+++ b/rswag-api/spec/lib/rswag/api_spec.rb
@@ -0,0 +1,9 @@
+require 'spec_helper'
+
+RSpec.describe Rswag::Api do
+ describe '::configure' do
+ it 'yields a configuration object' do
+ expect { |b| described_class.configure(&b) }.to yield_with_args(instance_of(Rswag::Api::Configuration))
+ end
+ end
+end
diff --git a/rswag-api/spec/rswag/api/middleware_spec.rb b/rswag-api/spec/rswag/api/middleware_spec.rb
index 6f9072476..a75a2ba14 100644
--- a/rswag-api/spec/rswag/api/middleware_spec.rb
+++ b/rswag-api/spec/rswag/api/middleware_spec.rb
@@ -1,132 +1,179 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
require 'rswag/api/middleware'
require 'rswag/api/configuration'
-module Rswag
- module Api
+describe Rswag::Api::Middleware do
+ let(:app) { double('app') }
+ let(:openapi_root) { File.expand_path('fixtures/swagger', __dir__) }
+ let(:config) do
+ Rswag::Api::Configuration.new.tap { |c| c.openapi_root = openapi_root }
+ end
+
+ subject { described_class.new(app, config) }
+
+ describe '#call(env)' do
+ let(:response) { subject.call(env) }
+ let(:env_defaults) do
+ {
+ 'HTTP_HOST' => 'tempuri.org',
+ 'REQUEST_METHOD' => 'GET'
+ }
+ end
+
+ context 'given a path that maps to an existing swagger file' do
+ let(:env) { env_defaults.merge('PATH_INFO' => 'v1/swagger.json') }
- describe Middleware do
- let(:app) { double('app') }
- let(:swagger_root) { File.expand_path('../fixtures/swagger', __FILE__) }
- let(:config) do
- Configuration.new.tap { |c| c.swagger_root = swagger_root }
+ it 'returns a 200 status' do
+ expect(response.length).to eql(3)
+ expect(response.first).to eql('200')
end
- subject { described_class.new(app, config) }
+ it 'returns contents of the swagger file' do
+ expect(response.length).to eql(3)
+ expect(response[1]).to include('Content-Type' => 'application/json')
+ expect(response[2].join).to include('"title":"API V1"')
+ end
- describe '#call(env)' do
- let(:response) { subject.call(env) }
- let(:env_defaults) do
- {
- 'HTTP_HOST' => 'tempuri.org',
- 'REQUEST_METHOD' => 'GET',
- }
- end
+ context 'configured with a Pathname similar to `Rails.root.join("swagger")`' do
+ let(:openapi_root_pathname) { Pathname.new(openapi_root) }
- context 'given a path that maps to an existing swagger file' do
- let(:env) { env_defaults.merge('PATH_INFO' => 'v1/swagger.json') }
+ before { config.openapi_root = openapi_root_pathname }
+
+ it 'returns a 200 status' do
+ expect(response.length).to eql(3)
+ expect(response.first).to eql('200')
+ end
+ end
+ end
- it 'returns a 200 status' do
- expect(response.length).to eql(3)
- expect(response.first).to eql('200')
- end
+ context 'when swagger_headers is configured' do
+ let(:env) { env_defaults.merge('PATH_INFO' => 'v1/swagger.json') }
- it 'returns contents of the swagger file' do
- expect(response.length).to eql(3)
- expect(response[1]).to include( 'Content-Type' => 'application/json')
- expect(response[2].join).to include('"title":"API V1"')
- end
+ context 'replacing the default content type header' do
+ before do
+ config.swagger_headers = { 'Content-Type' => 'application/json; charset=UTF-8' }
+ end
+ it 'returns a 200 status' do
+ expect(response.length).to eql(3)
+ expect(response.first).to eql('200')
end
- context 'when swagger_headers is configured' do
- let(:env) { env_defaults.merge('PATH_INFO' => 'v1/swagger.json') }
-
- context 'replacing the default content type header' do
- before do
- config.swagger_headers = { 'Content-Type' => 'application/json; charset=UTF-8' }
- end
- it 'returns a 200 status' do
- expect(response.length).to eql(3)
- expect(response.first).to eql('200')
- end
-
- it 'applies the headers to the response' do
- expect(response[1]).to include( 'Content-Type' => 'application/json; charset=UTF-8')
- end
- end
-
- context 'adding an additional header' do
- before do
- config.swagger_headers = { 'Access-Control-Allow-Origin' => '*' }
- end
- it 'returns a 200 status' do
- expect(response.length).to eql(3)
- expect(response.first).to eql('200')
- end
-
- it 'applies the headers to the response' do
- expect(response[1]).to include( 'Access-Control-Allow-Origin' => '*')
- end
-
- it 'keeps the default header' do
- expect(response[1]).to include( 'Content-Type' => 'application/json')
- end
- end
+ it 'applies the headers to the response' do
+ expect(response[1]).to include('Content-Type' => 'application/json; charset=UTF-8')
end
+ end
- context "given a path that doesn't map to any swagger file" do
- let(:env) { env_defaults.merge('PATH_INFO' => 'foobar.json') }
- before do
- allow(app).to receive(:call).and_return([ '500', {}, [] ])
- end
+ context 'adding an additional header' do
+ before do
+ config.swagger_headers = { 'Access-Control-Allow-Origin' => '*' }
+ end
+ it 'returns a 200 status' do
+ expect(response.length).to eql(3)
+ expect(response.first).to eql('200')
+ end
- it 'delegates to the next middleware' do
- expect(response).to include('500')
- end
+ it 'applies the headers to the response' do
+ expect(response[1]).to include('Access-Control-Allow-Origin' => '*')
end
- context 'when the env contains a specific swagger_root' do
- let(:env) do
- env_defaults.merge(
- 'PATH_INFO' => 'v1/swagger.json',
- 'action_dispatch.request.path_parameters' => {
- swagger_root: swagger_root
- }
- )
- end
-
- it 'locates files at the provided swagger_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
+ it 'keeps the default header' do
+ expect(response[1]).to include('Content-Type' => 'application/json')
end
+ end
+ end
- context 'when a swagger_filter is configured' do
- before do
- config.swagger_filter = lambda { |swagger, env| swagger['host'] = env['HTTP_HOST'] }
- end
- let(:env) { env_defaults.merge('PATH_INFO' => 'v1/swagger.json') }
+ context "given a path that doesn't map to any swagger file" do
+ let(:env) { env_defaults.merge('PATH_INFO' => 'foobar.json') }
+ before do
+ allow(app).to receive(:call).and_return(['500', {}, []])
+ end
- it 'applies the filter prior to serialization' do
- expect(response.length).to eql(3)
- expect(response[2].join).to include('"host":"tempuri.org"')
- end
- end
+ it 'delegates to the next middleware' do
+ expect(response).to include('500')
+ end
+ end
- context 'when a path maps to a yaml swagger file' do
- let(:env) { env_defaults.merge('PATH_INFO' => 'v1/swagger.yml') }
+ context 'Disallow path traversing on path info' do
+ let(:env) { env_defaults.merge('PATH_INFO' => '../traverse-secret.yml') }
+ before do
+ allow(app).to receive(:call).and_return(['500', {}, []])
+ end
- it 'returns a 200 status' do
- expect(response.length).to eql(3)
- expect(response.first).to eql('200')
- end
+ it 'delegates to the next middleware' do
+ expect(response).to include('500')
+ end
+ end
- it 'returns contents of the swagger 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')
- end
- end
+ context 'when the env contains a specific swagger_root' do
+ let(:env) do
+ env_defaults.merge(
+ 'PATH_INFO' => 'v1/swagger.json',
+ 'action_dispatch.request.path_parameters' => {
+ swagger_root: openapi_root
+ }
+ )
+ end
+
+ before do
+ allow(Rswag::Api.deprecator).to receive(:warn)
+ end
+
+ it 'locates files at the provided swagger_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"')
+ expect(Rswag::Api.deprecator).to(
+ have_received(:warn)
+ .with('swagger_root is deprecated and will be removed from rswag-api 3.0 (use openapi_root instead)')
+ )
+ end
+ end
+
+ context 'when the env contains a specific openapi_root' do
+ let(:env) do
+ env_defaults.merge(
+ 'PATH_INFO' => 'v1/swagger.json',
+ 'action_dispatch.request.path_parameters' => {
+ openapi_root: openapi_root
+ }
+ )
+ end
+
+ 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
+ before do
+ config.swagger_filter = ->(swagger, env) { swagger['host'] = env['HTTP_HOST'] }
+ end
+ let(:env) { env_defaults.merge('PATH_INFO' => 'v1/swagger.json') }
+
+ it 'applies the filter prior to serialization' do
+ expect(response.length).to eql(3)
+ expect(response[2].join).to include('"host":"tempuri.org"')
+ end
+ end
+
+ context 'when a path maps to a yaml swagger file' do
+ let(:env) { env_defaults.merge('PATH_INFO' => 'v1/swagger.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
+ expect(response.length).to eql(3)
+ expect(response[1]).to include('Content-Type' => 'text/yaml')
+ expect(response[2].join).to include('title: API V1')
end
end
end
diff --git a/rswag-api/spec/spec_helper.rb b/rswag-api/spec/spec_helper.rb
index e69de29bb..ca7eb4487 100644
--- a/rswag-api/spec/spec_helper.rb
+++ b/rswag-api/spec/spec_helper.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+require 'simplecov'
+
+RSpec.configure do |config|
+ SimpleCov.start do
+ enable_coverage :branch
+ primary_coverage :branch
+ filters.clear
+ add_filter %r{^/spec/}
+ end
+end
+
+require 'rswag/api'
diff --git a/rswag-specs/.rubocop_rspec_alias_config.yml b/rswag-specs/.rubocop_rspec_alias_config.yml
new file mode 100644
index 000000000..4acd0d729
--- /dev/null
+++ b/rswag-specs/.rubocop_rspec_alias_config.yml
@@ -0,0 +1,17 @@
+RSpec:
+ Language:
+ ExampleGroups:
+ Regular:
+ - path
+ - response
+ - get
+ - post
+ - patch
+ - put
+ - delete
+ - head
+ - options
+ - trace
+ Examples:
+ Regular:
+ - run_test!
diff --git a/rswag-specs/lib/generators/rspec/swagger_generator.rb b/rswag-specs/lib/generators/rspec/swagger_generator.rb
index 729917620..f3a22704e 100644
--- a/rswag-specs/lib/generators/rspec/swagger_generator.rb
+++ b/rswag-specs/lib/generators/rspec/swagger_generator.rb
@@ -6,13 +6,14 @@
module Rspec
class SwaggerGenerator < ::Rails::Generators::NamedBase
source_root File.expand_path('templates', __dir__)
+ class_option :spec_path, type: :string, default: 'requests'
def setup
@routes = Rswag::RouteParser.new(controller_path).routes
end
def create_spec_file
- template 'spec.rb', File.join('spec', 'requests', "#{controller_path}_spec.rb")
+ template 'spec.rb', File.join('spec', options['spec_path'], "#{controller_path}_spec.rb")
end
private
diff --git a/rswag-specs/lib/generators/rswag/specs/install/templates/swagger_helper.rb b/rswag-specs/lib/generators/rswag/specs/install/templates/swagger_helper.rb
index 8f715605d..14c121497 100644
--- a/rswag-specs/lib/generators/rswag/specs/install/templates/swagger_helper.rb
+++ b/rswag-specs/lib/generators/rswag/specs/install/templates/swagger_helper.rb
@@ -6,15 +6,15 @@
# Specify a root folder where Swagger 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
+ config.openapi_root = Rails.root.join('swagger').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
+ # 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 = {
+ # 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/swagger.json'
+ config.openapi_specs = {
'v1/swagger.yaml' => {
openapi: '3.0.1',
info: {
@@ -36,8 +36,8 @@
}
# Specify the format of the output Swagger file when running 'rswag:specs:swaggerize'.
- # The swagger_docs configuration option has the filename including format in
+ # The 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 1db62a55b..5205fd9de 100644
--- a/rswag-specs/lib/rswag/specs.rb
+++ b/rswag-specs/lib/rswag/specs.rb
@@ -8,12 +8,23 @@
module Rswag
module Specs
+ RENAMED_METHODS = {
+ swagger_root: :openapi_root,
+ swagger_docs: :openapi_specs,
+ swagger_dry_run: :rswag_dry_run,
+ swagger_format: :openapi_format
+ }.freeze
+ private_constant :RENAMED_METHODS
+
# Extend RSpec with a swagger-based DSL
::RSpec.configure do |c|
- c.add_setting :swagger_root
- c.add_setting :swagger_docs
- c.add_setting :swagger_dry_run
- c.add_setting :swagger_format
+ c.add_setting :openapi_root
+ c.add_setting :openapi_specs
+ c.add_setting :rswag_dry_run
+ c.add_setting :openapi_format, default: :json
+ c.add_setting :openapi_strict_schema_validation
+ c.add_setting :openapi_all_properties_required
+ c.add_setting :openapi_no_additional_properties
c.extend ExampleGroupHelpers, type: :request
c.include ExampleHelpers, type: :request
end
@@ -22,8 +33,40 @@ def self.config
@config ||= Configuration.new(RSpec.configuration)
end
+ def self.deprecator
+ @deprecator ||= ActiveSupport::Deprecation.new('3.0', 'rswag-specs')
+ end
+
# Support Rails 3+ and RSpec 2+ (sigh!)
RAILS_VERSION = Rails::VERSION::MAJOR
RSPEC_VERSION = RSpec::Core::Version::STRING.split('.').first.to_i
+
+ RSpec::Core::Configuration.class_eval do
+ RENAMED_METHODS.each do |old_name, new_name|
+ define_method("#{old_name}=") do |*args, &block|
+ public_send("#{new_name}=", *args, &block)
+ end
+ end
+
+ define_method('swagger_strict_schema_validation=') do |*args, &block|
+ public_send('openapi_strict_schema_validation=', *args, &block)
+ end
+ end
+
+ Specs.deprecator.deprecate_methods(
+ RSpec::Core::Configuration,
+ RENAMED_METHODS.to_h { |old_name, new_name| ["#{old_name}=".to_sym, "#{new_name}=".to_sym] }
+ )
+
+ Specs.deprecator.deprecate_methods(
+ RSpec::Core::Configuration,
+ :openapi_strict_schema_validation= => 'use openapi_all_properties_required and openapi_no_additional_properties set to true'
+ )
+
+ if RUBY_VERSION.start_with? '2.6'
+ Specs.deprecator.warn('Rswag::Specs: WARNING: Support for Ruby 2.6 will be dropped in v3.0')
+ end
+
+ Specs.deprecator.warn('Rswag::Specs: WARNING: Support for RSpec 2.X will be dropped in v3.0') if RSPEC_VERSION < 3
end
end
diff --git a/rswag-specs/lib/rswag/specs/configuration.rb b/rswag-specs/lib/rswag/specs/configuration.rb
index ab1317f88..759e54639 100644
--- a/rswag-specs/lib/rswag/specs/configuration.rb
+++ b/rswag-specs/lib/rswag/specs/configuration.rb
@@ -7,54 +7,68 @@ 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 ||=
+ @rspec_config.openapi_root || raise(ConfigurationError, 'No openapi_root provided. See swagger_helper.rb')
+ end
+
+ def openapi_specs
+ @openapi_specs ||= begin
+ if @rspec_config.openapi_specs.nil? || @rspec_config.openapi_specs.empty?
+ raise ConfigurationError, 'No openapi_specs defined. See swagger_helper.rb'
end
- @rspec_config.swagger_root
+ @rspec_config.openapi_specs
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 rswag_dry_run
+ @rswag_dry_run ||= begin
+ if ENV.key?('SWAGGER_DRY_RUN') || ENV.key?('RSWAG_DRY_RUN')
+ @rspec_config.rswag_dry_run = ENV['SWAGGER_DRY_RUN'] == '1' || ENV['RSWAG_DRY_RUN'] == '1'
end
- @rspec_config.swagger_docs
+ @rspec_config.rswag_dry_run.nil? || @rspec_config.rswag_dry_run
end
end
- def swagger_dry_run
- return @swagger_dry_run if defined? @swagger_dry_run
- if ENV.key?('SWAGGER_DRY_RUN')
- @rspec_config.swagger_dry_run = ENV['SWAGGER_DRY_RUN'] == '1'
- end
- @swagger_dry_run = @rspec_config.swagger_dry_run.nil? || @rspec_config.swagger_dry_run
- end
+ def openapi_format
+ @openapi_format ||= begin
+ if @rspec_config.openapi_format.nil? || @rspec_config.openapi_format.empty?
+ @rspec_config.openapi_format = :json
+ 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)
+ 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]
+ def get_openapi_spec(name)
+ return openapi_specs.values.first if name.nil?
+ raise ConfigurationError, "Unknown openapi_spec '#{name}'" unless openapi_specs[name]
- swagger_docs[name]
+ openapi_specs[name]
end
- def get_swagger_doc_version(name)
- doc = get_swagger_doc(name)
+ def get_openapi_spec_version(name)
+ doc = get_openapi_spec(name)
doc[:openapi] || doc[:swagger]
end
+
+ def openapi_strict_schema_validation
+ @rspec_config.openapi_strict_schema_validation || false
+ end
+
+ def openapi_all_properties_required
+ @rspec_config.openapi_all_properties_required || false
+ end
+
+ def openapi_no_additional_properties
+ @rspec_config.openapi_no_additional_properties || false
+ end
end
class ConfigurationError < StandardError; end
diff --git a/rswag-specs/lib/rswag/specs/example_group_helpers.rb b/rswag-specs/lib/rswag/specs/example_group_helpers.rb
index 2a2cdec29..03251ec92 100644
--- a/rswag-specs/lib/rswag/specs/example_group_helpers.rb
+++ b/rswag-specs/lib/rswag/specs/example_group_helpers.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require 'active_support'
+
module Rswag
module Specs
module ExampleGroupHelpers
@@ -9,9 +11,9 @@ def path(template, metadata = {}, &block)
end
[:get, :post, :patch, :put, :delete, :head, :options, :trace].each do |verb|
- define_method(verb) do |summary, &block|
- api_metadata = { operation: { verb: verb, summary: summary } }
- describe(verb, api_metadata, &block)
+ define_method(verb) do |summary, **metadata, &block|
+ api_metadata = { operation: { verb: verb, summary: summary } }.deep_merge(metadata)
+ describe(verb, **api_metadata, &block)
end
end
@@ -51,6 +53,17 @@ def parameter(attributes)
end
end
+ def request_body_example(value:, summary: nil, name: nil)
+ if metadata.key?(:operation)
+ metadata[:operation][:request_examples] ||= []
+ example = { value: value }
+ example[:summary] = summary if summary
+ # We need the examples to have a unique name for a set of examples, so just make the name the length if one isn't provided.
+ example[:name] = name || metadata[:operation][:request_examples].length()
+ metadata[:operation][:request_examples] << example
+ end
+ end
+
def response(code, description, metadata = {}, &block)
metadata[:response] = { code: code, description: description }
context(description, metadata, &block)
@@ -69,23 +82,56 @@ def header(name, attributes)
# NOTE: Similar to 'description', 'examples' need to handle the case when
# being invoked with no params to avoid overriding 'examples' method of
# rspec-core ExampleGroup
- def examples(example = nil)
- return super() if example.nil?
+ def examples(examples = nil)
+ return super() if examples.nil?
+ # should we add a deprecation warning?
+ examples.each_with_index do |(mime, example_object), index|
+ example(mime, "example_#{index}", example_object)
+ end
+ end
- metadata[:response][:content] =
- example.each_with_object({}) do |(mime, example_object), memo|
- memo[mime] = { example: example_object }
- end
+ def example(mime, name, value, summary=nil, description=nil)
+ # Todo - move initialization of metadata somewhere else.
+ if metadata[:response][:content].blank?
+ metadata[:response][:content] = {}
+ end
+
+ if metadata[:response][:content][mime].blank?
+ metadata[:response][:content][mime] = {}
+ metadata[:response][:content][mime][:examples] = {}
+ end
+
+ example_object = {
+ value: value,
+ summary: summary,
+ description: description
+ }.select { |_, v| v.present? }
+ # TODO, issue a warning if example is being overridden with the same key
+ metadata[:response][:content][mime][:examples].merge!(
+ { name.to_sym => example_object }
+ )
end
- def run_test!(&block)
- # NOTE: rspec 2.x support
+ #
+ # Perform request and assert response matches swagger definitions
+ #
+ # @param description [String] description of the test
+ # @param args [Array] arguments to pass to the `it` method
+ # @param options [Hash] options to pass to the `it` method
+ # @param &block [Proc] you can make additional assertions within that block
+ # @return [void]
+ def run_test!(description = nil, *args, **options, &block)
+ # rswag metadata value defaults to true
+ options[:rswag] = true unless options.key?(:rswag)
+
+ description ||= "returns a #{metadata[:response][:code]} response"
+
if RSPEC_VERSION < 3
before do
submit_request(example.metadata)
end
- it "returns a #{metadata[:response][:code]} response" do
+ it description, *args, **options do
assert_response_matches_metadata(metadata)
block.call(response) if block_given?
end
@@ -94,7 +140,7 @@ def run_test!(&block)
submit_request(example.metadata)
end
- it "returns a #{metadata[:response][:code]} response" do |example|
+ it description, *args, **options do |example|
assert_response_matches_metadata(example.metadata, &block)
example.instance_exec(response, &block) if block_given?
end
diff --git a/rswag-specs/lib/rswag/specs/extended_schema.rb b/rswag-specs/lib/rswag/specs/extended_schema.rb
index 3af8efc83..0446422b1 100644
--- a/rswag-specs/lib/rswag/specs/extended_schema.rb
+++ b/rswag-specs/lib/rswag/specs/extended_schema.rb
@@ -7,14 +7,11 @@ module Specs
class ExtendedSchema < JSON::Schema::Draft4
def initialize
super
- @attributes['type'] = ExtendedTypeAttribute
@uri = URI.parse('http://tempuri.org/rswag/specs/extended_schema')
@names = ['http://tempuri.org/rswag/specs/extended_schema']
end
- end
- class ExtendedTypeAttribute < JSON::Schema::TypeV4Attribute
- def self.validate(current_schema, data, fragments, processor, validator, options = {})
+ def validate(current_schema, data, *)
return if data.nil? && (current_schema.schema['nullable'] == true || current_schema.schema['x-nullable'] == true)
super
diff --git a/rswag-specs/lib/rswag/specs/railtie.rb b/rswag-specs/lib/rswag/specs/railtie.rb
index 0644f3c00..b21c6fdee 100644
--- a/rswag-specs/lib/rswag/specs/railtie.rb
+++ b/rswag-specs/lib/rswag/specs/railtie.rb
@@ -8,7 +8,13 @@ class Railtie < ::Rails::Railtie
end
generators do
- require 'generators/rspec/swagger_generator.rb'
+ require 'generators/rspec/swagger_generator'
+ end
+
+ initializer 'rswag-specs.deprecator' do |app|
+ if app.respond_to?(:deprecators)
+ app.deprecators[:rswag_specs] = Rswag::Specs.deprecator
+ end
end
end
end
diff --git a/rswag-specs/lib/rswag/specs/request_factory.rb b/rswag-specs/lib/rswag/specs/request_factory.rb
index eb13b2d5f..74c665dd0 100644
--- a/rswag-specs/lib/rswag/specs/request_factory.rb
+++ b/rswag-specs/lib/rswag/specs/request_factory.rb
@@ -1,5 +1,5 @@
# frozen_string_literal: true
-
+require "active_support"
require 'active_support/core_ext/hash/slice'
require 'active_support/core_ext/hash/conversions'
require 'json'
@@ -12,7 +12,7 @@ def initialize(config = ::Rswag::Specs.config)
end
def build_request(metadata, example)
- swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc])
+ swagger_doc = @config.get_openapi_spec(metadata[:openapi_spec] || metadata[:swagger_doc])
parameters = expand_parameters(metadata, swagger_doc, example)
{}.tap do |request|
@@ -34,7 +34,7 @@ def expand_parameters(metadata, swagger_doc, example)
(operation_params + path_item_params + security_params)
.map { |p| p['$ref'] ? resolve_parameter(p['$ref'], swagger_doc) : p }
.uniq { |p| p[:name] }
- .reject { |p| p[:required] == false && !example.respond_to?(p[:name]) }
+ .reject { |p| p[:required] == false && !example.respond_to?(extract_getter(p)) }
end
def derive_security_params(metadata, swagger_doc)
@@ -53,7 +53,7 @@ def security_version(scheme_names, swagger_doc)
(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)')
+ Rswag::Specs.deprecator.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
@@ -75,7 +75,7 @@ def key_version(ref, swagger_doc)
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/')
+ Rswag::Specs.deprecator.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
@@ -88,7 +88,7 @@ def definition_version(swagger_doc)
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)')
+ Rswag::Specs.deprecator.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] || {}
@@ -101,25 +101,85 @@ 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
+ variables = {}
+ server.fetch(:variables, {}).each_pair { |k,v| variables[k] = v[use_server] }
+ base_path = server[:url].gsub(/\{(.*?)\}/) { variables[$1.to_sym] }
+ URI(base_path).path
+ end
+
def add_path(request, metadata, swagger_doc, parameters, example)
- template = (swagger_doc[:basePath] || '') + metadata[:path_item][:template]
+ 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
+ Rswag::Specs.deprecator.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
request[:path] = template.tap do |path_template|
parameters.select { |p| p[:in] == :path }.each do |p|
- path_template.gsub!("{#{p[:name]}}", example.send(p[:name]).to_s)
+ unless example.respond_to?(extract_getter(p))
+ raise ArgumentError.new("`#{p[:name].to_s}` parameter key present, but not defined within example group"\
+ "(i. e `it` or `let` block)")
+ end
+ path_template.gsub!("{#{p[:name]}}", example.send(extract_getter(p)).to_s)
end
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])))
+ path_template.concat(build_query_string_part(p, example.send(extract_getter(p)), swagger_doc))
end
end
end
- def build_query_string_part(param, value)
+ def build_query_string_part(param, value, swagger_doc)
name = param[:name]
+ escaped_name = CGI.escape(name.to_s)
+
+ # OAS 3: https://swagger.io/docs/specification/serialization/
+ if swagger_doc && doc_version(swagger_doc).start_with?('3') && param[:schema]
+ style = param[:style]&.to_sym || :form
+ explode = param[:explode].nil? ? true : param[:explode]
+
+ case param[:schema][:type]&.to_sym
+ when :object
+ case style
+ when :deepObject
+ return { name => value }.to_query
+ when :form
+ if explode
+ return value.to_query
+ else
+ return "#{escaped_name}=" + value.to_a.flatten.map{|v| CGI.escape(v.to_s) }.join(',')
+ end
+ end
+ when :array
+ case explode
+ when true
+ return value.to_a.flatten.map{|v| "#{escaped_name}=#{CGI.escape(v.to_s)}"}.join('&')
+ else
+ separator = case style
+ when :form then ','
+ when :spaceDelimited then '%20'
+ when :pipeDelimited then '|'
+ end
+ return "#{escaped_name}=" + value.to_a.flatten.map{|v| CGI.escape(v.to_s) }.join(separator)
+ end
+ else
+ return "#{escaped_name}=#{CGI.escape(value.to_s)}"
+ end
+ end
+
type = param[:type] || param.dig(:schema, :type)
- return "#{name}=#{value}" unless type&.to_sym == :array
+ return "#{escaped_name}=#{CGI.escape(value.to_s)}" unless type&.to_sym == :array
case param[:collectionFormat]
when :ssv
@@ -138,7 +198,7 @@ def build_query_string_part(param, value)
def add_headers(request, metadata, swagger_doc, parameters, example)
tuples = parameters
.select { |p| p[:in] == :header }
- .map { |p| [p[:name], example.send(p[:name]).to_s] }
+ .map { |p| [p[:name], example.send(extract_getter(p)).to_s] }
# Accept header
produces = metadata[:operation][:produces] || swagger_doc[:produces]
@@ -154,31 +214,41 @@ def add_headers(request, metadata, swagger_doc, parameters, example)
tuples << ['Content-Type', content_type]
end
- # Rails test infrastructure requires rackified headers
- rackified_tuples = tuples.map do |pair|
+ # Host header
+ host = metadata[:operation][:host] || swagger_doc[:host]
+ if host.present?
+ host = example.respond_to?(:'Host') ? example.send(:'Host') : host
+ tuples << ['Host', host]
+ end
+
+ # Rails test infrastructure requires rack-formatted headers
+ rack_formatted_tuples = tuples.map do |pair|
[
case pair[0]
- when 'Accept' then 'HTTP_ACCEPT'
- when 'Content-Type' then 'CONTENT_TYPE'
- when 'Authorization' then 'HTTP_AUTHORIZATION'
- else pair[0]
+ when 'Accept' then 'HTTP_ACCEPT'
+ when 'Content-Type' then 'CONTENT_TYPE'
+ when 'Authorization' then 'HTTP_AUTHORIZATION'
+ when 'Host' then 'HTTP_HOST'
+ else pair[0]
end,
pair[1]
]
end
- request[:headers] = Hash[rackified_tuples]
+ request[:headers] = Hash[rack_formatted_tuples]
end
def add_payload(request, parameters, example)
content_type = request[:headers]['CONTENT_TYPE']
return if content_type.nil?
- if ['application/x-www-form-urlencoded', 'multipart/form-data'].include?(content_type)
- request[:payload] = build_form_payload(parameters, example)
- else
- request[:payload] = build_json_payload(parameters, example)
- end
+ request[:payload] = if ['application/x-www-form-urlencoded', 'multipart/form-data'].include?(content_type)
+ build_form_payload(parameters, example)
+ elsif content_type =~ /\Aapplication\/([0-9A-Za-z._-]+\+json\z|json\z)/
+ build_json_payload(parameters, example)
+ else
+ build_raw_payload(parameters, example)
+ end
end
def build_form_payload(parameters, example)
@@ -188,23 +258,30 @@ def build_form_payload(parameters, example)
# PROS: simple to implement, CONS: serialization/deserialization is bypassed in test
tuples = parameters
.select { |p| p[:in] == :formData }
- .map { |p| [p[:name], example.send(p[:name])] }
+ .map { |p| [p[:name], example.send(extract_getter(p))] }
Hash[tuples]
end
- def build_json_payload(parameters, example)
+ def build_raw_payload(parameters, example)
body_param = parameters.select { |p| p[:in] == :body }.first
-
return nil unless body_param
raise(MissingParameterError, body_param[:name]) unless example.respond_to?(body_param[:name])
- example.send(body_param[:name]).to_json
+ example.send(body_param[:name])
+ end
+
+ def build_json_payload(parameters, example)
+ build_raw_payload(parameters, example)&.to_json
end
def doc_version(doc)
doc[:openapi] || doc[:swagger] || '3'
end
+
+ def extract_getter(parameter)
+ parameter[:getter] || parameter[:name]
+ end
end
class MissingParameterError < StandardError
diff --git a/rswag-specs/lib/rswag/specs/response_validator.rb b/rswag-specs/lib/rswag/specs/response_validator.rb
index 6c31e0484..a4f577787 100644
--- a/rswag-specs/lib/rswag/specs/response_validator.rb
+++ b/rswag-specs/lib/rswag/specs/response_validator.rb
@@ -13,7 +13,7 @@ def initialize(config = ::Rswag::Specs.config)
end
def validate!(metadata, response)
- swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc])
+ swagger_doc = @config.get_openapi_spec(metadata[:openapi_spec] || metadata[:swagger_doc])
validate_code!(metadata, response)
validate_headers!(metadata, response.headers)
@@ -32,9 +32,22 @@ def validate_code!(metadata, response)
end
def validate_headers!(metadata, headers)
- expected = (metadata[:response][:headers] || {}).keys
+ header_schemas = (metadata[:response][:headers] || {})
+ expected = header_schemas.keys
expected.each do |name|
- raise UnexpectedResponse, "Expected response header #{name} to be present" if headers[name.to_s].nil?
+ nullable_attribute = header_schemas.dig(name.to_s, :schema, :nullable)
+ required_attribute = header_schemas.dig(name.to_s, :required)
+
+ is_nullable = nullable_attribute.nil? ? false : nullable_attribute
+ is_required = required_attribute.nil? ? true : required_attribute
+
+ if headers.exclude?(name.to_s) && is_required
+ raise UnexpectedResponse, "Expected response header #{name} to be present"
+ end
+
+ if headers.include?(name.to_s) && headers[name.to_s].nil? && !is_nullable
+ raise UnexpectedResponse, "Expected response header #{name} to not be null"
+ end
end
end
@@ -42,27 +55,50 @@ def validate_body!(metadata, swagger_doc, body)
response_schema = metadata[:response][:schema]
return if response_schema.nil?
- version = @config.get_swagger_doc_version(metadata[:swagger_doc])
+ version = @config.get_openapi_spec_version(metadata[:openapi_spec] || metadata[:swagger_doc])
schemas = definitions_or_component_schemas(swagger_doc, version)
validation_schema = response_schema
.merge('$schema' => 'http://tempuri.org/rswag/specs/extended_schema')
.merge(schemas)
- errors = JSON::Validator.fully_validate(validation_schema, body)
+ validation_options = validation_options_from(metadata)
+
+ errors = JSON::Validator.fully_validate(validation_schema, body, validation_options)
return unless errors.any?
raise UnexpectedResponse,
- "Expected response body to match schema: #{errors[0]}\n" \
+ "Expected response body to match schema: #{errors.join("\n")}\n" \
"Response body: #{JSON.pretty_generate(JSON.parse(body))}"
end
+ def validation_options_from(metadata)
+ is_strict = @config.openapi_strict_schema_validation
+
+ if metadata.key?(:swagger_strict_schema_validation)
+ Rswag::Specs.deprecator.warn('Rswag::Specs: WARNING: This option will be removed in v3.0 please use openapi_all_properties_required and openapi_no_additional_properties set to true')
+ is_strict = !!metadata[:swagger_strict_schema_validation]
+ elsif metadata.key?(:openapi_strict_schema_validation)
+ Rswag::Specs.deprecator.warn('Rswag::Specs: WARNING: This option will be removed in v3.0 please use openapi_all_properties_required and openapi_no_additional_properties set to true')
+ is_strict = !!metadata[:openapi_strict_schema_validation]
+ end
+
+ all_properties_required = metadata.fetch(:openapi_all_properties_required, @config.openapi_all_properties_required)
+ no_additional_properties = metadata.fetch(:openapi_no_additional_properties, @config.openapi_no_additional_properties)
+
+ {
+ strict: is_strict,
+ allPropertiesRequired: all_properties_required,
+ noAdditionalProperties: no_additional_properties
+ }
+ 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)')
+ Rswag::Specs.deprecator.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] || {}
diff --git a/rswag-specs/lib/rswag/specs/swagger_formatter.rb b/rswag-specs/lib/rswag/specs/swagger_formatter.rb
index 8b395d24d..625290562 100644
--- a/rswag-specs/lib/rswag/specs/swagger_formatter.rb
+++ b/rswag-specs/lib/rswag/specs/swagger_formatter.rb
@@ -7,21 +7,18 @@
module Rswag
module Specs
class SwaggerFormatter < ::RSpec::Core::Formatters::BaseTextFormatter
-
- # NOTE: rspec 2.x support
if RSPEC_VERSION > 2
::RSpec::Core::Formatters.register self, :example_group_finished, :stop
end
def initialize(output, config = Rswag::Specs.config)
- @output = output
+ super(output)
@config = config
@output.puts 'Generating Swagger docs ...'
end
def example_group_finished(notification)
- # NOTE: rspec 2.x support
metadata = if RSPEC_VERSION > 2
notification.group.metadata
else
@@ -33,7 +30,7 @@ def example_group_finished(notification)
return if metadata[:document] == false
return unless metadata.key?(:response)
- swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc])
+ swagger_doc = @config.get_openapi_spec(metadata[:openapi_spec] || metadata[:swagger_doc])
unless doc_version(swagger_doc).start_with?('2')
# This is called multiple times per file!
@@ -49,22 +46,39 @@ def example_group_finished(notification)
end
def stop(_notification = nil)
- @config.swagger_docs.each do |url_path, doc|
+ @config.openapi_specs.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.dig(:parameters)
- schema_param = value.dig(:parameters)&.find { |p| (p[:in] == :body || p[:in] == :formData) && p[:schema] }
- mime_list = value.dig(:consumes) || doc[:consumes]
+ 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
+ enum_param = value.dig(:parameters).find{|p| p[:enum]}
+ if enum_param && enum_param.is_a?(Hash)
+ enum_param[:description] = generate_enum_description(enum_param)
+ end
+
value[:parameters].reject! { |p| p[:in] == :body || p[:in] == :formData }
end
remove_invalid_operation_keys!(value)
@@ -72,7 +86,7 @@ def stop(_notification = nil)
end
end
- file_path = File.join(@config.swagger_root, url_path)
+ file_path = File.join(@config.openapi_root, url_path)
dirname = File.dirname(file_path)
FileUtils.mkdir_p dirname unless File.exist?(dirname)
@@ -87,10 +101,10 @@ def stop(_notification = nil)
private
def pretty_generate(doc)
- if @config.swagger_format == :yaml
+ if @config.openapi_format == :yaml
clean_doc = yaml_prepare(doc)
YAML.dump(clean_doc)
- else # config errors are thrown in 'def swagger_format', no throw needed here
+ else # config errors are thrown in 'def openapi_format', no throw needed here
JSON.pretty_generate(doc)
end
end
@@ -135,7 +149,7 @@ def upgrade_content!(mime_list, target_node)
target_node[:content] ||= {}
mime_list.each do |mime_type|
- # TODO upgrade to have content-type specific schema
+ # TODO: upgrade to have content-type specific schema
(target_node[:content][mime_type] ||= {}).merge!(schema: schema)
end
end
@@ -157,7 +171,7 @@ def upgrade_request_type!(metadata)
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)')
+ Rswag::Specs.deprecator.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|
@@ -177,14 +191,14 @@ def upgrade_oauth!(swagger_doc)
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)")
+ Rswag::Specs.deprecator.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)")
+ Rswag::Specs.deprecator.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)")
+ Rswag::Specs.deprecator.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|
@@ -195,9 +209,20 @@ def upgrade_oauth!(swagger_doc)
end
def remove_invalid_operation_keys!(value)
- is_hash = value.is_a?(Hash)
- value.delete(:consumes) if is_hash && value.dig(:consumes)
- value.delete(:produces) if is_hash && value.dig(:produces)
+ return unless value.is_a?(Hash)
+
+ value.delete(:consumes) if value[:consumes]
+ value.delete(:produces) if value[:produces]
+ value.delete(:request_examples) if value[:request_examples]
+ value[:parameters].each { |p| p.delete(:getter) } if value[:parameters]
+ end
+
+ def generate_enum_description(param)
+ enum_description = "#{param[:description]}:\n "
+ param[:enum].each do |k,v|
+ enum_description += "* `#{k}` #{v}\n "
+ end
+ enum_description
end
end
end
diff --git a/rswag-specs/lib/tasks/rswag-specs_tasks.rake b/rswag-specs/lib/tasks/rswag-specs_tasks.rake
index 41f264c44..4aca48d14 100644
--- a/rswag-specs/lib/tasks/rswag-specs_tasks.rake
+++ b/rswag-specs/lib/tasks/rswag-specs_tasks.rake
@@ -11,11 +11,17 @@ namespace :rswag do
'spec/requests/**/*_spec.rb, spec/api/**/*_spec.rb, spec/integration/**/*_spec.rb'
)
- # NOTE: rspec 2.x support
- if Rswag::Specs::RSPEC_VERSION > 2 && Rswag::Specs.config.swagger_dry_run
- t.rspec_opts = ['--format Rswag::Specs::SwaggerFormatter', '--dry-run', '--order defined']
+ additional_rspec_opts = ENV.fetch(
+ 'ADDITIONAL_RSPEC_OPTS',
+ ''
+ )
+
+ t.rspec_opts = [additional_rspec_opts]
+
+ if Rswag::Specs.config.rswag_dry_run
+ t.rspec_opts += ['--format Rswag::Specs::SwaggerFormatter', '--dry-run', '--order defined']
else
- t.rspec_opts = ['--format Rswag::Specs::SwaggerFormatter', '--order defined']
+ t.rspec_opts += ['--format Rswag::Specs::SwaggerFormatter', '--order defined']
end
end
end
diff --git a/rswag-specs/rswag-specs.gemspec b/rswag-specs/rswag-specs.gemspec
index 18434abb0..aa1333591 100644
--- a/rswag-specs/rswag-specs.gemspec
+++ b/rswag-specs/rswag-specs.gemspec
@@ -5,17 +5,20 @@ $LOAD_PATH.push File.expand_path('lib', __dir__)
# Describe your gem and declare its dependencies:
Gem::Specification.new do |s|
s.name = 'rswag-specs'
- s.version = ENV['TRAVIS_TAG'] || '0.0.0'
+ s.version = ENV['RUBYGEMS_VERSION'] || '0.0.0'
s.authors = ['Richie Morris', 'Greg Myers', 'Jay Danielian']
s.email = ['domaindrivendev@gmail.com']
s.homepage = 'https://github.com/rswag/rswag'
s.summary = 'An OpenAPI-based (formerly called Swagger) DSL for rspec-rails & accompanying rake task for generating OpenAPI specification files'
- s.description = 'Simplify API integration testing with a succinct rspec DSL and generate OpenAPI specification files directly from your rspecs. More about the OpenAPI initiative here: http://spec.openapis.org/'
+ s.description = 'Simplify API integration testing with a succinct rspec DSL and generate OpenAPI specification files directly from your rspec tests. More about the OpenAPI initiative here: http://spec.openapis.org/'
s.license = 'MIT'
- s.files = Dir['{lib}/**/*'] + ['MIT-LICENSE', 'Rakefile']
+ s.files = Dir['{lib}/**/*'] + ['MIT-LICENSE', 'Rakefile', '.rubocop_rspec_alias_config.yml']
- s.add_dependency 'activesupport', '>= 3.1', '< 7.1'
- s.add_dependency 'railties', '>= 3.1', '< 7.1'
- s.add_dependency 'json-schema', '~> 2.2'
+ s.add_dependency 'activesupport', '>= 5.2', '< 8.1'
+ s.add_dependency 'json-schema', '>= 2.2', '< 6.0'
+ s.add_dependency 'railties', '>= 5.2', '< 8.1'
+ s.add_dependency 'rspec-core', '>=2.14'
+
+ s.add_development_dependency 'simplecov', '=0.21.2'
end
diff --git a/rswag-specs/spec/generators/rspec/swagger_generator_spec.rb b/rswag-specs/spec/generators/rspec/swagger_generator_spec.rb
index 1349230ff..418fa8c4c 100644
--- a/rswag-specs/spec/generators/rspec/swagger_generator_spec.rb
+++ b/rswag-specs/spec/generators/rspec/swagger_generator_spec.rb
@@ -28,6 +28,16 @@ module Rspec
end
end
+ it 'generates spec file for a controller in a defined directory' do
+ allow_any_instance_of(Rswag::RouteParser).to receive(:routes).and_return(fake_routes)
+ run_generator %w(Posts::CommentsController --spec_path=integration)
+
+ assert_file('spec/integration/posts/comments_spec.rb') do |content|
+ assert_match(/parameter name: 'post_id', in: :path, type: :string/, content)
+ assert_match(/patch\('update_comments comment'\)/, content)
+ end
+ end
+
private
def fake_routes
diff --git a/rswag-specs/spec/rswag/specs/configuration_spec.rb b/rswag-specs/spec/rswag/specs/configuration_spec.rb
index 98345d48a..bc1e47548 100644
--- a/rswag-specs/spec/rswag/specs/configuration_spec.rb
+++ b/rswag-specs/spec/rswag/specs/configuration_spec.rb
@@ -1,101 +1,227 @@
# frozen_string_literal: true
require 'rswag/specs/configuration'
+require 'climate_control'
+
+RSpec.describe Rswag::Specs::Configuration do
+ subject { described_class.new(rspec_config) }
+
+ let(:rspec_config) do
+ OpenStruct.new(
+ openapi_root: openapi_root, openapi_specs: openapi_specs,
+ openapi_format: openapi_format, rswag_dry_run: rswag_dry_run,
+ openapi_strict_schema_validation: openapi_strict_schema_validation,
+ openapi_all_properties_required: openapi_all_properties_required,
+ openapi_no_additional_properties: openapi_no_additional_properties
+ )
+ end
+ let(:openapi_root) { 'foobar' }
+ let(:openapi_specs) do
+ {
+ 'v1/swagger.json' => { swagger: '2.0.0', info: { title: 'v1' } },
+ 'v2/swagger.json' => { openapi: '3.0.0', info: { title: 'v2' } }
+ }
+ end
+ let(:openapi_format) { :yaml }
+ let(:rswag_dry_run) { nil }
+ let(:openapi_strict_schema_validation) { nil }
+ let(:openapi_all_properties_required) { nil }
+ let(:openapi_no_additional_properties) { nil }
-module Rswag
- module Specs
- RSpec.describe Configuration do
- subject { described_class.new(rspec_config) }
+ describe '#openapi_root' do
+ let(:response) { subject.openapi_root }
- let(:rspec_config) do
- OpenStruct.new(swagger_root: swagger_root, swagger_docs: swagger_docs, swagger_format: swagger_format)
- end
- let(:swagger_root) { 'foobar' }
- let(:swagger_docs) do
- {
- 'v1/swagger.json' => { info: { title: 'v1' } },
- 'v2/swagger.json' => { info: { title: 'v2' } }
- }
- end
- let(:swagger_format) { :yaml }
+ context 'provided in rspec config' do
+ it { expect(response).to eq('foobar') }
+ end
- describe '#swagger_root' do
- let(:response) { subject.swagger_root }
+ context 'not provided' do
+ let(:openapi_root) { nil }
+ it { expect { response }.to raise_error Rswag::Specs::ConfigurationError }
+ end
+ end
- context 'provided in rspec config' do
- it { expect(response).to eq('foobar') }
- end
+ 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(:openapi_specs) { nil }
+ it { expect { response }.to raise_error Rswag::Specs::ConfigurationError }
+ end
+
+ context 'provided but empty' do
+ let(:openapi_specs) { {} }
+ it { expect { response }.to raise_error Rswag::Specs::ConfigurationError }
+ end
+ end
+
+ 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 'not provided' do
- let(:swagger_root) { nil }
- it { expect { response }.to raise_error ConfigurationError }
+ context 'unsupported format provided' do
+ let(:openapi_format) { :xml }
+
+ it { expect { response }.to raise_error Rswag::Specs::ConfigurationError }
+ end
+
+ context 'not provided' do
+ let(:openapi_format) { nil }
+
+ it { expect(response).to eq(:json) }
+ end
+ end
+
+ describe '#rswag_dry_run' do
+ let(:response) { subject.rswag_dry_run }
+
+ context 'when not provided' do
+ let(:rswag_dry_run) { nil }
+ it { expect(response).to eq(true) }
+ end
+
+ context 'when environment variable is provided' do
+ context 'when set to 0' do
+ it 'returns false' do
+ ClimateControl.modify RSWAG_DRY_RUN: '0' do
+ expect(response).to eq(false)
+ end
end
end
- describe '#swagger_docs' do
- let(:response) { subject.swagger_docs }
-
- context 'provided in rspec config' do
- it { expect(response).to be_an_instance_of(Hash) }
+ context 'when set to 1' do
+ it 'returns true' do
+ ClimateControl.modify RSWAG_DRY_RUN: '1' do
+ expect(response).to eq(true)
+ end
end
+ end
+ end
- context 'not provided' do
- let(:swagger_docs) { nil }
- it { expect { response }.to raise_error ConfigurationError }
+ context 'when deprecated environment variable is provided' do
+ context 'when set to 0' do
+ it 'returns false' do
+ ClimateControl.modify SWAGGER_DRY_RUN: '0' do
+ expect(response).to eq(false)
+ end
end
+ end
- context 'provided but empty' do
- let(:swagger_docs) { {} }
- it { expect { response }.to raise_error ConfigurationError }
+ context 'when set to 1' do
+ it 'returns true' do
+ ClimateControl.modify SWAGGER_DRY_RUN: '1' do
+ expect(response).to eq(true)
+ end
end
end
+ end
- describe '#swagger_format' do
- let(:response) { subject.swagger_format }
+ context 'when provided in rspec config' do
+ let(:rswag_dry_run) { false }
+ it { expect(response).to eq(false) }
+ end
+ end
- context 'provided in rspec config' do
- it { expect(response).to be_an_instance_of(Symbol) }
- end
+ describe '#get_openapi_spec(tag=nil)' do
+ let(:openapi_spec) { subject.get_openapi_spec(tag) }
- context 'unsupported format provided' do
- let(:swagger_format) { :xml }
+ context 'no tag provided' do
+ let(:tag) { nil }
- it { expect { response }.to raise_error ConfigurationError }
- end
+ it 'returns the first doc in rspec config' do
+ expect(openapi_spec).to match hash_including(info: { title: 'v1' })
+ end
+ end
- context 'not provided' do
- let(:swagger_format) { nil }
+ context 'tag provided' do
+ context 'matching doc' do
+ let(:tag) { 'v2/swagger.json' }
- it { expect(response).to eq(:json) }
+ it 'returns the matching doc in rspec config' do
+ expect(openapi_spec).to match hash_including(info: { title: 'v2' })
end
end
- describe '#get_swagger_doc(tag=nil)' do
- let(:swagger_doc) { subject.get_swagger_doc(tag) }
+ context 'no matching doc' do
+ let(:tag) { 'foobar' }
+ it { expect { openapi_spec }.to raise_error Rswag::Specs::ConfigurationError }
+ end
+ end
+ end
+
+ describe '#get_openapi_spec_version' do
+ let(:response) { subject.get_openapi_spec_version(tag) }
- context 'no tag provided' do
- let(:tag) { nil }
+ context 'when tag provided' do
+ context 'with matching doc' do
+ let(:tag) { 'v2/swagger.json' }
- it 'returns the first doc in rspec config' do
- expect(swagger_doc).to eq(info: { title: 'v1' })
- end
+ it 'returns the matching version in rspec config' do
+ expect(response).to eq('3.0.0')
end
+ end
- context 'tag provided' do
- context 'matching doc' do
- let(:tag) { 'v2/swagger.json' }
+ context 'with no matching doc' do
+ let(:tag) { 'foobar' }
+ it { expect { response }.to raise_error Rswag::Specs::ConfigurationError }
+ end
+ end
- it 'returns the matching doc in rspec config' do
- expect(swagger_doc).to eq(info: { title: 'v2' })
- end
- end
+ context 'when no tag provided' do
+ let(:tag) { nil }
- context 'no matching doc' do
- let(:tag) { 'foobar' }
- it { expect { swagger_doc }.to raise_error ConfigurationError }
- end
- end
+ it 'returns the first version in rspec config' do
+ expect(response).to eq('2.0.0')
end
end
end
+
+ describe '#openapi_strict_schema_validation' do
+ let(:response) { subject.openapi_strict_schema_validation }
+
+ context 'when not provided' do
+ let(:openapi_strict_schema_validation) { nil }
+ it { expect(response).to eq(false) }
+ end
+
+ context 'when provided in rspec config' do
+ let(:openapi_strict_schema_validation) { true }
+ it { expect(response).to eq(true) }
+ end
+ end
+
+ describe '#openapi_all_properties_required' do
+ let(:response) { subject.openapi_all_properties_required }
+
+ context 'when not provided' do
+ let(:openapi_all_properties_required) { nil }
+ it { expect(response).to eq(false) }
+ end
+
+ context 'when provided in rspec config' do
+ let(:openapi_all_properties_required) { true }
+ it { expect(response).to eq(true) }
+ end
+ end
+
+ describe '#openapi_no_additional_properties' do
+ let(:response) { subject.openapi_no_additional_properties }
+
+ context 'when not provided' do
+ let(:openapi_no_additional_properties) { nil }
+ it { expect(response).to eq(false) }
+ end
+
+ context 'when provided in rspec config' do
+ let(:openapi_no_additional_properties) { true }
+ it { expect(response).to eq(true) }
+ end
+ end
end
diff --git a/rswag-specs/spec/rswag/specs/example_group_helpers_spec.rb b/rswag-specs/spec/rswag/specs/example_group_helpers_spec.rb
index 40bca7bcf..2ebf5c9a1 100644
--- a/rswag-specs/spec/rswag/specs/example_group_helpers_spec.rb
+++ b/rswag-specs/spec/rswag/specs/example_group_helpers_spec.rb
@@ -26,12 +26,24 @@ module Specs
end
describe '#get|post|patch|put|delete|head|options|trace(verb, summary)' do
- before { subject.post('Creates a blog') }
+ context 'when called without keyword arguments' do
+ before { subject.post('Creates a blog') }
- it "delegates to 'describe' with 'operation' metadata" do
- expect(subject).to have_received(:describe).with(
- :post, operation: { verb: :post, summary: 'Creates a blog' }
- )
+ it "delegates to 'describe' with 'operation' metadata" do
+ expect(subject).to have_received(:describe).with(
+ :post, operation: { verb: :post, summary: 'Creates a blog' }
+ )
+ end
+ end
+
+ context 'when called with keyword arguments' do
+ before { subject.post('Creates a blog', foo: 'bar') }
+
+ it "delegates to 'describe' with 'operation' metadata and provided metadata" do
+ expect(subject).to have_received(:describe).with(
+ :post, operation: { verb: :post, summary: 'Creates a blog' }, foo: 'bar'
+ )
+ end
end
end
@@ -136,29 +148,123 @@ module Specs
end
end
+ describe '#request_body_example(value:, summary: nil, name: nil)' do
+ context "when adding one example" do
+ before { subject.request_body_example(value: value)}
+ let(:api_metadata) { { operation: {} } }
+ let(:value) { { field: 'A', another_field: 'B' } }
+
+ it "assigns the example to the metadata" do
+ expect(api_metadata[:operation][:request_examples].length()).to eq(1)
+ expect(api_metadata[:operation][:request_examples][0]).to eq({ value: value, name: 0 })
+ end
+ end
+
+ context "when adding multiple examples with additional information" do
+ before {
+ subject.request_body_example(value: example_one)
+ subject.request_body_example(value: example_two, name: example_two_name, summary: example_two_summary)
+ }
+ let(:api_metadata) { { operation: {} } }
+ let(:example_one) { { field: 'A', another_field: 'B' } }
+ let(:example_two) { { field: 'B', another_field: 'C' } }
+ let(:example_two_name) { 'example_two' }
+ let(:example_two_summary) { 'An example description' }
+
+ it "assigns all examples to the metadata" do
+ expect(api_metadata[:operation][:request_examples].length()).to eq(2)
+ expect(api_metadata[:operation][:request_examples][0]).to eq({ value: example_one, name: 0 })
+ expect(api_metadata[:operation][:request_examples][1]).to eq({ value: example_two, name: example_two_name, summary: example_two_summary })
+ end
+ end
+ end
+
+
describe '#examples(example)' do
let(:mime) { 'application/json' }
let(:json_example) do
{
- mime => {
foo: 'bar'
+ }
+ end
+ let(:api_metadata) { { response: {} } }
+
+ before do
+ subject.examples(mime => json_example)
+ end
+
+ it "adds to the 'response examples' metadata" do
+ expect(api_metadata[:response][:content]).to match(
+ mime => {
+ examples: {
+ example_0: {
+ value: json_example
+ }
+ }
}
+ )
+ end
+ end
+
+ describe '#example(single)' do
+ let(:mime) { 'application/json' }
+ let(:summary) { "this is a summary"}
+ let(:description) { "this is an example description "}
+ let(:json_example) do
+ {
+ foo: 'bar'
}
end
let(:api_metadata) { { response: {} } }
before do
- subject.examples(json_example)
+ subject.example(mime, :example_key, json_example, summary, description)
end
it "adds to the 'response examples' metadata" do
expect(api_metadata[:response][:content]).to match(
mime => {
- example: json_example[mime]
+ examples: {
+ example_key: {
+ value: json_example,
+ description: description,
+ summary: summary
+ }
+ }
}
)
end
end
+
+ describe "#run_test!" do
+ let(:rspec_version) { 3 }
+ let(:api_metadata) {
+ {
+ response: {
+ code: "200"
+ }
+ }
+ }
+
+ before do
+ stub_const("RSPEC_VERSION", rspec_version)
+ allow(subject).to receive(:before)
+ end
+
+ it "executes a specification" do
+ expected_spec_description = "returns a 200 response"
+ expect(subject).to receive(:it).with(expected_spec_description, rswag: true)
+ subject.run_test!
+ end
+
+ context "when options[:description] is passed" do
+ it "executes a specification described with passed description" do
+ expected_spec_description = "returns a 200 response - with a custom description"
+ expect(subject).to receive(:it).with(expected_spec_description, rswag: true)
+ subject.run_test!(expected_spec_description)
+ end
+ end
+ end
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 0f3e1ba32..3fd401d7f 100644
--- a/rswag-specs/spec/rswag/specs/example_helpers_spec.rb
+++ b/rswag-specs/spec/rswag/specs/example_helpers_spec.rb
@@ -10,11 +10,11 @@ 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: {
@@ -52,7 +52,7 @@ module Specs
allow(subject).to receive(:blog_id).and_return(1)
allow(subject).to receive(:id).and_return(2)
allow(subject).to receive(:q1).and_return('foo')
- allow(subject).to receive(:api_key).and_return('fookey')
+ allow(subject).to receive(:api_key).and_return('fooKey')
allow(subject).to receive(:blog).and_return(text: 'Some comment')
allow(subject).to receive(:put)
subject.submit_request(metadata)
@@ -60,7 +60,7 @@ module Specs
it "submits a request built from metadata and 'let' values" do
expect(subject).to have_received(:put).with(
- '/blogs/1/comments/2?q1=foo&api_key=fookey',
+ '/blogs/1/comments/2?q1=foo&api_key=fooKey',
'{"text":"Some comment"}',
{ 'CONTENT_TYPE' => 'application/json' }
)
diff --git a/rswag-specs/spec/rswag/specs/request_factory_spec.rb b/rswag-specs/spec/rswag/specs/request_factory_spec.rb
index a26f69459..a0776e299 100644
--- a/rswag-specs/spec/rswag/specs/request_factory_spec.rb
+++ b/rswag-specs/spec/rswag/specs/request_factory_spec.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+# cspell:ignore Bfoo Bbar
+
require 'rswag/specs/request_factory'
module Rswag
@@ -8,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) { { swagger: '2.0' } }
let(:example) { double('example') }
let(:metadata) do
{
@@ -35,12 +37,38 @@ module Specs
{ name: 'blog_id', in: :path, type: :number },
{ name: 'id', in: :path, type: :number }
]
- allow(example).to receive(:blog_id).and_return(1)
- allow(example).to receive(:id).and_return(2)
end
- it 'builds the path from example values' do
- expect(request[:path]).to eq('/blogs/1/comments/2')
+ context 'when `name` parameter key is required, but not defined within example group' do
+ it 'explicitly warns user about missing parameter, instead of giving generic error' do
+ expect { request[:path] }.to raise_error(/parameter key present, but not defined/)
+ end
+ end
+
+ context 'when `name` is defined' do
+ before do
+ allow(example).to receive(:blog_id).and_return(1)
+ allow(example).to receive(:id).and_return(2)
+ end
+
+ it 'builds the path from example values' do
+ expect(request[:path]).to eq('/blogs/1/comments/2')
+ end
+
+ context 'when `getter is defined`' do
+ before do
+ metadata[:operation][:parameters] = [
+ { name: 'blog_id', in: :path, type: :number },
+ { name: 'id', in: :path, type: :number, getter: :param_id }
+ ]
+
+ allow(example).to receive(:param_id).and_return(123)
+ end
+
+ it 'builds the path using getter method' do
+ expect(request[:path]).to eq('/blogs/1/comments/123')
+ end
+ end
end
end
@@ -57,60 +85,264 @@ module Specs
it 'builds the query string from example values' do
expect(request[:path]).to eq('/blogs?q1=foo&q2=bar')
end
+
+ context 'when `getter is defined`' do
+ before do
+ metadata[:operation][:parameters] << {
+ name: 'status', in: :query, type: :string, getter: :q3_status
+ }
+
+ allow(example).to receive(:status).and_return(nil)
+ allow(example).to receive(:q3_status).and_return(123)
+ end
+
+ it 'builds the query string using getter method' do
+ expect(request[:path]).to eq('/blogs?q1=foo&q2=bar&status=123')
+ end
+ end
end
context "'query' parameters of type 'array'" do
before do
metadata[:operation][:parameters] = [
- { name: 'things', in: :query, type: :array, collectionFormat: collection_format }
+ { name: 'things', in: :query, type: :array, collectionFormat: collection_format },
+ { name: 'numbers', in: :query, type: :array, collectionFormat: collection_format, getter: :magic_numbers },
]
allow(example).to receive(:things).and_return(['foo', 'bar'])
+ allow(example).to receive(:magic_numbers).and_return([0, 1])
+ expect(example).not_to receive(:numbers)
end
context 'collectionFormat = csv' do
let(:collection_format) { :csv }
it 'formats as comma separated values' do
- expect(request[:path]).to eq('/blogs?things=foo,bar')
+ expect(request[:path]).to eq('/blogs?things=foo,bar&numbers=0,1')
end
end
context 'collectionFormat = ssv' do
let(:collection_format) { :ssv }
it 'formats as space separated values' do
- expect(request[:path]).to eq('/blogs?things=foo bar')
+ expect(request[:path]).to eq('/blogs?things=foo bar&numbers=0 1')
end
end
context 'collectionFormat = tsv' do
let(:collection_format) { :tsv }
it 'formats as tab separated values' do
- expect(request[:path]).to eq('/blogs?things=foo\tbar')
+ expect(request[:path]).to eq('/blogs?things=foo\tbar&numbers=0\t1')
end
end
context 'collectionFormat = pipes' do
let(:collection_format) { :pipes }
it 'formats as pipe separated values' do
- expect(request[:path]).to eq('/blogs?things=foo|bar')
+ expect(request[:path]).to eq('/blogs?things=foo|bar&numbers=0|1')
end
end
context 'collectionFormat = multi' do
let(:collection_format) { :multi }
it 'formats as multiple parameter instances' do
- expect(request[:path]).to eq('/blogs?things=foo&things=bar')
+ expect(request[:path]).to eq('/blogs?things=foo&things=bar&numbers=0&numbers=1')
+ end
+ end
+ end
+
+ context "'query' parameter of format 'datetime'" do
+ let(:date_time) { DateTime.new(2001, 2, 3, 4, 5, 6, '-7').to_s }
+
+ before do
+ metadata[:operation][:parameters] = [
+ { name: 'date_time', in: :query, type: :string, format: :datetime, }
+ ]
+ allow(example).to receive(:date_time).and_return(date_time)
+ end
+
+ it 'formats the datetime properly' do
+ expect(request[:path]).to eq('/blogs?date_time=2001-02-03T04%3A05%3A06-07%3A00')
+ end
+
+ context "iso8601 format" do
+ let(:date_time) { DateTime.new(2001, 2, 3, 4, 5, 6, '-7').iso8601 }
+ it 'is also formatted properly' do
+ expect(request[:path]).to eq('/blogs?date_time=2001-02-03T04%3A05%3A06-07%3A00')
+ end
+ end
+
+ context 'openapi spec >= 3.0.0' do
+ let(:openapi_spec) { { swagger: '3.0' } }
+ before do
+ allow(example).to receive(:date_time).and_return(date_time)
+ end
+
+ it 'formats the datetime properly when type is defined in schema' do
+ metadata[:operation][:parameters] = [
+ { name: 'date_time', in: :query, schema: { type: :string }, format: :datetime, }
+ ]
+ expect(request[:path]).to eq('/blogs?date_time=2001-02-03T04%3A05%3A06-07%3A00')
+ end
+ end
+ end
+
+ context "'query' parameters of type 'object'" do
+ let(:things) { {'foo': 'bar'} }
+ let(:openapi_spec) { { swagger: '3.0' } }
+
+ before do
+ metadata[:operation][:parameters] = [
+ {
+ name: 'things', in: :query,
+ style: style,
+ explode: explode,
+ schema: { type: :object, additionalProperties: { type: :string } }
+ }
+ ]
+ allow(example).to receive(:things).and_return(things)
+ end
+
+ context 'deepObject' do
+ let(:style) { :deepObject }
+ let(:explode) { true }
+ it 'formats as deep object' do
+ expect(request[:path]).to eq('/blogs?things%5Bfoo%5D=bar')
+ end
+ end
+
+ context 'deepObject with nested objects' do
+ let(:things) { {'foo': { 'bar': 'baz' }} }
+ let(:style) { :deepObject }
+ let(:explode) { true }
+ it 'formats as deep object' do
+ expect(request[:path]).to eq('/blogs?things%5Bfoo%5D%5Bbar%5D=baz')
+ end
+ end
+
+ context 'form explode=false' do
+ let(:style) { :form }
+ let(:explode) { false }
+ it 'formats as unexploded form' do
+ expect(request[:path]).to eq('/blogs?things=foo,bar')
+ end
+ end
+
+ context 'form explode=true' do
+ let(:style) { :form }
+ let(:explode) { true }
+ it 'formats as an exploded form' do
+ expect(request[:path]).to eq('/blogs?foo=bar')
+ end
+ end
+
+ context 'form explode=true with nesting and uri encodable output' do
+ let(:things) { {'foo': { 'bar': 'baz' }, 'fo&b': 'x[]?y'} }
+ let(:style) { :form }
+ let(:explode) { true }
+ it 'formats as an exploded form' do
+ expect(request[:path]).to eq('/blogs?fo%26b=x%5B%5D%3Fy&foo%5Bbar%5D=baz')
+ end
+ end
+ end
+
+ context "'query' parameters of type 'array'" do
+ let(:id) { [3, 4, 5] }
+ let(:openapi_spec) { { swagger: '3.0' } }
+
+ before do
+ metadata[:operation][:parameters] = [
+ {
+ name: 'id', in: :query,
+ style: style,
+ explode: explode,
+ schema: { type: :array, items: { type: :integer } }
+ }
+ ]
+ allow(example).to receive(:id).and_return(id)
+ end
+
+ context 'form' do
+ let(:style) { :form }
+ context 'exploded' do
+ let(:explode) { true }
+ it 'formats as exploded form' do
+ expect(request[:path]).to eq('/blogs?id=3&id=4&id=5')
+ end
+ end
+
+ context 'not exploded' do
+ let(:explode) { false }
+ it 'formats as unexploded form' do
+ expect(request[:path]).to eq('/blogs?id=3,4,5')
+ end
+ end
+ end
+
+ context "spaceDelimited" do
+ let(:style) { :spaceDelimited }
+ context 'exploded' do
+ let(:explode) { true }
+ it 'formats as exploded form' do
+ expect(request[:path]).to eq('/blogs?id=3&id=4&id=5')
+ end
+ end
+
+ context 'not exploded' do
+ let(:explode) { false }
+ it 'formats as unexploded form' do
+ expect(request[:path]).to eq('/blogs?id=3%204%205')
+ end
+ end
+ end
+
+ context "pipeDelimited" do
+ let(:style) { :pipeDelimited }
+ context 'exploded' do
+ let(:explode) { true }
+ it 'formats as exploded form' do
+ expect(request[:path]).to eq('/blogs?id=3&id=4&id=5')
+ end
+ end
+
+ context 'not exploded' do
+ let(:explode) { false }
+ it 'formats as unexploded form' do
+ expect(request[:path]).to eq('/blogs?id=3|4|5')
+ end
end
end
end
+ context "'query' parameters with schema reference" do
+ let(:things) { 'foo' }
+ let(:openapi_spec) { { swagger: '3.0' } }
+
+ before do
+ metadata[:operation][:parameters] = [
+ {
+ name: 'things', in: :query,
+ schema: { '$ref' => '#/components/schemas/FooType' }
+ }
+ ]
+ allow(example).to receive(:things).and_return(things)
+ end
+
+ it 'builds the query string' do
+ expect(request[:path]).to eq('/blogs?things=foo')
+ end
+ end
+
context "'header' parameters" do
before do
- metadata[:operation][:parameters] = [{ name: 'Api-Key', in: :header, type: :string }]
+ metadata[:operation][:parameters] = [
+ { name: 'Api-Key', in: :header, type: :string },
+ { name: 'Token', getter: :token_param, in: :header, type: :string }
+ ]
allow(example).to receive(:'Api-Key').and_return('foobar')
+ allow(example).to receive(:'token_param').and_return('my_token')
end
it 'adds names and example values to headers' do
- expect(request[:headers]).to eq({ 'Api-Key' => 'foobar' })
+ expect(request[:headers]).to eq({ 'Api-Key' => 'foobar', 'Token' => 'my_token' })
end
end
@@ -160,6 +392,18 @@ module Specs
end
end
+ context 'JSON:API payload' do
+ before do
+ metadata[:operation][:consumes] = 'application/vnd.api+json'
+ metadata[:operation][:parameters] = [{ name: 'comment', in: :body, schema: { type: 'object' } }]
+ allow(example).to receive(:comment).and_return(text: 'Some comment')
+ end
+
+ it "serializes first 'body' parameter to JSON object" do
+ expect(request[:payload]).to eq(text: 'Some comment')
+ end
+ end
+
context 'missing body parameter' do
before do
metadata[:operation][:parameters] = [{ name: 'comment', in: :body, schema: { type: 'object' } }]
@@ -193,6 +437,18 @@ module Specs
)
end
end
+
+ context 'plain text payload' do
+ before do
+ metadata[:operation][:consumes] = ['text/plain']
+ metadata[:operation][:parameters] = [{ name: 'comment', in: :body, type: 'string' }]
+ allow(example).to receive(:comment).and_return('plain text comment')
+ end
+
+ it 'keeps payload as a raw string data' do
+ expect(request[:payload]).to eq('plain text comment')
+ end
+ end
end
context 'produces content' do
@@ -217,10 +473,32 @@ module Specs
end
end
+ context 'host header' do
+ context "explicit 'Host' value provided" do
+ before do
+ metadata[:operation][:host] = 'swagger.io'
+ end
+
+ it "sets 'Host' header" do
+ expect(request[:headers]).to eq('HTTP_HOST' => 'swagger.io')
+ end
+ end
+
+ context "no 'Host' value provided" do
+ before do
+ metadata[:operation][:host] = nil
+ end
+
+ it "does not set 'Host' header" do
+ expect(request[:headers]).to eq({})
+ end
+ end
+ end
+
context 'basic auth' do
context 'swagger 2.0' do
before do
- swagger_doc[:securityDefinitions] = { basic: { type: :basic } }
+ openapi_spec[:securityDefinitions] = { basic: { type: :basic } }
metadata[:operation][:security] = [basic: []]
allow(example).to receive(:Authorization).and_return('Basic foobar')
end
@@ -231,9 +509,9 @@ module Specs
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
@@ -244,26 +522,26 @@ module Specs
end
context 'openapi 3.0.1 upgrade notice' do
- let(:swagger_doc) { { openapi: '3.0.1' } }
+ let(:openapi_spec) { { openapi: '3.0.1' } }
before do
- allow(ActiveSupport::Deprecation).to receive(:warn)
- swagger_doc[:securityDefinitions] = { basic: { type: :basic } }
+ allow(Rswag::Specs.deprecator).to receive(:warn)
+ openapi_spec[: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)
+ expect(Rswag::Specs.deprecator).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)
+ expect(openapi_spec[: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 } }
+ openapi_spec[:securityDefinitions] = { apiKey: { type: :apiKey, name: 'api_key', in: key_location } }
metadata[:operation][:security] = [apiKey: []]
allow(example).to receive(:api_key).and_return('foobar')
end
@@ -304,7 +582,7 @@ module Specs
context 'oauth2' do
before do
- swagger_doc[:securityDefinitions] = { oauth2: { type: :oauth2, scopes: ['read:blogs'] } }
+ openapi_spec[:securityDefinitions] = { oauth2: { type: :oauth2, scopes: ['read:blogs'] } }
metadata[:operation][:security] = [oauth2: ['read:blogs']]
allow(example).to receive(:Authorization).and_return('Bearer foobar')
end
@@ -316,7 +594,7 @@ module Specs
context 'paired security requirements' do
before do
- swagger_doc[:securityDefinitions] = {
+ openapi_spec[:securityDefinitions] = {
basic: { type: :basic },
api_key: { type: :apiKey, name: 'api_key', in: :query }
}
@@ -347,7 +625,7 @@ module Specs
context 'referenced parameters' do
context 'swagger 2.0' do
before do
- swagger_doc[:parameters] = { q1: { name: 'q1', in: :query, type: :string } }
+ openapi_spec[:parameters] = { q1: { name: 'q1', in: :query, type: :string } }
metadata[:operation][:parameters] = [{ '$ref' => '#/parameters/q1' }]
allow(example).to receive(:q1).and_return('foo')
end
@@ -358,9 +636,9 @@ module Specs
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
@@ -371,34 +649,70 @@ module Specs
end
context 'openapi 3.0.1 upgrade notice' do
- let(:swagger_doc) { { openapi: '3.0.1' } }
+ let(:openapi_spec) { { openapi: '3.0.1' } }
before do
- allow(ActiveSupport::Deprecation).to receive(:warn)
- swagger_doc[:parameters] = { q1: { name: 'q1', in: :query, type: :string } }
+ allow(Rswag::Specs.deprecator).to receive(:warn)
+ openapi_spec[: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)
+ expect(Rswag::Specs.deprecator).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)
+ expect(Rswag::Specs.deprecator).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 'global basePath' do
- before { swagger_doc[:basePath] = '/api' }
+ context 'base path' do
+ context 'openapi 2.0' do
+ before { openapi_spec[:basePath] = '/api' }
+
+ it 'prepends to the path' do
+ expect(request[:path]).to eq('/api/blogs')
+ end
+ end
+
+ context 'openapi 3.0' do
+ before do
+ openapi_spec[:servers] = [{
+ :url => "{protocol}://{defaultHost}",
+ :variables => {
+ :protocol => {
+ :default => :https
+ },
+ :defaultHost => {
+ :default => "www.example.com"
+ }
+ }
+ }]
+ end
- it 'prepends to the path' do
- expect(request[:path]).to eq('/api/blogs')
+ it 'generates the path' do
+ expect(request[:path]).to eq('/blogs')
+ end
+ end
+
+ context 'openapi 3.0 with old config' do
+ let(:openapi_spec) { {:openapi => '3.0', :basePath => '/blogs' } }
+
+ before do
+ allow(Rswag::Specs.deprecator).to receive(:warn)
+ end
+
+ it 'generates the path' do
+ expect(request[:headers]).to eq({})
+ expect(Rswag::Specs.deprecator).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')
@@ -407,12 +721,12 @@ 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[:securityDefinitions] = { apiKey: { type: :apiKey, name: 'api_key', in: :query } }
+ openapi_spec[:security] = [apiKey: []]
allow(example).to receive(:api_key).and_return('foobar')
end
- it 'applieds the scheme by default' do
+ it 'applies the scheme by default' do
expect(request[:path]).to eq('/blogs?api_key=foobar')
end
end
diff --git a/rswag-specs/spec/rswag/specs/response_validator_spec.rb b/rswag-specs/spec/rswag/specs/response_validator_spec.rb
index e4c8e9c2e..e2151b930 100644
--- a/rswag-specs/spec/rswag/specs/response_validator_spec.rb
+++ b/rswag-specs/spec/rswag/specs/response_validator_spec.rb
@@ -8,33 +8,78 @@ 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)
+ allow(config).to receive(:get_openapi_spec_version).and_return('2.0')
+ allow(config).to receive(:openapi_strict_schema_validation).and_return(openapi_strict_schema_validation)
+ allow(config).to receive(:openapi_all_properties_required).and_return(openapi_all_properties_required)
+ allow(config).to receive(:openapi_no_additional_properties).and_return(openapi_no_additional_properties)
end
+
let(:config) { double('config') }
- let(:swagger_doc) { {} }
+ let(:openapi_spec) { {} }
let(:example) { double('example') }
+ let(:openapi_strict_schema_validation) { false }
+ let(:openapi_all_properties_required) { false }
+ let(:openapi_no_additional_properties) { false }
+ let(:schema) do
+ {
+ type: :object,
+ properties: {
+ text: { type: :string },
+ number: { type: :integer }
+ },
+ required: ['text', 'number']
+ }
+ end
+
let(:metadata) do
{
response: {
code: 200,
- headers: { 'X-Rate-Limit-Limit' => { type: :integer } },
- schema: {
- type: :object,
- properties: { text: { type: :string } },
- required: ['text']
- }
+ headers: {
+ 'X-Rate-Limit-Limit' => { type: :integer },
+ 'X-Cursor' => {
+ schema: {
+ type: :string
+ },
+ required: false
+ },
+ 'X-Per-Page' => {
+ schema: {
+ type: :string,
+ nullable: true
+ }
+ }
+ },
+ schema: { **schema }
}
}
end
+ shared_context 'with strict deprecation warning' do
+ before do
+ allow(Rswag::Specs.deprecator).to receive(:warn)
+ end
+
+ after do
+ expect(Rswag::Specs.deprecator)
+ .to have_received(:warn).with('Rswag::Specs: WARNING: This option will be removed in v3.0' \
+ ' please use openapi_all_properties_required' \
+ ' and openapi_no_additional_properties set to true')
+ end
+ end
+
describe '#validate!(metadata, response)' do
let(:call) { subject.validate!(metadata, response) }
let(:response) do
OpenStruct.new(
code: '200',
- headers: { 'X-Rate-Limit-Limit' => '10' },
- body: '{"text":"Some comment"}'
+ headers: {
+ 'X-Rate-Limit-Limit' => '10',
+ 'X-Cursor' => 'test_cursor',
+ 'X-Per-Page' => 25
+ },
+ body: '{"text":"Some comment", "number": 3}'
)
end
@@ -52,15 +97,670 @@ module Specs
it { expect { call }.to raise_error(/Expected response header/) }
end
+ context 'response headers do not include optional header' do
+ before {
+ response.headers = {
+ 'X-Rate-Limit-Limit' => '10',
+ 'X-Per-Page' => 25
+ }
+ }
+ it { expect { call }.to_not raise_error }
+ end
+
+ context 'response headers include nullable header' do
+ before {
+ response.headers = {
+ 'X-Rate-Limit-Limit' => '10',
+ 'X-Cursor' => 'test_cursor',
+ 'X-Per-Page' => nil
+ }
+ }
+ it { expect { call }.to_not raise_error }
+ end
+
+ context 'response headers missing nullable header' do
+ before {
+ response.headers = {
+ 'X-Rate-Limit-Limit' => '10',
+ 'X-Cursor' => 'test_cursor'
+ }
+ }
+ it { expect { call }.to raise_error(/Expected response header/) }
+ end
+
context 'response body differs from metadata' do
before { response.body = '{"foo":"Some comment"}' }
it { expect { call }.to raise_error(/Expected response body/) }
end
+ context "when response body does not have additional properties and missing properties" do
+ context "with strict schema validation enabled" do
+ let(:openapi_strict_schema_validation) { true }
+
+ it { expect { call }.not_to raise_error }
+ end
+
+ context "with strict schema validation disabled" do
+ let(:openapi_strict_schema_validation) { false }
+
+ it { expect { call }.not_to raise_error }
+ end
+
+ context "with strict schema validation disabled in config but enabled in metadata" do
+ let(:openapi_strict_schema_validation) { false }
+ let(:metadata) { super().merge(openapi_strict_schema_validation: true) }
+
+ include_context 'with strict deprecation warning'
+
+ it { expect { call }.not_to raise_error }
+ end
+
+ context "with strict schema validation enabled in config but disabled in metadata" do
+ let(:openapi_strict_schema_validation) { true }
+ let(:metadata) { super().merge(openapi_strict_schema_validation: false) }
+
+ include_context 'with strict deprecation warning'
+
+ it { expect { call }.not_to raise_error }
+ end
+
+ context 'with all properties required enabled' do
+ let(:openapi_all_properties_required) { true }
+
+ it { expect { call }.not_to raise_error }
+ end
+
+ context 'with all properties required disabled' do
+ let(:openapi_all_properties_required) { false }
+
+ it { expect { call }.not_to raise_error }
+ end
+
+ context "with all properties required disabled in config but enabled in metadata" do
+ let(:openapi_all_properties_required) { false }
+ let(:metadata) { super().merge(openapi_all_properties_required: true) }
+
+ it { expect { call }.not_to raise_error }
+ end
+
+ context "with all properties required enabled in config but disabled in metadata" do
+ let(:openapi_all_properties_required) { true }
+ let(:metadata) { super().merge(openapi_all_properties_required: false) }
+
+ it { expect { call }.not_to raise_error }
+ end
+
+ context 'with no additional properties enabled' do
+ let(:openapi_no_additional_properties) { true }
+
+ it { expect { call }.not_to raise_error }
+ end
+
+ context 'with no additional properties disabled' do
+ let(:openapi_no_additional_properties) { false }
+
+ it { expect { call }.not_to raise_error }
+ end
+
+ context "with no additional properties validation disabled in config but enabled in metadata" do
+ let(:openapi_no_additional_properties) { false }
+ let(:metadata) { super().merge(openapi_no_additional_properties: true) }
+
+ it { expect { call }.not_to raise_error }
+ end
+
+ context "with no additional properties validation enabled in config but disabled in metadata" do
+ let(:openapi_no_additional_properties) { true }
+ let(:metadata) { super().merge(openapi_no_additional_properties: false) }
+
+ it { expect { call }.not_to raise_error }
+ end
+
+ context 'when schema does not have required property' do
+ let(:schema) do
+ {
+ type: :object,
+ properties: {
+ text: { type: :string },
+ number: { type: :integer }
+ }
+ }
+ end
+
+ context "with strict schema validation enabled" do
+ let(:openapi_strict_schema_validation) { true }
+
+ it { expect { call }.not_to raise_error }
+ end
+
+ context "with strict schema validation disabled" do
+ let(:openapi_strict_schema_validation) { false }
+
+ it { expect { call }.not_to raise_error }
+ end
+
+ context "with strict schema validation disabled in config but enabled in metadata" do
+ let(:openapi_strict_schema_validation) { false }
+ let(:metadata) { super().merge(openapi_strict_schema_validation: true) }
+
+ include_context 'with strict deprecation warning'
+
+ it { expect { call }.not_to raise_error }
+ end
+
+ context "with strict schema validation enabled in config but disabled in metadata" do
+ let(:openapi_strict_schema_validation) { true }
+ let(:metadata) { super().merge(openapi_strict_schema_validation: false) }
+
+ include_context 'with strict deprecation warning'
+
+ it { expect { call }.not_to raise_error }
+ end
+
+ context 'with all properties required enabled' do
+ let(:openapi_all_properties_required) { true }
+
+ it { expect { call }.not_to raise_error }
+ end
+
+ context 'with all properties required disabled' do
+ let(:openapi_all_properties_required) { false }
+
+ it { expect { call }.not_to raise_error }
+ end
+
+ context "with all properties required disabled in config but enabled in metadata" do
+ let(:openapi_all_properties_required) { false }
+ let(:metadata) { super().merge(openapi_all_properties_required: true) }
+
+ it { expect { call }.not_to raise_error }
+ end
+
+ context "with all properties required enabled in config but disabled in metadata" do
+ let(:openapi_all_properties_required) { true }
+ let(:metadata) { super().merge(openapi_all_properties_required: false) }
+
+ it { expect { call }.not_to raise_error }
+ end
+
+ context 'with no additional properties enabled' do
+ let(:openapi_no_additional_properties) { true }
+
+ it { expect { call }.not_to raise_error }
+ end
+
+ context 'with no additional properties disabled' do
+ let(:openapi_no_additional_properties) { false }
+
+ it { expect { call }.not_to raise_error }
+ end
+
+ context "with no additional properties validation disabled in config but enabled in metadata" do
+ let(:openapi_no_additional_properties) { false }
+ let(:metadata) { super().merge(openapi_no_additional_properties: true) }
+
+ it { expect { call }.not_to raise_error }
+ end
+
+ context "with no additional properties validation enabled in config but disabled in metadata" do
+ let(:openapi_no_additional_properties) { true }
+ let(:metadata) { super().merge(openapi_no_additional_properties: false) }
+
+ it { expect { call }.not_to raise_error }
+ end
+ end
+ end
+
+ context "when response body has additional properties" do
+ before { response.body = '{"foo":"Some comment", "number": 3, "text":"bar"}' }
+
+ context "with strict schema validation enabled" do
+ let(:openapi_strict_schema_validation) { true }
+
+ it { expect { call }.to raise_error /Expected response body/ }
+ end
+
+ context "with strict schema validation disabled" do
+ let(:openapi_strict_schema_validation) { false }
+
+ it { expect { call }.not_to raise_error }
+ end
+
+ context "with strict schema validation disabled in config but enabled in metadata" do
+ let(:openapi_strict_schema_validation) { false }
+ let(:metadata) { super().merge(openapi_strict_schema_validation: true) }
+
+ include_context 'with strict deprecation warning'
+
+ it { expect { call }.to raise_error /Expected response body/ }
+ end
+
+ context "with strict schema validation enabled in config but disabled in metadata" do
+ let(:openapi_strict_schema_validation) { true }
+ let(:metadata) { super().merge(openapi_strict_schema_validation: false) }
+
+ include_context 'with strict deprecation warning'
+
+ it { expect { call }.not_to raise_error }
+ end
+
+ context 'with all properties required enabled' do
+ let(:openapi_all_properties_required) { true }
+
+ it { expect { call }.not_to raise_error }
+ end
+
+ context 'with all properties required disabled' do
+ let(:openapi_all_properties_required) { false }
+
+ it { expect { call }.not_to raise_error }
+ end
+
+ context "with all properties required disabled in config but enabled in metadata" do
+ let(:openapi_all_properties_required) { false }
+ let(:metadata) { super().merge(openapi_all_properties_required: true) }
+
+ it { expect { call }.not_to raise_error }
+ end
+
+ context "with all properties required enabled in config but disabled in metadata" do
+ let(:openapi_all_properties_required) { true }
+ let(:metadata) { super().merge(openapi_all_properties_required: false) }
+
+ it { expect { call }.not_to raise_error }
+ end
+
+ context 'with no additional properties enabled' do
+ let(:openapi_no_additional_properties) { true }
+
+ it { expect { call }.to raise_error /Expected response body/ }
+ end
+
+ context 'with no additional properties disabled' do
+ let(:openapi_no_additional_properties) { false }
+
+ it { expect { call }.not_to raise_error }
+ end
+
+ context "with no additional properties validation disabled in config but enabled in metadata" do
+ let(:openapi_no_additional_properties) { false }
+ let(:metadata) { super().merge(openapi_no_additional_properties: true) }
+
+ it { expect { call }.to raise_error /Expected response body/ }
+ end
+
+ context "with no additional properties validation enabled in config but disabled in metadata" do
+ let(:openapi_no_additional_properties) { true }
+ let(:metadata) { super().merge(openapi_no_additional_properties: false) }
+
+ it { expect { call }.not_to raise_error }
+ end
+
+ context 'when schema does not have required property' do
+ let(:schema) do
+ {
+ type: :object,
+ properties: {
+ text: { type: :string },
+ number: { type: :integer }
+ }
+ }
+ end
+
+ context "with strict schema validation enabled" do
+ let(:openapi_strict_schema_validation) { true }
+
+ it { expect { call }.to raise_error /Expected response body/ }
+ end
+
+ context "with strict schema validation disabled" do
+ let(:openapi_strict_schema_validation) { false }
+
+ it { expect { call }.not_to raise_error }
+ end
+
+ context "with strict schema validation disabled in config but enabled in metadata" do
+ let(:openapi_strict_schema_validation) { false }
+ let(:metadata) { super().merge(openapi_strict_schema_validation: true) }
+
+ include_context 'with strict deprecation warning'
+
+ it { expect { call }.to raise_error /Expected response body/ }
+ end
+
+ context "with strict schema validation enabled in config but disabled in metadata" do
+ let(:openapi_strict_schema_validation) { true }
+ let(:metadata) { super().merge(openapi_strict_schema_validation: false) }
+
+ include_context 'with strict deprecation warning'
+
+ it { expect { call }.not_to raise_error }
+ end
+
+ context 'with all properties required enabled' do
+ let(:openapi_all_properties_required) { true }
+
+ it { expect { call }.not_to raise_error }
+ end
+
+ context 'with all properties required disabled' do
+ let(:openapi_all_properties_required) { false }
+
+ it { expect { call }.not_to raise_error }
+ end
+
+ context "with all properties required disabled in config but enabled in metadata" do
+ let(:openapi_all_properties_required) { false }
+ let(:metadata) { super().merge(openapi_all_properties_required: true) }
+
+ it { expect { call }.not_to raise_error }
+ end
+
+ context "with all properties required enabled in config but disabled in metadata" do
+ let(:openapi_all_properties_required) { true }
+ let(:metadata) { super().merge(openapi_all_properties_required: false) }
+
+ it { expect { call }.not_to raise_error }
+ end
+
+ context 'with no additional properties enabled' do
+ let(:openapi_no_additional_properties) { true }
+
+ it { expect { call }.to raise_error /Expected response body/ }
+ end
+
+ context 'with no additional properties disabled' do
+ let(:openapi_no_additional_properties) { false }
+
+ it { expect { call }.not_to raise_error }
+ end
+
+ context "with no additional properties validation disabled in config but enabled in metadata" do
+ let(:openapi_no_additional_properties) { false }
+ let(:metadata) { super().merge(openapi_no_additional_properties: true) }
+
+ it { expect { call }.to raise_error /Expected response body/ }
+ end
+
+ context "with no additional properties validation enabled in config but disabled in metadata" do
+ let(:openapi_no_additional_properties) { true }
+ let(:metadata) { super().merge(openapi_no_additional_properties: false) }
+
+ it { expect { call }.not_to raise_error }
+ end
+ end
+ end
+
+ context "when response body has missing properties" do
+ before { response.body = '{"number": 3}' }
+
+ context "with strict schema validation enabled" do
+ let(:openapi_strict_schema_validation) { true }
+
+ it { expect { call }.to raise_error /Expected response body/ }
+ end
+
+ context "with strict schema validation disabled" do
+ let(:openapi_strict_schema_validation) { false }
+
+ it { expect { call }.to raise_error /Expected response body/ }
+ end
+
+ context "with strict schema validation disabled in config but enabled in metadata" do
+ let(:openapi_strict_schema_validation) { false }
+ let(:metadata) { super().merge(openapi_strict_schema_validation: true) }
+
+ include_context 'with strict deprecation warning'
+
+ it { expect { call }.to raise_error /Expected response body/ }
+ end
+
+ context "with strict schema validation enabled in config but disabled in metadata" do
+ let(:openapi_strict_schema_validation) { true }
+ let(:metadata) { super().merge(openapi_strict_schema_validation: false) }
+
+ include_context 'with strict deprecation warning'
+
+ it { expect { call }.to raise_error /Expected response body/ }
+ end
+
+ context 'with all properties required enabled' do
+ let(:openapi_all_properties_required) { true }
+
+ it { expect { call }.to raise_error /Expected response body/ }
+ end
+
+ context 'with all properties required disabled' do
+ let(:openapi_all_properties_required) { false }
+
+ it { expect { call }.to raise_error /Expected response body/ }
+ end
+
+ context "with all properties required disabled in config but enabled in metadata" do
+ let(:openapi_all_properties_required) { false }
+ let(:metadata) { super().merge(openapi_all_properties_required: true) }
+
+ it { expect { call }.to raise_error /Expected response body/ }
+ end
+
+ context "with all properties required enabled in config but disabled in metadata" do
+ let(:openapi_all_properties_required) { true }
+ let(:metadata) { super().merge(openapi_all_properties_required: false) }
+
+ it { expect { call }.to raise_error /Expected response body/ }
+ end
+
+ context 'with no additional properties enabled' do
+ let(:openapi_no_additional_properties) { true }
+
+ it { expect { call }.to raise_error /Expected response body/ }
+ end
+
+ context 'with no additional properties disabled' do
+ let(:openapi_no_additional_properties) { false }
+
+ it { expect { call }.to raise_error /Expected response body/ }
+ end
+
+ context "with no additional properties validation disabled in config but enabled in metadata" do
+ let(:openapi_no_additional_properties) { false }
+ let(:metadata) { super().merge(openapi_no_additional_properties: true) }
+
+ it { expect { call }.to raise_error /Expected response body/ }
+ end
+
+ context "with no additional properties validation enabled in config but disabled in metadata" do
+ let(:openapi_no_additional_properties) { true }
+ let(:metadata) { super().merge(openapi_no_additional_properties: false) }
+
+ it { expect { call }.to raise_error /Expected response body/ }
+ end
+ end
+
+ context "when response body has missing properties and additional properties" do
+ before { response.body = '{"foo":"Some comment", "text":"bar"}' }
+
+ context "with strict schema validation enabled" do
+ let(:openapi_strict_schema_validation) { true }
+
+ it { expect { call }.to raise_error /Expected response body/ }
+ end
+
+ context "with strict schema validation disabled" do
+ let(:openapi_strict_schema_validation) { false }
+
+ it { expect { call }.to raise_error /Expected response body/ }
+ end
+
+ context "with strict schema validation disabled in config but enabled in metadata" do
+ let(:openapi_strict_schema_validation) { false }
+ let(:metadata) { super().merge(openapi_strict_schema_validation: true) }
+
+ include_context 'with strict deprecation warning'
+
+ it { expect { call }.to raise_error /Expected response body/ }
+ end
+
+ context "with strict schema validation enabled in config but disabled in metadata" do
+ let(:openapi_strict_schema_validation) { true }
+ let(:metadata) { super().merge(openapi_strict_schema_validation: false) }
+
+ include_context 'with strict deprecation warning'
+
+ it { expect { call }.to raise_error /Expected response body/ }
+ end
+
+ context 'with all properties required enabled' do
+ let(:openapi_all_properties_required) { true }
+
+ it { expect { call }.to raise_error /Expected response body/ }
+ end
+
+ context 'with all properties required disabled' do
+ let(:openapi_all_properties_required) { false }
+
+ it { expect { call }.to raise_error /Expected response body/ }
+ end
+
+ context "with all properties required disabled in config but enabled in metadata" do
+ let(:openapi_all_properties_required) { false }
+ let(:metadata) { super().merge(openapi_all_properties_required: true) }
+
+ it { expect { call }.to raise_error /Expected response body/ }
+ end
+
+ context "with all properties required enabled in config but disabled in metadata" do
+ let(:openapi_all_properties_required) { true }
+ let(:metadata) { super().merge(openapi_all_properties_required: false) }
+
+ it { expect { call }.to raise_error /Expected response body/ }
+ end
+
+ context 'with no additional properties enabled' do
+ let(:openapi_no_additional_properties) { true }
+
+ it { expect { call }.to raise_error /Expected response body/ }
+ end
+
+ context 'with no additional properties disabled' do
+ let(:openapi_no_additional_properties) { false }
+
+ it { expect { call }.to raise_error /Expected response body/ }
+ end
+
+ context "with no additional properties validation disabled in config but enabled in metadata" do
+ let(:openapi_no_additional_properties) { false }
+ let(:metadata) { super().merge(openapi_no_additional_properties: true) }
+
+ it { expect { call }.to raise_error /Expected response body/ }
+ end
+
+ context "with no additional properties validation enabled in config but disabled in metadata" do
+ let(:openapi_no_additional_properties) { true }
+ let(:metadata) { super().merge(openapi_no_additional_properties: false) }
+
+ it { expect { call }.to raise_error /Expected response body/ }
+ end
+
+ context 'when schema does not have required property' do
+ let(:schema) do
+ {
+ type: :object,
+ properties: {
+ text: { type: :string },
+ number: { type: :integer }
+ }
+ }
+ end
+
+ context "with strict schema validation enabled" do
+ let(:openapi_strict_schema_validation) { true }
+
+ it { expect { call }.to raise_error /Expected response body/ }
+ end
+
+ context "with strict schema validation disabled" do
+ let(:openapi_strict_schema_validation) { false }
+
+ it { expect { call }.not_to raise_error }
+ end
+
+ context "with strict schema validation disabled in config but enabled in metadata" do
+ let(:openapi_strict_schema_validation) { false }
+ let(:metadata) { super().merge(openapi_strict_schema_validation: true) }
+
+ include_context 'with strict deprecation warning'
+
+ it { expect { call }.to raise_error /Expected response body/ }
+ end
+
+ context "with strict schema validation enabled in config but disabled in metadata" do
+ let(:openapi_strict_schema_validation) { true }
+ let(:metadata) { super().merge(openapi_strict_schema_validation: false) }
+
+ include_context 'with strict deprecation warning'
+
+ it { expect { call }.not_to raise_error }
+ end
+
+ context 'with all properties required enabled' do
+ let(:openapi_all_properties_required) { true }
+
+ it { expect { call }.to raise_error /Expected response body/ }
+ end
+
+ context 'with all properties required disabled' do
+ let(:openapi_all_properties_required) { false }
+
+ it { expect { call }.not_to raise_error }
+ end
+
+ context "with all properties required disabled in config but enabled in metadata" do
+ let(:openapi_all_properties_required) { false }
+ let(:metadata) { super().merge(openapi_all_properties_required: true) }
+
+ it { expect { call }.to raise_error /Expected response body/ }
+ end
+
+ context "with all properties required enabled in config but disabled in metadata" do
+ let(:openapi_all_properties_required) { true }
+ let(:metadata) { super().merge(openapi_all_properties_required: false) }
+
+ it { expect { call }.not_to raise_error }
+ end
+
+ context 'with no additional properties enabled' do
+ let(:openapi_no_additional_properties) { true }
+
+ it { expect { call }.to raise_error /Expected response body/ }
+ end
+
+ context 'with no additional properties disabled' do
+ let(:openapi_no_additional_properties) { false }
+
+ it { expect { call }.not_to raise_error }
+ end
+
+ context "with no additional properties validation disabled in config but enabled in metadata" do
+ let(:openapi_no_additional_properties) { false }
+ let(:metadata) { super().merge(openapi_no_additional_properties: true) }
+
+ it { expect { call }.to raise_error /Expected response body/ }
+ end
+
+ context "with no additional properties validation enabled in config but disabled in metadata" do
+ let(:openapi_no_additional_properties) { true }
+ let(:metadata) { super().merge(openapi_no_additional_properties: false) }
+
+ it { expect { call }.not_to raise_error }
+ end
+ end
+ end
+
context 'referenced schemas' do
context 'swagger 2.0' do
before do
- swagger_doc[:definitions] = {
+ openapi_spec[:definitions] = {
'blog' => {
type: :object,
properties: { foo: { type: :string } },
@@ -78,9 +778,9 @@ module Specs
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] = {
+ allow(Rswag::Specs.deprecator).to receive(:warn)
+ allow(config).to receive(:get_openapi_spec_version).and_return('3.0.1')
+ openapi_spec[:components] = {
schemas: {
'blog' => {
type: :object,
@@ -95,13 +795,99 @@ module Specs
it 'uses the referenced schema to validate the response body' do
expect { call }.to raise_error(/Expected response body/)
end
+
+ context 'nullable referenced schema' do
+ let(:response) do
+ OpenStruct.new(
+ code: '200',
+ headers: {
+ 'X-Rate-Limit-Limit' => '10',
+ 'X-Cursor' => 'test_cursor',
+ 'X-Per-Page' => 25
+ },
+ body: '{ "blog": null }'
+ )
+ end
+
+ before do
+ metadata[:response][:schema] = {
+ properties: { blog: { '$ref' => '#/components/schema/blog' } },
+ required: ['blog']
+ }
+ end
+
+ context 'using x-nullable attribute' do
+ before do
+ metadata[:response][:schema][:properties][:blog]['x-nullable'] = true
+ end
+
+ context 'response matches metadata' do
+ it { expect { call }.to_not raise_error }
+ end
+ end
+
+ context 'using nullable attribute' do
+ before do
+ metadata[:response][:schema][:properties][:blog]['nullable'] = true
+ end
+
+ context 'response matches metadata' do
+ it { expect { call }.to_not raise_error }
+ end
+ end
+ end
+
+ context 'nullable oneOf with referenced schema' do
+ let(:response) do
+ OpenStruct.new(
+ code: '200',
+ headers: {
+ 'X-Rate-Limit-Limit' => '10',
+ 'X-Cursor' => 'test_cursor',
+ 'X-Per-Page' => 25
+ },
+ body: '{ "blog": null }'
+ )
+ end
+
+ before do
+ metadata[:response][:schema] = {
+ properties: {
+ blog: {
+ oneOf: [{ '$ref' => '#/components/schema/blog' }]
+ }
+ },
+ required: ['blog']
+ }
+ end
+
+ context 'using x-nullable attribute' do
+ before do
+ metadata[:response][:schema][:properties][:blog]['x-nullable'] = true
+ end
+
+ context 'response matches metadata' do
+ it { expect { call }.to_not raise_error }
+ end
+ end
+
+ context 'using nullable attribute' do
+ before do
+ metadata[:response][:schema][:properties][:blog]['nullable'] = true
+ end
+
+ context 'response matches metadata' do
+ it { expect { call }.to_not raise_error }
+ end
+ 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] = {
+ allow(Rswag::Specs.deprecator).to receive(:warn)
+ allow(config).to receive(:get_openapi_spec_version).and_return('3.0.1')
+ openapi_spec[:definitions] = {
'blog' => {
type: :object,
properties: { foo: { type: :string } },
@@ -113,7 +899,7 @@ module Specs
it 'warns the user to upgrade' do
expect { call }.to raise_error(/Expected response body/)
- expect(ActiveSupport::Deprecation).to have_received(:warn)
+ expect(Rswag::Specs.deprecator).to have_received(:warn)
.with('Rswag::Specs: WARNING: definitions is replaced in OpenAPI3! Rename to components/schemas (in swagger_helper.rb)')
end
end
diff --git a/rswag-specs/spec/rswag/specs/swagger_formatter_spec.rb b/rswag-specs/spec/rswag/specs/swagger_formatter_spec.rb
index 64d6a0c0f..421257bf0 100644
--- a/rswag-specs/spec/rswag/specs/swagger_formatter_spec.rb
+++ b/rswag-specs/spec/rswag/specs/swagger_formatter_spec.rb
@@ -10,24 +10,29 @@ module Specs
# Mock out some infrastructure
before do
- allow(config).to receive(:swagger_root).and_return(swagger_root)
+ allow(config).to receive(:openapi_root).and_return(openapi_root)
- allow(ActiveSupport::Deprecation).to receive(:warn) # Silence deprecation output from specs
+ allow(Rswag::Specs.deprecator).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__) }
+ let(:openapi_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)
+ 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: { verb: :post, summary: 'Creates a blog', parameters: [{ type: :string }] },
+ operation: operation,
response: response_metadata,
document: document
}
@@ -35,21 +40,21 @@ module Specs
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(:openapi_spec) { { swagger: '2.0' } }
let(:document) { false }
it 'does not update the swagger doc' do
- expect(swagger_doc).to match({ swagger: '2.0' })
+ expect(openapi_spec).to match({ swagger: '2.0' })
end
end
context 'with the document tag set to anything but false' do
- let(:swagger_doc) { { swagger: '2.0' } }
+ let(:openapi_spec) { { 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(
+ expect(openapi_spec).to match(
swagger: '2.0',
paths: {
'/blogs' => {
@@ -72,7 +77,7 @@ module Specs
end
context 'with metadata upgrades for 3.0' do
- let(:swagger_doc) do
+ let(:openapi_spec) do
{
openapi: '3.0.1',
basePath: '/foo',
@@ -103,7 +108,7 @@ module Specs
let(:document) { nil }
it 'converts query and path params, type: to schema: { type: }' do
- expect(swagger_doc.slice(:paths)).to match(
+ expect(openapi_spec.slice(:paths)).to match(
paths: {
'/blogs' => {
parameters: [{ schema: { type: :string } }],
@@ -142,7 +147,7 @@ module Specs
end
it 'adds example to definition' do
- expect(swagger_doc.slice(:paths)).to match(
+ expect(openapi_spec.slice(:paths)).to match(
paths: {
'/blogs' => {
parameters: [{ schema: { type: :string } }],
@@ -172,7 +177,7 @@ module Specs
end
context 'with empty content' do
- let(:swagger_doc) do
+ let(:openapi_spec) do
{
openapi: '3.0.1',
basePath: '/foo',
@@ -201,7 +206,7 @@ module Specs
end
it 'converts query and path params, type: to schema: { type: }' do
- expect(swagger_doc.slice(:paths)).to match(
+ expect(openapi_spec.slice(:paths)).to match(
paths: {
'/blogs' => {
parameters: [{ schema: { type: :string } }],
@@ -222,7 +227,7 @@ module Specs
end
it 'converts basePath, schemas and host to urls' do
- expect(swagger_doc.slice(:servers)).to match(
+ expect(openapi_spec.slice(:servers)).to match(
servers: {
urls: ['http://api.example.com/foo', 'https://api.example.com/foo']
}
@@ -230,7 +235,7 @@ module Specs
end
it 'upgrades oauth flow to flows' do
- expect(swagger_doc.slice(:components)).to match(
+ expect(openapi_spec.slice(:components)).to match(
components: {
securitySchemes: {
myClientCredentials: {
@@ -266,36 +271,36 @@ module Specs
describe '#stop' do
before do
- FileUtils.rm_r(swagger_root) if File.exist?(swagger_root)
- allow(config).to receive(:swagger_docs).and_return(
+ FileUtils.rm_r(openapi_root) if File.exist?(openapi_root)
+ allow(config).to receive(:openapi_specs).and_return(
'v1/swagger.json' => doc_1,
'v2/swagger.json' => doc_2
)
- allow(config).to receive(:swagger_format).and_return(swagger_format)
+ 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(:swagger_format) { :json }
+ let(:openapi_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
+ it 'writes the openapi_spec(s) to file' do
+ expect(File).to exist("#{openapi_root}/v1/swagger.json")
+ expect(File).to exist("#{openapi_root}/v2/swagger.json")
+ expect { JSON.parse(File.read("#{openapi_root}/v2/swagger.json")) }.not_to raise_error
end
end
context 'with yaml format' do
- let(:swagger_format) { :yaml }
+ let(:openapi_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)
+ it 'writes the openapi_spec(s) as yaml' do
+ expect(File).to exist("#{openapi_root}/v1/swagger.json")
+ expect { JSON.parse(File.read("#{openapi_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
+ expect { YAML.safe_load(File.read("#{openapi_root}/v1/swagger.json")) }.not_to raise_error
end
end
@@ -367,9 +372,158 @@ module Specs
})
end
end
+
+ context 'with enum parameters' do
+ let(:doc_2) do
+ {
+ paths: {
+ '/path/' => {
+ get: {
+ summary: 'Retrieve Nested Paths',
+ tags: ['nested Paths'],
+ produces: ['application/json'],
+ consumes: ['application/json'],
+ parameters: [{
+ in: :query,
+ name: :foo,
+ enum: {
+ 'bar': 'list bars',
+ 'baz': 'lists people named baz'
+ },
+ description: 'get by foo'
+ }]
+ }
+ }
+ }
+ }
+ end
+
+ it 'writes the enum description' do
+ expect(doc_2[:paths]['/path/'][:get][:parameters]).to match(
+ [{
+ in: :query,
+ name: :foo,
+ enum: {
+ bar: "list bars",
+ baz: "lists people named baz"
+ },
+ description: "get by foo:\n * `bar` list bars\n * `baz` lists people named baz\n "
+ }]
+ )
+ 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(swagger_root) if File.exist?(swagger_root)
+ FileUtils.rm_r(openapi_root) if File.exist?(openapi_root)
end
end
end
diff --git a/rswag-specs/spec/rswag/specs_spec.rb b/rswag-specs/spec/rswag/specs_spec.rb
new file mode 100644
index 000000000..a05edbf69
--- /dev/null
+++ b/rswag-specs/spec/rswag/specs_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+RSpec.describe Rswag::Specs do
+ describe 'settings' do
+ let(:rspec_config) do
+ RSpec::Core::Configuration.new.tap do |c|
+ c.add_setting :openapi_root
+ end
+ end
+
+ it 'defines openapi settings' do
+ expect { rspec_config.openapi_root = 'foobar' }.not_to raise_error
+ end
+
+ it 'defines deprecated swagger settings' do
+ allow(Rswag::Specs.deprecator).to receive(:warn)
+ rspec_config.swagger_root = 'foobar'
+ expect(rspec_config.openapi_root).to eq('foobar')
+ expect(Rswag::Specs.deprecator).to(
+ have_received(:warn)
+ .with('swagger_root= is deprecated and will be removed from rswag-specs 3.0 (use openapi_root= instead)',
+ any_args)
+ )
+ end
+ end
+
+ describe '::config' do
+ it 'returns a configuration object' do
+ expect(described_class.config).to be_a Rswag::Specs::Configuration
+ end
+ end
+end
diff --git a/rswag-specs/spec/spec_helper.rb b/rswag-specs/spec/spec_helper.rb
index eba5ebe68..56e62f90d 100644
--- a/rswag-specs/spec/spec_helper.rb
+++ b/rswag-specs/spec/spec_helper.rb
@@ -6,4 +6,15 @@ module VERSION
end
end
+require 'simplecov'
+
+RSpec.configure do |config|
+ SimpleCov.start do
+ enable_coverage :branch
+ primary_coverage :branch
+ filters.clear
+ add_filter %r{^/spec/}
+ end
+end
+
require 'rswag/specs'
diff --git a/rswag-ui/lib/generators/rswag/ui/install/install_generator.rb b/rswag-ui/lib/generators/rswag/ui/install/install_generator.rb
index f6f5b198b..d2bb9fb79 100644
--- a/rswag-ui/lib/generators/rswag/ui/install/install_generator.rb
+++ b/rswag-ui/lib/generators/rswag/ui/install/install_generator.rb
@@ -7,7 +7,7 @@ class InstallGenerator < Rails::Generators::Base
source_root File.expand_path('../templates', __FILE__)
def add_initializer
- template('rswag-ui.rb', 'config/initializers/rswag-ui.rb')
+ template('rswag_ui.rb', 'config/initializers/rswag_ui.rb')
end
def add_routes
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
deleted file mode 100644
index 0b9a4ab17..000000000
--- a/rswag-ui/lib/generators/rswag/ui/install/templates/rswag-ui.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-Rswag::Ui.configure do |c|
-
- # List the Swagger endpoints that you want to be documented through the swagger-ui
- # The first parameter is the path (absolute or relative to the UI host) to the corresponding
- # endpoint and the second is a title that will be displayed in the document selector
- # NOTE: If you're using rspec-api to expose Swagger files (under swagger_root) as JSON or YAML endpoints,
- # then the list below should correspond to the relative paths for those endpoints
-
- c.swagger_endpoint '/api-docs/v1/swagger.yaml', 'API V1 Docs'
-
- # Add Basic Auth in case your API is private
- # c.basic_auth_enabled = true
- # c.basic_auth_credentials 'username', 'password'
-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
new file mode 100644
index 000000000..1d6151b6e
--- /dev/null
+++ b/rswag-ui/lib/generators/rswag/ui/install/templates/rswag_ui.rb
@@ -0,0 +1,16 @@
+Rswag::Ui.configure do |c|
+
+ # List the Swagger endpoints that you want to be documented through the
+ # swagger-ui. The first parameter is the path (absolute or relative to the UI
+ # host) to the corresponding 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 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'
+
+ # Add Basic Auth in case your API is private
+ # c.basic_auth_enabled = true
+ # c.basic_auth_credentials 'username', 'password'
+end
diff --git a/rswag-ui/lib/rswag/ui.rb b/rswag-ui/lib/rswag/ui.rb
index bf5c810a8..e21c7e805 100644
--- a/rswag-ui/lib/rswag/ui.rb
+++ b/rswag-ui/lib/rswag/ui.rb
@@ -10,5 +10,9 @@ def self.configure
def self.config
@config ||= Configuration.new
end
+
+ def self.deprecator
+ @deprecator ||= ActiveSupport::Deprecation.new('3.0', 'rswag-ui')
+ end
end
end
diff --git a/rswag-ui/lib/rswag/ui/configuration.rb b/rswag-ui/lib/rswag/ui/configuration.rb
index ad46a4988..e35517f3c 100644
--- a/rswag-ui/lib/rswag/ui/configuration.rb
+++ b/rswag-ui/lib/rswag/ui/configuration.rb
@@ -12,7 +12,7 @@ class Configuration
def initialize
@template_locations = [
- # preffered override location
+ # preferred override location
"#{Rack::Directory.new('').root}/swagger/index.erb",
# backwards compatible override location
"#{Rack::Directory.new('').root}/app/views/rswag/ui/home/index.html.erb",
@@ -26,6 +26,11 @@ def initialize
end
def swagger_endpoint(url, name)
+ Rswag::Ui.deprecator.warn('Rswag::Ui: WARNING: The method will be renamed to "openapi_endpoint" in v3.0')
+ openapi_endpoint(url, name)
+ end
+
+ def openapi_endpoint(url, name)
@config_object[:urls] ||= []
@config_object[:urls] << { url: url, name: name }
end
diff --git a/rswag-ui/lib/rswag/ui/middleware.rb b/rswag-ui/lib/rswag/ui/middleware.rb
index 038379e87..65cf5b252 100644
--- a/rswag-ui/lib/rswag/ui/middleware.rb
+++ b/rswag-ui/lib/rswag/ui/middleware.rb
@@ -14,7 +14,7 @@ def call(env)
end
if index_path?(env)
- return [ 200, { 'Content-Type' => 'text/html' }, [ render_template ] ]
+ return [ 200, { 'Content-Type' => 'text/html', 'Content-Security-Policy' => csp }, [ render_template ] ]
end
super
@@ -37,7 +37,17 @@ def render_template
end
def template_filename
- @config.template_locations.find { |filename| File.exists?(filename) }
+ @config.template_locations.find { |filename| File.exist?(filename) }
+ end
+
+ def csp
+ <<~POLICY.tr "\n", ' '
+ default-src 'self';
+ img-src 'self' data: https://validator.swagger.io;
+ font-src 'self' https://fonts.gstatic.com;
+ style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
+ script-src 'self' 'unsafe-inline';
+ POLICY
end
end
end
diff --git a/rswag-ui/package-lock.json b/rswag-ui/package-lock.json
index 01f5ca7fb..67e4c5282 100644
--- a/rswag-ui/package-lock.json
+++ b/rswag-ui/package-lock.json
@@ -1,13 +1,20 @@
{
"name": "rswag-ui",
- "version": "1.0.0",
- "lockfileVersion": 1,
+ "version": "1.0.1",
+ "lockfileVersion": 3,
"requires": true,
- "dependencies": {
- "swagger-ui-dist": {
- "version": "3.52.5",
- "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.52.5.tgz",
- "integrity": "sha512-8z18eX8G/jbTXYzyNIaobrnD7PSN7yU/YkSasMmajrXtw0FGS64XjrKn5v37d36qmU3o1xLeuYnktshRr7uIFw=="
+ "packages": {
+ "": {
+ "name": "rswag-ui",
+ "version": "1.0.1",
+ "dependencies": {
+ "swagger-ui-dist": "5.9.4"
+ }
+ },
+ "node_modules/swagger-ui-dist": {
+ "version": "5.9.4",
+ "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.9.4.tgz",
+ "integrity": "sha512-Ppghvj6Q8XxH5xiSrUjEeCUitrasGtz7v9FCUIBR/4t89fACQ4FnUT9D0yfodUYhB+PrCmYmxwe/2jTDLslHDw=="
}
}
}
diff --git a/rswag-ui/package.json b/rswag-ui/package.json
index 47cd28f6b..45694aa23 100644
--- a/rswag-ui/package.json
+++ b/rswag-ui/package.json
@@ -1,8 +1,8 @@
{
"name": "rswag-ui",
- "version": "1.0.0",
+ "version": "1.0.1",
"private": true,
"dependencies": {
- "swagger-ui-dist": "3.52.5"
+ "swagger-ui-dist": "5.9.4"
}
}
diff --git a/rswag-ui/rswag-ui.gemspec b/rswag-ui/rswag-ui.gemspec
index 7dc973442..7b3c561a5 100644
--- a/rswag-ui/rswag-ui.gemspec
+++ b/rswag-ui/rswag-ui.gemspec
@@ -5,7 +5,7 @@ $LOAD_PATH.push File.expand_path('../lib', __FILE__)
# Describe your gem and declare its dependencies:
Gem::Specification.new do |s|
s.name = 'rswag-ui'
- s.version = ENV['TRAVIS_TAG'] || '0.0.0'
+ s.version = ENV['RUBYGEMS_VERSION'] || '0.0.0'
s.authors = ['Richie Morris', 'Greg Myers', 'Jay Danielian']
s.email = ['domaindrivendev@gmail.com']
s.homepage = 'https://github.com/rswag/rswag'
@@ -15,6 +15,8 @@ Gem::Specification.new do |s|
s.files = Dir.glob('{lib,node_modules}/**/*') + ['MIT-LICENSE', 'Rakefile' ]
- s.add_dependency 'actionpack', '>=3.1', '< 7.1'
- s.add_dependency 'railties', '>= 3.1', '< 7.1'
+ s.add_dependency 'actionpack', '>= 5.2', '< 8.1'
+ s.add_dependency 'railties', '>= 5.2', '< 8.1'
+
+ s.add_development_dependency 'simplecov', '=0.21.2'
end
diff --git a/rswag-ui/spec/generators/rswag/ui/install_generator_spec.rb b/rswag-ui/spec/generators/rswag/ui/install_generator_spec.rb
index 2015459e1..85784f372 100644
--- a/rswag-ui/spec/generators/rswag/ui/install_generator_spec.rb
+++ b/rswag-ui/spec/generators/rswag/ui/install_generator_spec.rb
@@ -17,7 +17,7 @@ module Ui
end
it 'installs the Rails initializer' do
- assert_file('config/initializers/rswag-ui.rb')
+ assert_file('config/initializers/rswag_ui.rb')
end
# Don't know how to test this
diff --git a/rswag-ui/spec/spec_helper.rb b/rswag-ui/spec/spec_helper.rb
index 251aa5106..6fc0364a1 100644
--- a/rswag-ui/spec/spec_helper.rb
+++ b/rswag-ui/spec/spec_helper.rb
@@ -1,3 +1,5 @@
+require 'simplecov'
+
# This file was generated by the `rspec --init` command. Conventionally, all
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
# The generated `.rspec` file contains `--require spec_helper` which will cause
@@ -14,6 +16,13 @@
#
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
RSpec.configure do |config|
+ SimpleCov.start do
+ enable_coverage :branch
+ primary_coverage :branch
+ filters.clear
+ add_filter %r{^/spec/}
+ # Note: any file that isn't loaded by require isn't in the report
+ end
# rspec-expectations config goes here. You can use an alternate
# assertion/expectation library such as wrong or the stdlib/minitest
# assertions if you prefer.
diff --git a/rswag/lib/generators/rswag/install/USAGE b/rswag/lib/generators/rswag/install/USAGE
index dfcf6cea1..dbaf32e30 100644
--- a/rswag/lib/generators/rswag/install/USAGE
+++ b/rswag/lib/generators/rswag/install/USAGE
@@ -6,5 +6,5 @@ Example:
This will create:
spec/swagger_helper.rb
- config/initializers/rswag-api.rb
- config/initializers/rswag-ui.rb
+ config/initializers/rswag_api.rb
+ config/initializers/rswag_ui.rb
diff --git a/rswag/rswag.gemspec b/rswag/rswag.gemspec
index 8a69f556a..402007357 100644
--- a/rswag/rswag.gemspec
+++ b/rswag/rswag.gemspec
@@ -5,7 +5,7 @@ $LOAD_PATH.push File.expand_path('../lib', __FILE__)
# Describe your gem and declare its dependencies:
Gem::Specification.new do |s|
s.name = 'rswag'
- s.version = ENV['TRAVIS_TAG'] || '0.0.0'
+ s.version = ENV['RUBYGEMS_VERSION'] || '0.0.0'
s.authors = ['Richie Morris', 'Greg Myers', 'Jay Danielian']
s.email = ['domaindrivendev@gmail.com']
s.homepage = 'https://github.com/rswag/rswag'
@@ -15,7 +15,7 @@ Gem::Specification.new do |s|
s.files = Dir['{lib}/**/*'] + [ 'MIT-LICENSE' ]
- s.add_dependency 'rswag-specs', ENV['TRAVIS_TAG'] || '0.0.0'
- s.add_dependency 'rswag-api', ENV['TRAVIS_TAG'] || '0.0.0'
- s.add_dependency 'rswag-ui', ENV['TRAVIS_TAG'] || '0.0.0'
+ s.add_dependency 'rswag-specs', ENV['RUBYGEMS_VERSION'] || '0.0.0'
+ s.add_dependency 'rswag-api', ENV['RUBYGEMS_VERSION'] || '0.0.0'
+ s.add_dependency 'rswag-ui', ENV['RUBYGEMS_VERSION'] || '0.0.0'
end
diff --git a/rswag/spec/generators/rswag/specs/install_generator_spec.rb b/rswag/spec/generators/rswag/specs/install_generator_spec.rb
index 38cf4f7c1..3a5dfb39f 100644
--- a/rswag/spec/generators/rswag/specs/install_generator_spec.rb
+++ b/rswag/spec/generators/rswag/specs/install_generator_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'generator_spec'
require 'generators/rswag/install/install_generator'
@@ -26,7 +28,7 @@ module Specs
end
it 'installs initializer for rswag-ui' do
- assert_file('config/rswag-ui.rb')
+ assert_file('config/rswag_ui.rb')
end
end
end
diff --git a/test-app/app/controllers/blogs_controller.rb b/test-app/app/controllers/blogs_controller.rb
index 3bf6e5e09..3ba5ac182 100644
--- a/test-app/app/controllers/blogs_controller.rb
+++ b/test-app/app/controllers/blogs_controller.rb
@@ -10,7 +10,6 @@ def create
# POST /blogs/flexible
def flexible_create
-
# contrived example to play around with new anyOf and oneOf
# request body definition for 3.0
blog_params = params.require(:blog).permit(:title, :content, :headline, :text)
@@ -21,7 +20,6 @@ def flexible_create
# POST /blogs/alternate
def alternate_create
-
# contrived example to show different :examples in the requestBody section
@blog = Blog.create(params.require(:blog).permit(:title, :content))
respond_with @blog
@@ -38,6 +36,8 @@ def upload
# GET /blogs
def index
@blogs = Blog.all
+ @blogs = @blogs.where(status: params[:status]) if params[:status].present?
+
respond_with @blogs
end
diff --git a/test-app/app/models/blog.rb b/test-app/app/models/blog.rb
index 9d248d76d..838325dca 100644
--- a/test-app/app/models/blog.rb
+++ b/test-app/app/models/blog.rb
@@ -6,6 +6,14 @@ class Blog < ActiveRecord::Base
alias_attribute :headline, :title
alias_attribute :text, :content
+ # Rails 7.0 introduced new syntax to define enums.
+ # See https://github.com/rails/rails/pull/50987
+ if Gem::Version.new(Rails.version) >= Gem::Version.new("7.0")
+ enum :status, [:draft, :published, :archived]
+ else
+ enum status: [:draft, :published, :archived]
+ end
+
def as_json(_options)
{
id: id,
diff --git a/test-app/config/application.rb b/test-app/config/application.rb
index dfafc7519..2a117d6b9 100644
--- a/test-app/config/application.rb
+++ b/test-app/config/application.rb
@@ -4,14 +4,14 @@
require "active_record/railtie"
require "action_controller/railtie"
require "action_mailer/railtie"
-require "sprockets/railtie"
+# require "sprockets/railtie"
# require "rails/test_unit/railtie"
Bundler.require(*Rails.groups)
module TestApp
class Application < Rails::Application
- config.load_defaults 5.2
+ config.load_defaults Rails::VERSION::STRING.to_f
# Settings in config/environments/* take precedence over those specified here.
# Application configuration should go into files in config/initializers
# -- all .rb files in that directory are automatically loaded.
@@ -55,10 +55,10 @@ class Application < Rails::Application
# config.active_record.whitelist_attributes = true
# Enable the asset pipeline
- config.assets.enabled = true
+ # config.assets.enabled = true
# Version of your assets, change this if you want to expire all your assets
- config.assets.version = '1.0'
+ # config.assets.version = '1.0'
end
end
diff --git a/test-app/config/environments/development.rb b/test-app/config/environments/development.rb
index 9bcfc66f0..721282e7a 100644
--- a/test-app/config/environments/development.rb
+++ b/test-app/config/environments/development.rb
@@ -24,10 +24,10 @@
# Do not compress assets
- config.assets.compress = false
+ # config.assets.compress = false
# Expands the lines which load the assets
- config.assets.debug = true
+ # config.assets.debug = true
config.eager_load = false
end
diff --git a/test-app/config/initializers/rswag-api.rb b/test-app/config/initializers/rswag-api.rb
index 5f3ddc40f..c4462b277 100644
--- a/test-app/config/initializers/rswag-api.rb
+++ b/test-app/config/initializers/rswag-api.rb
@@ -4,9 +4,9 @@
# 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 + '/swagger'
- # Inject a lamda function to alter the returned Swagger prior to serialization
+ # 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
# For example, you could leverage this to dynamically assign the "host" property
#
diff --git a/test-app/config/initializers/rswag-ui.rb b/test-app/config/initializers/rswag-ui.rb
index 084a51236..c4ba9b937 100644
--- a/test-app/config/initializers/rswag-ui.rb
+++ b/test-app/config/initializers/rswag-ui.rb
@@ -3,8 +3,8 @@
# List the Swagger endpoints that you want to be documented through the swagger-ui
# The first parameter is the path (absolute or relative to the UI host) to the corresponding
# JSON endpoint and the second is a title that will be displayed in the document selector
- # NOTE: If you're using rspec-api to expose Swagger files (under swagger_root) as JSON endpoints,
+ # NOTE: If you're using rspec-api to expose Swagger 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/swagger.json', 'API V1 Docs'
end
diff --git a/test-app/db/migrate/20160218212104_create_blogs.rb b/test-app/db/migrate/20160218212104_create_blogs.rb
index 9dd862b96..b682d9e9f 100644
--- a/test-app/db/migrate/20160218212104_create_blogs.rb
+++ b/test-app/db/migrate/20160218212104_create_blogs.rb
@@ -10,6 +10,7 @@ def change
t.string :title
t.text :content
t.string :thumbnail
+ t.string :status
t.timestamps
end
diff --git a/test-app/db/schema.rb b/test-app/db/schema.rb
index 440d91969..d202ba742 100644
--- a/test-app/db/schema.rb
+++ b/test-app/db/schema.rb
@@ -2,11 +2,11 @@
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
#
-# Note that this schema.rb definition is the authoritative source for your
-# database schema. If you need to create the application database on another
-# system, you should be using db:schema:load, not running all the migrations
-# from scratch. The latter is a flawed and unsustainable approach (the more migrations
-# you'll amass, the slower it'll run and the greater likelihood for issues).
+# This file is the source Rails uses to define your schema when running `bin/rails
+# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
+# be faster and is potentially less error prone than running all of your
+# migrations from scratch. Old migrations may fail to apply correctly if those
+# migrations use external dependencies or application code.
#
# It's strongly recommended that you check this file into your version control system.
@@ -16,6 +16,7 @@
t.string "title"
t.text "content"
t.string "thumbnail"
+ t.string "status"
t.datetime "created_at"
t.datetime "updated_at"
end
diff --git a/test-app/spec/integration/auth_tests_spec.rb b/test-app/spec/integration/auth_tests_spec.rb
index 926dcba87..ab70c18a3 100644
--- a/test-app/spec/integration/auth_tests_spec.rb
+++ b/test-app/spec/integration/auth_tests_spec.rb
@@ -2,9 +2,9 @@
require 'swagger_helper'
-RSpec.describe 'Auth Tests API', type: :request, swagger_doc: 'v1/swagger.json' do
+RSpec.describe 'Auth Tests API', type: :request, openapi_spec: 'v1/swagger.json' do
before do
- allow(ActiveSupport::Deprecation).to receive(:warn) # Silence deprecation output from specs
+ allow(Rswag::Specs.deprecator).to receive(:warn) # Silence deprecation output from specs
end
path '/auth-tests/basic' do
@@ -37,7 +37,7 @@
end
response '401', 'Invalid credentials' do
- let(:api_key) { 'barfoo' }
+ let(:api_key) { 'barFoo' }
run_test!
end
end
@@ -57,7 +57,7 @@
response '401', 'Invalid credentials' do
let(:Authorization) { "Basic #{::Base64.strict_encode64('jsmith:jspass')}" }
- let(:api_key) { 'barfoo' }
+ let(:api_key) { 'barFoo' }
run_test!
end
end
diff --git a/test-app/spec/integration/blogs_spec.rb b/test-app/spec/integration/blogs_spec.rb
index bfe67b697..6447d4d18 100644
--- a/test-app/spec/integration/blogs_spec.rb
+++ b/test-app/spec/integration/blogs_spec.rb
@@ -1,10 +1,10 @@
require 'swagger_helper'
-RSpec.describe 'Blogs API', type: :request, swagger_doc: 'v1/swagger.json' do
+RSpec.describe 'Blogs API', type: :request, openapi_spec: 'v1/swagger.json' do
let(:api_key) { 'fake_key' }
before do
- allow(ActiveSupport::Deprecation).to receive(:warn) # Silence deprecation output from specs
+ # allow(Rswag::Specs.deprecator).to receive(:warn) # Silence deprecation output from specs
end
path '/blogs' do
@@ -14,20 +14,24 @@
operationId 'createBlog'
consumes 'application/json'
produces 'application/json'
- parameter name: :blog, in: :body, schema: { '$ref' => '#/definitions/blog' }
+ parameter name: :blog, in: :body, schema: { '$ref' => '#/components/schemas/blog' }
- let(:blog) { { title: 'foo', content: 'bar' } }
+ let(:blog) { { title: 'foo', content: 'bar', status: 'published' } }
response '201', 'blog created' do
# schema '$ref' => '#/definitions/blog'
run_test!
end
- response '422', 'invalid request' do
- schema '$ref' => '#/definitions/errors_object'
+ response "422", "invalid request" do
+ schema "$ref" => "#/components/schemas/errors_object"
- let(:blog) { { title: 'foo' } }
- run_test! do |response|
+ let(:blog) { {title: "foo"} }
+
+ run_test!
+
+ # Example to show custom specification description
+ run_test!("returns a 422 response - with error for missing content") do |response|
expect(response.body).to include("can't be blank")
end
end
@@ -39,11 +43,33 @@
operationId 'searchBlogs'
produces 'application/json'
parameter name: :keywords, in: :query, type: 'string'
+ parameter name: :status, in: :query, getter: :blog_status,
+ enum: { 'draft': 'Retrieves draft blogs', 'published': 'Retrieves published blogs', 'archived': 'Retrieves archived blogs' },
+ description: 'Filter by status'
+
+ before do
+ Blog.create(title: 'foo', content: 'hello world', status: :published)
+ end
let(:keywords) { 'foo bar' }
+ let(:blog_status) { 'published' }
response '200', 'success' do
- schema type: 'array', items: { '$ref' => '#/definitions/blog' }
+ schema type: 'array', items: { '$ref' => '#/components/schemas/blog' }
+
+ run_test! do
+ expect(JSON.parse(response.body).size).to eq(1)
+ end
+ end
+
+ response '200', 'no content' do
+ schema type: 'array', items: { '$ref' => '#/components/schemas/blog' }
+
+ let(:blog_status) { 'invalid' }
+
+ run_test! do
+ expect(JSON.parse(response.body).size).to eq(0)
+ end
end
response '406', 'unsupported accept header' do
@@ -71,7 +97,7 @@
let(:flexible_blog) { { blog: { headline: 'my headline', text: 'my text' } } }
response '201', 'flexible blog created' do
- schema oneOf: [{ '$ref' => '#/definitions/blog' }, { '$ref' => '#/definitions/flexible_blog' }]
+ schema oneOf: [{ '$ref' => '#/components/schemas/blog' }, { '$ref' => '#/components/schemas/flexible_blog' }]
run_test!
end
end
@@ -94,23 +120,75 @@
header 'Last-Modified', type: :string
header 'Cache-Control', type: :string
- schema '$ref' => '#/definitions/blog'
+ schema '$ref' => '#/components/schemas/blog'
+ #Legacy
examples 'application/json' => {
+ id: 1,
+ title: 'Hello legacy world!',
+ content: 'Hello legacy world and hello universe. Thank you all very much!!!',
+ thumbnail: 'legacy-thumbnail.png'
+ }
+
+ example 'application/json', :blog_example_1, {
id: 1,
title: 'Hello world!',
content: 'Hello world and hello universe. Thank you all very much!!!',
thumbnail: 'thumbnail.png'
+ }, "Summary of the example", "A longer description of a fine blog post about a wonderful universe!"
+
+ example 'application/json', :blog_example_2, {
+ id: 1,
+ title: 'Another fine example!',
+ content: 'Oh... what a fine example this is, indeed, a fine example!',
+ thumbnail: 'thumbnail.png'
}
let(:id) { blog.id }
+
run_test!
+
+ context 'when openapi_strict_schema_validation is true' do
+ run_test!(openapi_strict_schema_validation: true)
+ end
+
+ context 'when openapi_all_properties_required is true' do
+ run_test!(openapi_all_properties_required: true)
+ end
+
+ context 'when openapi_no_additional_properties is true' do
+ run_test!(openapi_no_additional_properties: true)
+ end
end
response '404', 'blog not found' do
let(:id) { 'invalid' }
run_test!
end
+
+ response '200', 'blog found - openapi_strict_schema_validation = true', openapi_strict_schema_validation: true do
+ schema '$ref' => '#/components/schemas/blog'
+
+ let(:id) { blog.id }
+
+ run_test!
+ end
+
+ response '200', 'blog found - openapi_all_properties_required = true', openapi_all_properties_required: true do
+ schema '$ref' => '#/components/schemas/blog'
+
+ let(:id) { blog.id }
+
+ run_test!
+ end
+
+ response '200', 'blog found - openapi_no_additional_properties = true', openapi_no_additional_properties: true do
+ schema '$ref' => '#/components/schemas/blog'
+
+ let(:id) { blog.id }
+
+ run_test!
+ end
end
end
@@ -125,7 +203,13 @@
description 'Upload a thumbnail for specific blog by id'
operationId 'uploadThumbnailBlog'
consumes 'multipart/form-data'
- parameter name: :file, :in => :formData, :type => :file, required: true
+ parameter(
+ name: :file,
+ description: "The content of the blog thumbnail",
+ in: :formData,
+ type: :file,
+ required: true
+ )
response '200', 'blog updated' do
let(:file) { Rack::Test::UploadedFile.new(Rails.root.join("spec/fixtures/thumbnail.png")) }
diff --git a/test-app/spec/integration/openapi3_spec.rb b/test-app/spec/integration/openapi3_spec.rb
index d3e5b5f8c..677da483a 100644
--- a/test-app/spec/integration/openapi3_spec.rb
+++ b/test-app/spec/integration/openapi3_spec.rb
@@ -5,19 +5,19 @@
# 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 )
+ openapi_root = File.expand_path('tmp/swagger', __dir__)
+ config = double('config', openapi_root: openapi_root, get_openapi_spec: openapi_spec)
formatter = Rswag::Specs::SwaggerFormatter.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
+ let(:openapi_spec) do
{ # That which would be defined in swagger_helper.rb
openapi: api_openapi,
info: {},
@@ -42,7 +42,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 +55,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" }
@@ -68,18 +68,18 @@
url: "https://{defaultHost}/foo",
variables: {
defaultHost: {
- default: "api.example.com"
+ default: "api.example.com"
}
}
}]}
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: {
defaultHost: {
- default: "api.example.com"
+ default: "api.example.com"
}
}
}])
@@ -102,7 +102,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 +129,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 +159,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 +195,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
@@ -212,138 +212,4 @@
describe 'Components Section'
describe 'Using $ref'
describe 'Grouping Operations with Tags'
-
-
- # path '/blogs' do
- # post 'Creates a blog' do
- # tags 'Blogs'
- # description 'Creates a new blog from provided data'
- # operationId 'createBlog'
- # consumes 'application/json'
- # produces 'application/json'
- # parameter name: :blog, in: :body, schema: { '$ref' => '#/definitions/blog' }
-
- # let(:blog) { { title: 'foo', content: 'bar' } }
-
- # response '201', 'blog created' do
- # # schema '$ref' => '#/definitions/blog'
- # run_test!
- # end
-
- # response '422', 'invalid request' do
- # schema '$ref' => '#/definitions/errors_object'
-
- # let(:blog) { { title: 'foo' } }
- # run_test! do |response|
- # expect(response.body).to include("can't be blank")
- # end
-
- # it 'outputs parameters' do
- # pp swagger_doc
- # params = swagger_doc.dig(:paths, "/blogs", :post, :parameters)
- # expect(params[0][:name]).to eq(:blog)
- # end
- # end
- # end
-
- # get 'Searches blogs' do
- # tags 'Blogs'
- # description 'Searches blogs by keywords'
- # operationId 'searchBlogs'
- # produces 'application/json'
- # parameter name: :keywords, in: :query, type: 'string'
-
- # let(:keywords) { 'foo bar' }
-
- # response '200', 'success' do
- # schema type: 'array', items: { '$ref' => '#/definitions/blog' }
- # end
-
- # response '406', 'unsupported accept header' do
- # let(:'Accept') { 'application/foo' }
- # run_test!
- # end
- # end
- # end
-
- # path '/blogs/flexible' do
- # post 'Creates a blog flexible body' do
- # tags 'Blogs'
- # description 'Creates a flexible blog from provided data'
- # operationId 'createFlexibleBlog'
- # consumes 'application/json'
- # produces 'application/json'
-
- # parameter name: :flexible_blog, in: :body, schema: {
- # oneOf: [
- # { '$ref' => '#/definitions/blog' },
- # { '$ref' => '#/definitions/flexible_blog' }
- # ]
- # }
-
- # let(:flexible_blog) { { blog: { headline: 'my headline', text: 'my text' } } }
-
- # response '201', 'flexible blog created' do
- # schema oneOf: [{ '$ref' => '#/definitions/blog' }, { '$ref' => '#/definitions/flexible_blog' }]
- # run_test!
- # end
- # end
- # end
-
- # path '/blogs/{id}' do
- # parameter name: :id, in: :path, type: :string
-
- # let(:id) { blog.id }
- # let(:blog) { Blog.create(title: 'foo', content: 'bar', thumbnail: 'thumbnail.png') }
-
- # get 'Retrieves a blog' do
- # tags 'Blogs'
- # description 'Retrieves a specific blog by id'
- # operationId 'getBlog'
- # produces 'application/json'
-
- # response '200', 'blog found' do
- # header 'ETag', type: :string
- # header 'Last-Modified', type: :string
- # header 'Cache-Control', type: :string
-
- # schema '$ref' => '#/definitions/blog'
-
- # examples 'application/json' => {
- # id: 1,
- # title: 'Hello world!',
- # content: 'Hello world and hello universe. Thank you all very much!!!',
- # thumbnail: 'thumbnail.png'
- # }
-
- # let(:id) { blog.id }
- # run_test!
- # end
-
- # response '404', 'blog not found' do
- # let(:id) { 'invalid' }
- # run_test!
- # end
- # end
- # end
-
- # path '/blogs/{id}/upload' do
- # parameter name: :id, in: :path, type: :string
-
- # let(:id) { blog.id }
- # let(:blog) { Blog.create(title: 'foo', content: 'bar') }
-
- # put 'Uploads a blog thumbnail' do
- # tags 'Blogs'
- # description 'Upload a thumbnail for specific blog by id'
- # operationId 'uploadThumbnailBlog'
- # consumes 'multipart/form-data'
- # parameter name: :file, :in => :formData, :type => :file, required: true
-
- # response '200', 'blog updated' do
- # let(:file) { Rack::Test::UploadedFile.new(Rails.root.join("spec/fixtures/thumbnail.png")) }
- # run_test!
- # end
- # end
- # end
end
diff --git a/test-app/spec/rails_helper.rb b/test-app/spec/rails_helper.rb
index 01a3ce4eb..71e03c17e 100644
--- a/test-app/spec/rails_helper.rb
+++ b/test-app/spec/rails_helper.rb
@@ -25,19 +25,16 @@
# Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
RSpec.configure do |config|
- # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
- config.fixture_path = "#{::Rails.root}/spec/fixtures"
-
# If you're not using ActiveRecord, or you'd prefer not to run each of your
# examples within a transaction, remove the following line or assign false
# instead of true.
config.use_transactional_fixtures = true
- # RSpec Rails can automatically mix in different behaviours to your tests
+ # RSpec Rails can automatically mix in different behaviors to your tests
# based on their file location, for example enabling you to call `get` and
# `post` in specs under `spec/controllers`.
#
- # You can disable this behaviour by removing the line below, and instead
+ # You can disable this behavior by removing the line below, and instead
# explicitly tag your specs with their type, e.g.:
#
# RSpec.describe UsersController, :type => :controller do
diff --git a/test-app/spec/rake/rswag_specs_swaggerize_spec.rb b/test-app/spec/rake/rswag_specs_swaggerize_spec.rb
index dab7da2ff..e5c390255 100644
--- a/test-app/spec/rake/rswag_specs_swaggerize_spec.rb
+++ b/test-app/spec/rake/rswag_specs_swaggerize_spec.rb
@@ -4,14 +4,15 @@
require 'rake'
RSpec.describe 'rswag:specs:swaggerize' do
- let(:swagger_root) { Rails.root.to_s + '/swagger' }
+ let(:openapi_root) { Rails.root.to_s + '/swagger' }
+
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
Rake::Task['rswag:specs:swaggerize'].invoke
- expect(File).to exist("#{swagger_root}/v1/swagger.json")
+ expect(File).to exist("#{openapi_root}/v1/swagger.json")
end
end
diff --git a/test-app/spec/spec_helper.rb b/test-app/spec/spec_helper.rb
index 3d22c436c..c0dd95e37 100644
--- a/test-app/spec/spec_helper.rb
+++ b/test-app/spec/spec_helper.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require 'simplecov'
+
# This file was generated by the `rails generate rspec:install` command. Conventionally, all
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
# The generated `.rspec` file contains `--require spec_helper` which will cause
@@ -19,6 +21,11 @@
#
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
RSpec.configure do |config|
+ SimpleCov.start 'rails' do
+ enable_coverage :branch
+ primary_coverage :branch
+ end
+
# rspec-expectations config goes here. You can use an alternate
# assertion/expectation library such as wrong or the stdlib/minitest
# assertions if you prefer.
diff --git a/test-app/spec/swagger_helper.rb b/test-app/spec/swagger_helper.rb
index cadb59276..3673bb012 100644
--- a/test-app/spec/swagger_helper.rb
+++ b/test-app/spec/swagger_helper.rb
@@ -6,15 +6,15 @@
# Specify a root folder where Swagger 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'
- config.swagger_dry_run = false
+ config.openapi_root = Rails.root.to_s + '/swagger'
+ config.rswag_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
+ # 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 = {
+ # 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/swagger.json'
+ config.openapi_specs = {
'v1/swagger.json' => {
openapi: '3.0.0',
info: {
@@ -24,49 +24,17 @@
paths: {},
servers: [
{
- url: 'https://{defaultHost}',
+ url: '{protocol}://{defaultHost}',
variables: {
+ protocol: {
+ default: :https
+ },
defaultHost: {
default: 'www.example.com'
}
}
}
],
- definitions: {
- errors_object: {
- type: 'object',
- properties: {
- errors: { '$ref' => '#/definitions/errors_map' }
- }
- },
- errors_map: {
- type: 'object',
- additionalProperties: {
- type: 'array',
- items: { type: 'string' }
- }
- },
- blog: {
- type: 'object',
- properties: {
- id: { type: 'integer' },
- title: { type: 'string' },
- content: { type: 'string', 'x-nullable': true },
- thumbnail: { type: 'string', 'x-nullable': true}
- },
- required: [ 'id', 'title' ]
- },
- flexible_blog: {
- type: 'object',
- properties: {
- id: { type: 'integer' },
- headline: { type: 'string' },
- text: { type: 'string', nullable: true },
- thumbnail: { type: 'string', nullable: true }
- },
- required: ['id', 'headline']
- }
- },
components: {
securitySchemes: {
basic_auth: {
@@ -78,6 +46,41 @@
name: 'api_key',
in: :query
}
+ },
+ schemas: {
+ errors_object: {
+ type: 'object',
+ properties: {
+ errors: { '$ref' => '#/components/schemas/errors_map' }
+ }
+ },
+ errors_map: {
+ type: 'object',
+ additionalProperties: {
+ type: 'array',
+ items: { type: 'string' }
+ }
+ },
+ blog: {
+ type: 'object',
+ properties: {
+ id: { type: 'integer' },
+ title: { type: 'string' },
+ content: { type: 'string', 'x-nullable': true },
+ thumbnail: { type: 'string', 'x-nullable': true}
+ },
+ required: [ 'id', 'title' ]
+ },
+ flexible_blog: {
+ type: 'object',
+ properties: {
+ id: { type: 'integer' },
+ headline: { type: 'string' },
+ text: { type: 'string', nullable: true },
+ thumbnail: { type: 'string', nullable: true }
+ },
+ required: ['id', 'headline']
+ }
}
}
},
@@ -98,41 +101,6 @@
}
}
],
- definitions: {
- errors_object: {
- type: 'object',
- properties: {
- errors: { '$ref' => '#/definitions/errors_map' }
- }
- },
- errors_map: {
- type: 'object',
- additionalProperties: {
- type: 'array',
- items: { type: 'string' }
- }
- },
- blog: {
- type: 'object',
- properties: {
- id: { type: 'integer' },
- title: { type: 'string' },
- content: { type: 'string', 'x-nullable': true },
- thumbnail: { type: 'string', 'x-nullable': true}
- },
- required: [ 'id', 'title' ]
- },
- flexible_blog: {
- type: 'object',
- properties: {
- id: { type: 'integer' },
- headline: { type: 'string' },
- text: { type: 'string', nullable: true },
- thumbnail: { type: 'string', nullable: true }
- },
- required: ['id', 'headline']
- }
- },
components: {
securitySchemes: {
basic_auth: {
@@ -144,6 +112,41 @@
name: 'api_key',
in: :query
}
+ },
+ schemas: {
+ errors_object: {
+ type: 'object',
+ properties: {
+ errors: { '$ref' => '#/components/errors_map' }
+ }
+ },
+ errors_map: {
+ type: 'object',
+ additionalProperties: {
+ type: 'array',
+ items: { type: 'string' }
+ }
+ },
+ blog: {
+ type: 'object',
+ properties: {
+ id: { type: 'integer' },
+ title: { type: 'string' },
+ content: { type: 'string', 'x-nullable': true },
+ thumbnail: { type: 'string', 'x-nullable': true}
+ },
+ required: [ 'id', 'title' ]
+ },
+ flexible_blog: {
+ type: 'object',
+ properties: {
+ id: { type: 'integer' },
+ headline: { type: 'string' },
+ text: { type: 'string', nullable: true },
+ thumbnail: { type: 'string', nullable: true }
+ },
+ required: ['id', 'headline']
+ }
}
}
}
diff --git a/test-app/swagger/v1/swagger.json b/test-app/swagger/v1/swagger.json
index 866f24016..e9125cf89 100644
--- a/test-app/swagger/v1/swagger.json
+++ b/test-app/swagger/v1/swagger.json
@@ -100,7 +100,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/definitions/errors_object"
+ "$ref": "#/components/schemas/errors_object"
}
}
}
@@ -110,7 +110,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/definitions/blog"
+ "$ref": "#/components/schemas/blog"
}
}
}
@@ -130,9 +130,32 @@
"schema": {
"type": "string"
}
+ },
+ {
+ "name": "status",
+ "in": "query",
+ "enum": {
+ "draft": "Retrieves draft blogs",
+ "published": "Retrieves published blogs",
+ "archived": "Retrieves archived blogs"
+ },
+ "description": "Filter by status:\n * `draft` Retrieves draft blogs\n * `published` Retrieves published blogs\n * `archived` Retrieves archived blogs\n "
}
],
"responses": {
+ "200": {
+ "description": "no content",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/blog"
+ }
+ }
+ }
+ }
+ },
"406": {
"description": "unsupported accept header"
}
@@ -158,10 +181,10 @@
"schema": {
"oneOf": [
{
- "$ref": "#/definitions/blog"
+ "$ref": "#/components/schemas/blog"
},
{
- "$ref": "#/definitions/flexible_blog"
+ "$ref": "#/components/schemas/flexible_blog"
}
]
}
@@ -207,7 +230,7 @@
"operationId": "getBlog",
"responses": {
"200": {
- "description": "blog found",
+ "description": "blog found - openapi_no_additional_properties = true",
"headers": {
"ETag": {
"type": "string"
@@ -221,14 +244,36 @@
},
"content": {
"application/json": {
- "example": {
- "id": 1,
- "title": "Hello world!",
- "content": "Hello world and hello universe. Thank you all very much!!!",
- "thumbnail": "thumbnail.png"
+ "examples": {
+ "example_0": {
+ "value": {
+ "id": 1,
+ "title": "Hello legacy world!",
+ "content": "Hello legacy world and hello universe. Thank you all very much!!!",
+ "thumbnail": "legacy-thumbnail.png"
+ }
+ },
+ "blog_example_1": {
+ "value": {
+ "id": 1,
+ "title": "Hello world!",
+ "content": "Hello world and hello universe. Thank you all very much!!!",
+ "thumbnail": "thumbnail.png"
+ },
+ "summary": "Summary of the example",
+ "description": "A longer description of a fine blog post about a wonderful universe!"
+ },
+ "blog_example_2": {
+ "value": {
+ "id": 1,
+ "title": "Another fine example!",
+ "content": "Oh... what a fine example this is, indeed, a fine example!",
+ "thumbnail": "thumbnail.png"
+ }
+ }
},
"schema": {
- "$ref": "#/definitions/blog"
+ "$ref": "#/components/schemas/blog"
}
}
}
@@ -273,86 +318,25 @@
}
}
},
- "required": true
+ "required": true,
+ "description": "The content of the blog thumbnail"
}
}
}
},
"servers": [
{
- "url": "https://{defaultHost}",
+ "url": "{protocol}://{defaultHost}",
"variables": {
+ "protocol": {
+ "default": "https"
+ },
"defaultHost": {
"default": "www.example.com"
}
}
}
],
- "definitions": {
- "errors_object": {
- "type": "object",
- "properties": {
- "errors": {
- "$ref": "#/definitions/errors_map"
- }
- }
- },
- "errors_map": {
- "type": "object",
- "additionalProperties": {
- "type": "array",
- "items": {
- "type": "string"
- }
- }
- },
- "blog": {
- "type": "object",
- "properties": {
- "id": {
- "type": "integer"
- },
- "title": {
- "type": "string"
- },
- "content": {
- "type": "string",
- "x-nullable": true
- },
- "thumbnail": {
- "type": "string",
- "x-nullable": true
- }
- },
- "required": [
- "id",
- "title"
- ]
- },
- "flexible_blog": {
- "type": "object",
- "properties": {
- "id": {
- "type": "integer"
- },
- "headline": {
- "type": "string"
- },
- "text": {
- "type": "string",
- "nullable": true
- },
- "thumbnail": {
- "type": "string",
- "nullable": true
- }
- },
- "required": [
- "id",
- "headline"
- ]
- }
- },
"components": {
"securitySchemes": {
"basic_auth": {
@@ -364,6 +348,71 @@
"name": "api_key",
"in": "query"
}
+ },
+ "schemas": {
+ "errors_object": {
+ "type": "object",
+ "properties": {
+ "errors": {
+ "$ref": "#/components/schemas/errors_map"
+ }
+ }
+ },
+ "errors_map": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "blog": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "title": {
+ "type": "string"
+ },
+ "content": {
+ "type": "string",
+ "x-nullable": true
+ },
+ "thumbnail": {
+ "type": "string",
+ "x-nullable": true
+ }
+ },
+ "required": [
+ "id",
+ "title"
+ ]
+ },
+ "flexible_blog": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "headline": {
+ "type": "string"
+ },
+ "text": {
+ "type": "string",
+ "nullable": true
+ },
+ "thumbnail": {
+ "type": "string",
+ "nullable": true
+ }
+ },
+ "required": [
+ "id",
+ "headline"
+ ]
+ }
}
}
}
\ No newline at end of file
diff --git a/test-app/swagger/v3/openapi.json b/test-app/swagger/v3/openapi.json
index 19983c04e..2a2d866a2 100644
--- a/test-app/swagger/v3/openapi.json
+++ b/test-app/swagger/v3/openapi.json
@@ -79,71 +79,6 @@
}
}
],
- "definitions": {
- "errors_object": {
- "type": "object",
- "properties": {
- "errors": {
- "$ref": "#/definitions/errors_map"
- }
- }
- },
- "errors_map": {
- "type": "object",
- "additionalProperties": {
- "type": "array",
- "items": {
- "type": "string"
- }
- }
- },
- "blog": {
- "type": "object",
- "properties": {
- "id": {
- "type": "integer"
- },
- "title": {
- "type": "string"
- },
- "content": {
- "type": "string",
- "x-nullable": true
- },
- "thumbnail": {
- "type": "string",
- "x-nullable": true
- }
- },
- "required": [
- "id",
- "title"
- ]
- },
- "flexible_blog": {
- "type": "object",
- "properties": {
- "id": {
- "type": "integer"
- },
- "headline": {
- "type": "string"
- },
- "text": {
- "type": "string",
- "nullable": true
- },
- "thumbnail": {
- "type": "string",
- "nullable": true
- }
- },
- "required": [
- "id",
- "headline"
- ]
- }
- },
"components": {
"securitySchemes": {
"basic_auth": {
@@ -155,6 +90,71 @@
"name": "api_key",
"in": "query"
}
+ },
+ "schemas": {
+ "errors_object": {
+ "type": "object",
+ "properties": {
+ "errors": {
+ "$ref": "#/components/errors_map"
+ }
+ }
+ },
+ "errors_map": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "blog": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "title": {
+ "type": "string"
+ },
+ "content": {
+ "type": "string",
+ "x-nullable": true
+ },
+ "thumbnail": {
+ "type": "string",
+ "x-nullable": true
+ }
+ },
+ "required": [
+ "id",
+ "title"
+ ]
+ },
+ "flexible_blog": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "headline": {
+ "type": "string"
+ },
+ "text": {
+ "type": "string",
+ "nullable": true
+ },
+ "thumbnail": {
+ "type": "string",
+ "nullable": true
+ }
+ },
+ "required": [
+ "id",
+ "headline"
+ ]
+ }
}
}
}
\ No newline at end of file
diff --git a/test-app/traverse-secret.yml b/test-app/traverse-secret.yml
new file mode 100644
index 000000000..cba2e2e6c
--- /dev/null
+++ b/test-app/traverse-secret.yml
@@ -0,0 +1,5 @@
+this:
+ should:
+ never:
+ be:
+ seen: true
\ No newline at end of file