diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..3e16eafe --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +version: 2 +updates: +- package-ecosystem: "github-actions" + directory: "/" + schedule: + # Check for updates to GitHub Actions every weekday + interval: "daily" +- package-ecosystem: bundler + directory: "/" + schedule: + interval: daily + time: "11:00" + open-pull-requests-limit: 10 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..93077e3a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,65 @@ +--- + +name: CI + +on: [push, pull_request] + +jobs: + test: + name: "Testing" + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + # Recent Rubies and Rails + - ruby-version: '3.2' + - ruby-version: '3.1' + - ruby-version: '3.0' + - ruby-version: '2.7' + - ruby-version: '2.6' + - ruby-version: '2.6' + - ruby-version: '2.7' + - ruby-version: '2.6' + # Old Rubies and Rails + - ruby-version: '2.5' + bundler: '1' + - ruby-version: '2.4' + bundler: '1' + - ruby-version: '2.4' + bundler: '1' + # Failing with a stack trace in active support + # - ruby-version: '2.4' + # rails-version: '4.1' + # bundler: '1' + + continue-on-error: "${{ endsWith(matrix.ruby-version, 'head') }}" + + env: + CI: "1" + + steps: + - name: "Checkout Code" + uses: "actions/checkout@v4" + timeout-minutes: 5 + with: + fetch-depth: 0 + + # - name: Install required libs + # run: | + # sudo apt-get -yqq install libsqlite3-dev + + - name: "Build Ruby" + uses: ruby/setup-ruby@v1 + with: + ruby-version: "${{ matrix.ruby-version }}" + bundler: "${{ matrix.bundler || 2 }}" + bundler-cache: true + # env: + # RAILS_VERSION: ${{ matrix.rails-version }} + + - name: "Run tests" + run: | + bundle exec rake + # env: + # RAILS_VERSION: ${{ matrix.rails-version }} diff --git a/.gitignore b/.gitignore index 1063635d..e4bf1377 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ example/public/docs *.swp /html/ /.idea +Gemfile.lock diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 65742749..00000000 --- a/.travis.yml +++ /dev/null @@ -1,13 +0,0 @@ -language: ruby -sudo: false -rvm: - - 2.1.8 - - 2.2.4 - - 2.3.0 -gemfile: - - Gemfile -script: - - bundle exec rake -branches: - only: - - master diff --git a/Gemfile b/Gemfile index 04bc0b1a..d65e2a66 100644 --- a/Gemfile +++ b/Gemfile @@ -1,5 +1,3 @@ source 'http://rubygems.org' gemspec - -gem 'inch' diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index 6e8db2a9..00000000 --- a/Gemfile.lock +++ /dev/null @@ -1,160 +0,0 @@ -PATH - remote: . - specs: - rspec_api_documentation (6.1.0) - activesupport (>= 3.0.0) - mustache (~> 1.0, >= 0.99.4) - rspec (~> 3.0) - -GEM - remote: http://rubygems.org/ - specs: - activesupport (4.2.5.1) - i18n (~> 0.7) - json (~> 1.7, >= 1.7.7) - minitest (~> 5.1) - thread_safe (~> 0.3, >= 0.3.4) - tzinfo (~> 1.1) - addressable (2.4.0) - aruba (0.13.0) - childprocess (~> 0.5.6) - contracts (~> 0.9) - cucumber (>= 1.3.19) - ffi (~> 1.9.10) - rspec-expectations (>= 2.99) - thor (~> 0.19) - attr_required (1.0.1) - builder (3.2.2) - capybara (2.6.2) - addressable - mime-types (>= 1.16) - nokogiri (>= 1.3.3) - rack (>= 1.0.0) - rack-test (>= 0.5.4) - xpath (~> 2.0) - childprocess (0.5.9) - ffi (~> 1.0, >= 1.0.11) - coderay (1.1.2) - contracts (0.13.0) - crack (0.4.3) - safe_yaml (~> 1.0.0) - cucumber (2.3.2) - builder (>= 2.1.2) - cucumber-core (~> 1.4.0) - cucumber-wire (~> 0.0.1) - diff-lcs (>= 1.1.3) - gherkin (~> 3.2.0) - multi_json (>= 1.7.5, < 2.0) - multi_test (>= 0.1.2) - cucumber-core (1.4.0) - gherkin (~> 3.2.0) - cucumber-wire (0.0.1) - daemons (1.2.3) - diff-lcs (1.2.5) - eventmachine (1.0.9.1) - fakefs (0.6.0) - faraday (0.9.2) - multipart-post (>= 1.2, < 3) - ffi (1.9.10) - gherkin (3.2.0) - hashdiff (0.2.3) - httpclient (2.7.1) - i18n (0.7.0) - inch (0.8.0) - pry - sparkr (>= 0.2.0) - term-ansicolor - yard (~> 0.9.12) - json (1.8.6) - method_source (0.9.0) - mime-types (3.0) - mime-types-data (~> 3.2015) - mime-types-data (3.2015.1120) - mini_portile2 (2.3.0) - minitest (5.8.4) - multi_json (1.11.2) - multi_test (0.1.2) - multipart-post (2.0.0) - mustache (1.0.5) - nokogiri (1.8.4) - mini_portile2 (~> 2.3.0) - pry (0.11.3) - coderay (~> 1.1.0) - method_source (~> 0.9.0) - rack (1.6.4) - rack-oauth2 (1.2.2) - activesupport (>= 2.3) - attr_required (>= 0.0.5) - httpclient (>= 2.4) - multi_json (>= 1.3.6) - rack (>= 1.1) - rack-protection (1.5.3) - rack - rack-test (0.6.3) - rack (>= 1.0) - rake (10.5.0) - rspec (3.4.0) - rspec-core (~> 3.4.0) - rspec-expectations (~> 3.4.0) - rspec-mocks (~> 3.4.0) - rspec-core (3.4.2) - rspec-support (~> 3.4.0) - rspec-expectations (3.4.0) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.4.0) - rspec-its (1.2.0) - rspec-core (>= 3.0.0) - rspec-expectations (>= 3.0.0) - rspec-mocks (3.4.1) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.4.0) - rspec-support (3.4.1) - safe_yaml (1.0.4) - sinatra (1.4.7) - rack (~> 1.5) - rack-protection (~> 1.4) - tilt (>= 1.3, < 3) - sparkr (0.4.1) - term-ansicolor (1.6.0) - tins (~> 1.0) - thin (1.6.4) - daemons (~> 1.0, >= 1.0.9) - eventmachine (~> 1.0, >= 1.0.4) - rack (~> 1.0) - thor (0.19.1) - thread_safe (0.3.5) - tilt (2.0.2) - tins (1.16.3) - tzinfo (1.2.2) - thread_safe (~> 0.1) - webmock (1.22.6) - addressable (>= 2.3.6) - crack (>= 0.3.2) - hashdiff - xpath (2.0.0) - nokogiri (~> 1.3) - yard (0.9.15) - -PLATFORMS - ruby - -DEPENDENCIES - aruba (~> 0.5) - bundler (~> 1.0) - capybara (~> 2.2) - fakefs (~> 0.4) - faraday (~> 0.9, >= 0.9.0) - inch - nokogiri (~> 1.8, >= 1.8.2) - rack-oauth2 (~> 1.2.2, >= 1.0.7) - rack-test (~> 0.6.2) - rake (~> 10.1) - rspec-its (~> 1.0) - rspec_api_documentation! - sinatra (~> 1.4, >= 1.4.4) - thin (~> 1.6, >= 1.6.3) - webmock (~> 1.7) - yard (>= 0.9.11) - -BUNDLED WITH - 1.16.4 diff --git a/README.md b/README.md index a89d2ecb..3c413732 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,3 @@ -[![Build Status](https://travis-ci.org/zipmark/rspec_api_documentation.svg?branch=master)](https://travis-ci.org/zipmark/rspec_api_documentation) -[![Dependency Status](https://gemnasium.com/badges/github.com/zipmark/rspec_api_documentation.svg)](https://gemnasium.com/github.com/zipmark/rspec_api_documentation) [![Code Climate](https://codeclimate.com/github/zipmark/rspec_api_documentation/badges/gpa.svg)](https://codeclimate.com/github/zipmark/rspec_api_documentation) [![Inline docs](https://inch-ci.org/github/zipmark/rspec_api_documentation.svg?branch=master)](https://inch-ci.org/github/zipmark/rspec_api_documentation) [![Gem Version](https://badge.fury.io/rb/rspec_api_documentation.svg)](https://badge.fury.io/rb/rspec_api_documentation) @@ -185,6 +183,7 @@ RspecApiDocumentation.configure do |config| config.configurations_dir = Rails.root.join("doc", "configurations", "api") # Output folder + # **WARNING*** All contents of the configured directory will be cleared, use a dedicated directory. config.docs_dir = Rails.root.join("doc", "api") # An array of output format(s). @@ -238,6 +237,7 @@ RspecApiDocumentation.configure do |config| config.define_group :public do |config| # By default the group's doc_dir is a subfolder under the parent group, based # on the group's name. + # **WARNING*** All contents of the configured directory will be cleared, use a dedicated directory. config.docs_dir = Rails.root.join("doc", "api", "public") # Change the filter to only include :public examples diff --git a/features/oauth2_mac_client.feature b/features/oauth2_mac_client.feature index 133cf603..dd9cd026 100644 --- a/features/oauth2_mac_client.feature +++ b/features/oauth2_mac_client.feature @@ -2,6 +2,7 @@ Feature: Use OAuth2 MAC client as a test client Background: Given a file named "app_spec.rb" with: """ + require "webmock/rspec" require "rspec_api_documentation" require "rspec_api_documentation/dsl" require "rack/builder" diff --git a/features/readme.md b/features/readme.md index 5e8f4c05..365510ec 100644 --- a/features/readme.md +++ b/features/readme.md @@ -1,6 +1,3 @@ -[![Travis status](https://secure.travis-ci.org/zipmark/rspec_api_documentation.png)](https://secure.travis-ci.org/zipmark/rspec_api_documentation) -[![Gemnasium status](https://gemnasium.com/zipmark/rspec_api_documentation.png)](https://gemnasium.com/zipmark/rspec_api_documentation) - http://github.com/zipmark/rspec_api_documentation # RSpec API Doc Generator diff --git a/lib/rspec_api_documentation.rb b/lib/rspec_api_documentation.rb index 5986aadb..69cdb52c 100644 --- a/lib/rspec_api_documentation.rb +++ b/lib/rspec_api_documentation.rb @@ -1,7 +1,9 @@ require 'active_support' require 'active_support/inflector' +require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/hash/conversions' require 'active_support/core_ext/hash/deep_merge' +require 'active_support/core_ext/hash/keys' require 'cgi' require 'json' @@ -52,25 +54,27 @@ module Writers module OpenApi extend ActiveSupport::Autoload + autoload :Components + autoload :Contact + autoload :ExternalDocs + autoload :Flow + autoload :Header autoload :Helper - autoload :Node - autoload :Root autoload :Info - autoload :Contact autoload :License - autoload :Paths - autoload :Path - autoload :Tag + autoload :Media + autoload :Node autoload :Operation autoload :Parameter - autoload :Responses + autoload :Path + autoload :RequestBody autoload :Response - autoload :Example - autoload :Headers - autoload :Header + autoload :Root autoload :Schema - autoload :SecurityDefinitions - autoload :SecuritySchema + autoload :SecurityScheme + autoload :Server + autoload :Tag + autoload :Variable end module Views diff --git a/lib/rspec_api_documentation/dsl/resource.rb b/lib/rspec_api_documentation/dsl/resource.rb index f03a610b..150895d3 100644 --- a/lib/rspec_api_documentation/dsl/resource.rb +++ b/lib/rspec_api_documentation/dsl/resource.rb @@ -30,7 +30,7 @@ def self.define_action(method) def callback(*args, &block) begin - require 'webmock' + require 'webmock/rspec' rescue LoadError raise "Callbacks require webmock to be installed" end diff --git a/lib/rspec_api_documentation/oauth2_mac_client.rb b/lib/rspec_api_documentation/oauth2_mac_client.rb index e5ebcb26..596171a9 100644 --- a/lib/rspec_api_documentation/oauth2_mac_client.rb +++ b/lib/rspec_api_documentation/oauth2_mac_client.rb @@ -4,7 +4,7 @@ # ActiveSupport::SecureRandom not provided in activesupport >= 3.2 end begin - require "webmock" + require "webmock/rspec" rescue LoadError raise "Webmock needs to be installed before using the OAuth2MACClient" end diff --git a/lib/rspec_api_documentation/open_api/components.rb b/lib/rspec_api_documentation/open_api/components.rb new file mode 100644 index 00000000..104fa52f --- /dev/null +++ b/lib/rspec_api_documentation/open_api/components.rb @@ -0,0 +1,13 @@ +module RspecApiDocumentation + module OpenApi + class Components < Node + add_setting :schemas, :schema => { '' => Schema } + add_setting :responses, :schema => { '' => Response } + add_setting :parameters, :schema => { '' => Parameter } + add_setting :headers, :schema => { '' => Header } + add_setting :securitySchemes, :schema => { '' => SecurityScheme } + add_setting :links + add_setting :callbacks + end + end +end diff --git a/lib/rspec_api_documentation/open_api/example.rb b/lib/rspec_api_documentation/open_api/example.rb deleted file mode 100644 index c641b191..00000000 --- a/lib/rspec_api_documentation/open_api/example.rb +++ /dev/null @@ -1,7 +0,0 @@ -module RspecApiDocumentation - module OpenApi - class Example < Node - CHILD_CLASS = true - end - end -end diff --git a/lib/rspec_api_documentation/open_api/external_docs.rb b/lib/rspec_api_documentation/open_api/external_docs.rb new file mode 100644 index 00000000..e13a8285 --- /dev/null +++ b/lib/rspec_api_documentation/open_api/external_docs.rb @@ -0,0 +1,8 @@ +module RspecApiDocumentation + module OpenApi + class ExternalDocs < Node + add_setting :description + add_setting :url, :required => true + end + end +end diff --git a/lib/rspec_api_documentation/open_api/flow.rb b/lib/rspec_api_documentation/open_api/flow.rb new file mode 100644 index 00000000..83c2b055 --- /dev/null +++ b/lib/rspec_api_documentation/open_api/flow.rb @@ -0,0 +1,10 @@ +module RspecApiDocumentation + module OpenApi + class Flow < Node + add_setting :authorizationUrl, :required => true + add_setting :tokenUrl, :required => true + add_setting :refreshUrl + add_setting :scopes, :required => true + end + end +end diff --git a/lib/rspec_api_documentation/open_api/header.rb b/lib/rspec_api_documentation/open_api/header.rb index 7bf25883..e61cb9c4 100644 --- a/lib/rspec_api_documentation/open_api/header.rb +++ b/lib/rspec_api_documentation/open_api/header.rb @@ -2,11 +2,16 @@ module RspecApiDocumentation module OpenApi class Header < Node add_setting :description - add_setting :type, :required => true, :default => lambda { |header| - Helper.extract_type(header.public_send('x-example-value')) - } - add_setting :format - add_setting 'x-example-value' + add_setting :required + add_setting :deprecated + # add_setting :allowEmptyValue + # add_setting :style + # add_setting :explode + # add_setting :allowReserved + add_setting :schema, :schema => Schema + add_setting :example + # add_setting :examples, :schema => { '' => Example } + # add_setting :content, :schema => { '' => Media } end end end diff --git a/lib/rspec_api_documentation/open_api/headers.rb b/lib/rspec_api_documentation/open_api/headers.rb deleted file mode 100644 index 6a073a14..00000000 --- a/lib/rspec_api_documentation/open_api/headers.rb +++ /dev/null @@ -1,7 +0,0 @@ -module RspecApiDocumentation - module OpenApi - class Headers < Node - CHILD_CLASS = Header - end - end -end diff --git a/lib/rspec_api_documentation/open_api/helper.rb b/lib/rspec_api_documentation/open_api/helper.rb index 0e25ad65..83a06a90 100644 --- a/lib/rspec_api_documentation/open_api/helper.rb +++ b/lib/rspec_api_documentation/open_api/helper.rb @@ -16,7 +16,7 @@ def extract_type(value) end def extract_items(value, opts = {}) - result = {type: extract_type(value)} + result = { type: extract_type(value) } if result[:type] == :array result[:items] = extract_items(value[0], opts) else diff --git a/lib/rspec_api_documentation/open_api/media.rb b/lib/rspec_api_documentation/open_api/media.rb new file mode 100644 index 00000000..6b7c5feb --- /dev/null +++ b/lib/rspec_api_documentation/open_api/media.rb @@ -0,0 +1,10 @@ +module RspecApiDocumentation + module OpenApi + class Media < Node + add_setting :schema, :schema => Schema + add_setting :example + # add_setting :examples, :schema => { '' => Example } + # add_setting :encoding, :schema => { '' => Encoding } + end + end +end diff --git a/lib/rspec_api_documentation/open_api/node.rb b/lib/rspec_api_documentation/open_api/node.rb index 2f102c88..920b7950 100644 --- a/lib/rspec_api_documentation/open_api/node.rb +++ b/lib/rspec_api_documentation/open_api/node.rb @@ -1,12 +1,6 @@ module RspecApiDocumentation module OpenApi class Node - # this is used to define class of incoming option attribute - # If +false+ then do not create new setting - # If +true+ then create new setting with raw passed value - # If RspecApiDocumentation::OpenApi::Node then create new setting and wrap it in this class - CHILD_CLASS = false - # This attribute allow us to hide some of children through configuration file attr_accessor :hide @@ -36,14 +30,15 @@ def initialize(opts = {}) opts.each do |name, value| if name.to_s == 'hide' self.hide = value - elsif self.class::CHILD_CLASS - add_setting name, :value => self.class::CHILD_CLASS === true ? value : self.class::CHILD_CLASS.new(value) elsif setting_exist?(name.to_sym) schema = setting_schema(name) converted = - case - when schema.is_a?(Array) && schema[0] <= Node then value.map { |v| v.is_a?(schema[0]) ? v : schema[0].new(v) } - when schema <= Node then value.is_a?(schema) ? value : schema.new(value) + if schema.is_a?(Hash) && schema.values[0] <= Node + Hash[value.map { |k, v| [k, v.is_a?(schema.values[0]) ? v : schema.values[0].new(v)] }] + elsif schema.is_a?(Array) && schema[0] <= Node + value.map { |v| v.is_a?(schema[0]) ? v : schema[0].new(v) } + elsif schema <= Node + value.is_a?(schema) ? value : schema.new(value) else value end @@ -69,7 +64,13 @@ def add_setting(name, opts = {}) settings[name] = opts[:value] if opts[:value] define_singleton_method("#{name}_schema") { opts[:schema] || NilClass } - define_singleton_method("#{name}=") { |value| settings[name] = value } + define_singleton_method("#{name}=") do |value| + if setting[name].is_a?(Hash) && value.is_a?(Hash) + value.each { |k, v| setting[name][k] = setting[name][k] ? setting[name][k].merge(v) : v } + else + settings[name] = value + end + end define_singleton_method("#{name}") do if settings.has_key?(name) settings[name] @@ -94,6 +95,8 @@ def as_json when value.is_a?(Array) && value[0].is_a?(Node) tmp = value.select { |v| !v.hide }.map { |v| v.as_json } hash[name] = tmp unless tmp.empty? + when value.is_a?(Hash) && value.values[0].is_a?(Node) + hash[name] = Hash[value.select { |k, v| !v.hide }.map { |k, v| [k, v.as_json] }] else hash[name] = value end unless value.nil? diff --git a/lib/rspec_api_documentation/open_api/operation.rb b/lib/rspec_api_documentation/open_api/operation.rb index deb0c797..b7713e0f 100644 --- a/lib/rspec_api_documentation/open_api/operation.rb +++ b/lib/rspec_api_documentation/open_api/operation.rb @@ -4,15 +4,14 @@ class Operation < Node add_setting :tags, :default => [] add_setting :summary add_setting :description - add_setting :externalDocs - add_setting :operationId - add_setting :consumes - add_setting :produces + add_setting :externalDocs, :schema => ExternalDocs + # add_setting :operationId add_setting :parameters, :default => [], :schema => [Parameter] - add_setting :responses, :required => true, :schema => Responses - add_setting :schemes + add_setting :requestBody, :schema => RequestBody + add_setting :responses, :required => true, :schema => { '' => Response } add_setting :deprecated, :default => false add_setting :security + # add_setting :servers, :schema => [Server] end end end diff --git a/lib/rspec_api_documentation/open_api/parameter.rb b/lib/rspec_api_documentation/open_api/parameter.rb index af93fb2c..e8c1c370 100644 --- a/lib/rspec_api_documentation/open_api/parameter.rb +++ b/lib/rspec_api_documentation/open_api/parameter.rb @@ -1,24 +1,19 @@ module RspecApiDocumentation module OpenApi class Parameter < Node - # Required to write example values to description of parameter when option `with_example: true` is provided - attr_accessor :value - attr_accessor :with_example - add_setting :name, :required => true add_setting :in, :required => true add_setting :description - add_setting :required, :default => lambda { |parameter| parameter.in.to_s == 'path' ? true : false } - add_setting :schema - add_setting :type - add_setting :items - add_setting :default - add_setting :minimum - add_setting :maximum - add_setting :enum - add_setting :example, :default => lambda { |parameter| parameter.with_example ? parameter.value : nil } - - alias_method :description_without_example, :description + add_setting :required, :default => lambda { |parameter| parameter.in.to_s == 'path' } + add_setting :deprecated + # add_setting :allowEmptyValue + # add_setting :style + # add_setting :explode + # add_setting :allowReserved + add_setting :schema, :schema => Schema + add_setting :example + # add_setting :examples, :schema => { '' => Example } + # add_setting :content, :schema => { '' => Media } end end end diff --git a/lib/rspec_api_documentation/open_api/path.rb b/lib/rspec_api_documentation/open_api/path.rb index 241bba8c..158ec616 100644 --- a/lib/rspec_api_documentation/open_api/path.rb +++ b/lib/rspec_api_documentation/open_api/path.rb @@ -8,6 +8,7 @@ class Path < Node add_setting :options, :schema => Operation add_setting :head, :schema => Operation add_setting :patch, :schema => Operation + add_setting :trace, :schema => Operation end end end diff --git a/lib/rspec_api_documentation/open_api/paths.rb b/lib/rspec_api_documentation/open_api/paths.rb deleted file mode 100644 index b3a9efb1..00000000 --- a/lib/rspec_api_documentation/open_api/paths.rb +++ /dev/null @@ -1,7 +0,0 @@ -module RspecApiDocumentation - module OpenApi - class Paths < Node - CHILD_CLASS = Path - end - end -end diff --git a/lib/rspec_api_documentation/open_api/request_body.rb b/lib/rspec_api_documentation/open_api/request_body.rb new file mode 100644 index 00000000..00fc63c0 --- /dev/null +++ b/lib/rspec_api_documentation/open_api/request_body.rb @@ -0,0 +1,9 @@ +module RspecApiDocumentation + module OpenApi + class RequestBody < Node + add_setting :description, :default => 'body' + add_setting :content, :required => true, :schema => { '' => Media } + add_setting :required, :default => false + end + end +end diff --git a/lib/rspec_api_documentation/open_api/response.rb b/lib/rspec_api_documentation/open_api/response.rb index 6584db6f..04887a46 100644 --- a/lib/rspec_api_documentation/open_api/response.rb +++ b/lib/rspec_api_documentation/open_api/response.rb @@ -2,9 +2,9 @@ module RspecApiDocumentation module OpenApi class Response < Node add_setting :description, :required => true, :default => 'Successful operation' - add_setting :schema, :schema => Schema - add_setting :headers, :schema => Headers - add_setting :examples, :schema => Example + add_setting :headers, :schema => { '' => Header } + add_setting :content, :schema => { '' => Media } + # add_setting :links end end end diff --git a/lib/rspec_api_documentation/open_api/responses.rb b/lib/rspec_api_documentation/open_api/responses.rb deleted file mode 100644 index 4b8c7025..00000000 --- a/lib/rspec_api_documentation/open_api/responses.rb +++ /dev/null @@ -1,9 +0,0 @@ -module RspecApiDocumentation - module OpenApi - class Responses < Node - CHILD_CLASS = Response - - add_setting :default, :default => lambda { |responses| responses.existing_settings.size > 1 ? nil : Response.new } - end - end -end diff --git a/lib/rspec_api_documentation/open_api/root.rb b/lib/rspec_api_documentation/open_api/root.rb index edaeae96..b02f61e0 100644 --- a/lib/rspec_api_documentation/open_api/root.rb +++ b/lib/rspec_api_documentation/open_api/root.rb @@ -1,21 +1,14 @@ module RspecApiDocumentation module OpenApi class Root < Node - add_setting :swagger, :default => '2.0', :required => true + add_setting :openapi, :default => '3.0.0', :required => true add_setting :info, :default => Info.new, :required => true, :schema => Info - add_setting :host, :default => 'localhost:3000' - add_setting :basePath - add_setting :schemes, :default => %w(http https) - add_setting :consumes, :default => %w(application/json application/xml) - add_setting :produces, :default => %w(application/json application/xml) - add_setting :paths, :default => Paths.new, :required => true, :schema => Paths - add_setting :definitions - add_setting :parameters - add_setting :responses - add_setting :securityDefinitions, :schema => SecurityDefinitions + add_setting :servers, :schema => [Server] + add_setting :paths, :default => { '/' => Path.new }, :required => true, :schema => { '' => Path } + add_setting :components, :schema => Components add_setting :security - add_setting :tags, :default => [], :schema => [Tag] - add_setting :externalDocs + add_setting :tags, :schema => [Tag] + add_setting :externalDocs, :schema => ExternalDocs end end end diff --git a/lib/rspec_api_documentation/open_api/schema.rb b/lib/rspec_api_documentation/open_api/schema.rb index 5dd11a31..7f3d186c 100644 --- a/lib/rspec_api_documentation/open_api/schema.rb +++ b/lib/rspec_api_documentation/open_api/schema.rb @@ -1,15 +1,36 @@ module RspecApiDocumentation module OpenApi class Schema < Node - add_setting :format add_setting :title - add_setting :description + add_setting :multipleOf + add_setting :maximum + add_setting :exclusiveMaximum + add_setting :minimum + add_setting :exclusiveMinimum + add_setting :maxLength + add_setting :minLength + add_setting :pattern + add_setting :maxItems + add_setting :minItems + add_setting :uniqueItems + add_setting :maxProperties + add_setting :minProperties add_setting :required add_setting :enum add_setting :type + add_setting :allOf, :schema => [Schema] + add_setting :oneOf, :schema => [Schema] + add_setting :anyOf, :schema => [Schema] + add_setting :not, :schema => [Schema] add_setting :items - add_setting :properties + add_setting :properties, :schema => { '' => Schema } + add_setting :description + add_setting :format add_setting :example + add_setting :externalDocs, :schema => ExternalDocs + add_setting :nullable + add_setting :deprecated + add_setting :discriminator end end end diff --git a/lib/rspec_api_documentation/open_api/security_definitions.rb b/lib/rspec_api_documentation/open_api/security_definitions.rb deleted file mode 100644 index e1ddc136..00000000 --- a/lib/rspec_api_documentation/open_api/security_definitions.rb +++ /dev/null @@ -1,7 +0,0 @@ -module RspecApiDocumentation - module OpenApi - class SecurityDefinitions < Node - CHILD_CLASS = SecuritySchema - end - end -end diff --git a/lib/rspec_api_documentation/open_api/security_schema.rb b/lib/rspec_api_documentation/open_api/security_scheme.rb similarity index 50% rename from lib/rspec_api_documentation/open_api/security_schema.rb rename to lib/rspec_api_documentation/open_api/security_scheme.rb index 25218498..aae46019 100644 --- a/lib/rspec_api_documentation/open_api/security_schema.rb +++ b/lib/rspec_api_documentation/open_api/security_scheme.rb @@ -1,14 +1,14 @@ module RspecApiDocumentation module OpenApi - class SecuritySchema < Node + class SecurityScheme < Node add_setting :type, :required => true add_setting :description add_setting :name add_setting :in - add_setting :flow - add_setting :authorizationUrl - add_setting :tokenUrl - add_setting :scopes + add_setting :scheme + add_setting :bearerFormat + add_setting :flows, :schema => { '' => Flow } + add_setting :openIdConnectUrl end end end diff --git a/lib/rspec_api_documentation/open_api/server.rb b/lib/rspec_api_documentation/open_api/server.rb new file mode 100644 index 00000000..11ffe8a7 --- /dev/null +++ b/lib/rspec_api_documentation/open_api/server.rb @@ -0,0 +1,9 @@ +module RspecApiDocumentation + module OpenApi + class Server < Node + add_setting :url, :required => true + add_setting :description + add_setting :variables, :schema => { '' => Variable } + end + end +end diff --git a/lib/rspec_api_documentation/open_api/tag.rb b/lib/rspec_api_documentation/open_api/tag.rb index 4d70fca8..5a1e8422 100644 --- a/lib/rspec_api_documentation/open_api/tag.rb +++ b/lib/rspec_api_documentation/open_api/tag.rb @@ -3,7 +3,7 @@ module OpenApi class Tag < Node add_setting :name, :required => true add_setting :description - add_setting :externalDocs + # add_setting :externalDocs, :schema => ExternalDocs end end end diff --git a/lib/rspec_api_documentation/open_api/variable.rb b/lib/rspec_api_documentation/open_api/variable.rb new file mode 100644 index 00000000..7f7947bf --- /dev/null +++ b/lib/rspec_api_documentation/open_api/variable.rb @@ -0,0 +1,9 @@ +module RspecApiDocumentation + module OpenApi + class Variable < Node + add_setting :enum + add_setting :default, :required => true + add_setting :description + end + end +end diff --git a/lib/rspec_api_documentation/views/markup_example.rb b/lib/rspec_api_documentation/views/markup_example.rb index 682075bf..181ab028 100644 --- a/lib/rspec_api_documentation/views/markup_example.rb +++ b/lib/rspec_api_documentation/views/markup_example.rb @@ -49,7 +49,9 @@ def response_fields def requests super.map do |hash| hash[:request_headers_text] = format_hash(hash[:request_headers]) - hash[:request_query_parameters_text] = format_hash(hash[:request_query_parameters]) + hash[:request_query_parameters_text] = if hash[:request_query_parameters].present? + CGI.unescape(hash[:request_query_parameters].to_query) + end hash[:response_headers_text] = format_hash(hash[:response_headers]) if @host if hash[:curl].is_a? RspecApiDocumentation::Curl diff --git a/lib/rspec_api_documentation/writers/open_api_writer.rb b/lib/rspec_api_documentation/writers/open_api_writer.rb index ed5d0420..9c2ade77 100644 --- a/lib/rspec_api_documentation/writers/open_api_writer.rb +++ b/lib/rspec_api_documentation/writers/open_api_writer.rb @@ -33,9 +33,10 @@ def initialize(index, configuration, init_config) def as_json @specs = OpenApi::Root.new(init_config) + add_components! add_tags! add_paths! - add_security_definitions! + add_security_schemes! specs.as_json end @@ -47,8 +48,12 @@ def examples index.examples.map { |example| OpenApiExample.new(example) } end - def add_security_definitions! - security_definitions = OpenApi::SecurityDefinitions.new + def add_components! + specs.safe_assign_setting(:components, OpenApi::Components.new) + end + + def add_security_schemes! + security_schemes = {} arr = examples.map do |example| example.respond_to?(:authentications) ? example.authentications : nil @@ -56,16 +61,16 @@ def add_security_definitions! arr.each do |securities| securities.each do |security, opts| - schema = OpenApi::SecuritySchema.new( + schema = OpenApi::SecurityScheme.new( name: opts[:name], description: opts[:description], type: opts[:type], in: opts[:in] ) - security_definitions.add_setting security, :value => schema + security_schemes[security.to_s] = schema end end - specs.securityDefinitions = security_definitions unless arr.empty? + specs.components.safe_assign_setting(:securitySchemes, security_schemes) unless arr.empty? end def add_tags! @@ -80,89 +85,140 @@ def add_tags! end def add_paths! - specs.safe_assign_setting(:paths, OpenApi::Paths.new) + specs.safe_assign_setting(:paths, {}) examples.each do |example| - specs.paths.add_setting example.route, :value => OpenApi::Path.new + route = example.route.to_s + specs.paths[route] ||= OpenApi::Path.new - operation = specs.paths.setting(example.route).setting(example.http_method) || OpenApi::Operation.new + operation = specs.paths[route].setting(example.http_method) || OpenApi::Operation.new operation.safe_assign_setting(:tags, [example.resource_name]) - operation.safe_assign_setting(:summary, example.respond_to?(:route_summary) ? example.route_summary : '') - operation.safe_assign_setting(:description, example.respond_to?(:route_description) ? example.route_description : '') - operation.safe_assign_setting(:responses, OpenApi::Responses.new) + operation.safe_assign_setting(:summary, example.route_summary) if example.respond_to?(:route_summary) + operation.safe_assign_setting(:description, example.route_description) if example.respond_to?(:route_description) + operation.safe_assign_setting(:externalDocs, OpenApi::ExternalDocs.new(url: example.external_docs)) if example.respond_to?(:external_docs) + operation.safe_assign_setting(:responses, {}) + operation.safe_assign_setting(:deprecated, example.deprecated) if example.respond_to?(:deprecated) operation.safe_assign_setting(:parameters, extract_parameters(example)) - operation.safe_assign_setting(:consumes, example.requests.map { |request| request[:request_content_type] }.compact.map { |q| q[/[^;]+/] }) - operation.safe_assign_setting(:produces, example.requests.map { |request| request[:response_content_type] }.compact.map { |q| q[/[^;]+/] }) + operation.safe_assign_setting(:requestBody, extract_request_body(example)) operation.safe_assign_setting(:security, example.respond_to?(:authentications) ? example.authentications.map { |(k, _)| {k => []} } : []) process_responses(operation.responses, example) - specs.paths.setting(example.route).assign_setting(example.http_method, operation) + specs.paths[route].assign_setting(example.http_method, operation) end end def process_responses(responses, example) - schema = extract_schema(example.respond_to?(:response_fields) ? example.response_fields : []) + schema = extract_schema(example.respond_to?(:response_fields) ? example.response_fields : nil) example.requests.each do |request| response = OpenApi::Response.new( - description: example.description, - schema: schema + description: example.description ) if request[:response_headers] - response.safe_assign_setting(:headers, OpenApi::Headers.new) + response.safe_assign_setting(:headers, {}) request[:response_headers].each do |header, value| - response.headers.add_setting header, :value => OpenApi::Header.new('x-example-value' => value) + response.headers[header.to_s] = OpenApi::Header.new(schema: OpenApi::Schema.new(type: 'string'), example: value) end end + response_body = JSON.parse(request[:response_body]) rescue nil if /\A(?[^;]+)/ =~ request[:response_content_type] - response.safe_assign_setting(:examples, OpenApi::Example.new) - response_body = JSON.parse(request[:response_body]) rescue nil - response.examples.add_setting response_content_type, :value => response_body + content_type = response_content_type.to_s + else + content_type = 'application/json' + end + + if response_body + response.safe_assign_setting(:content, { + content_type => OpenApi::Media.new(schema: schema || get_schema(response_body), example: response_body) + }) end - responses.add_setting "#{request[:response_status]}", :value => response + + responses[request[:response_status].to_s] = response end end def extract_schema(fields) - schema = {type: 'object', properties: {}} + return nil if fields.nil? + + schema = { type: 'object', properties: {} } fields.each do |field| current = schema if field[:scope] [*field[:scope]].each do |scope| - current[:properties][scope] ||= {type: 'object', properties: {}} + current[:properties][scope] ||= { type: 'object', properties: {} } current = current[:properties][scope] end end - current[:properties][field[:name]] = {type: field[:type] || OpenApi::Helper.extract_type(field[:value])} + current[:properties][field[:name]] = { type: field[:type] || OpenApi::Helper.extract_type(field[:value]) } current[:properties][field[:name]][:example] = field[:value] if field[:value] && field[:with_example] - current[:properties][field[:name]][:default] = field[:default] if field[:default] - current[:properties][field[:name]][:description] = field[:description] if field[:description] - opts = {enum: field[:enum], minimum: field[:minimum], maximum: field[:maximum]} + opts = { + description: field[:description], + default: field[:default], + enum: field[:enum], + minimum: field[:minimum], + maximum: field[:maximum], + required: field[:required], + nullable: field[:nullable] || field[:value].nil? || nil + } if current[:properties][field[:name]][:type] == :array current[:properties][field[:name]][:items] = field[:items] || OpenApi::Helper.extract_items(field[:value][0], opts) else opts.each { |k, v| current[:properties][field[:name]][k] = v if v } end - - if field[:required] - current[:required] ||= [] - current[:required] << field[:name] - end end OpenApi::Schema.new(schema) end + def get_schema(field) + type = OpenApi::Helper.extract_type(field).to_s + case type + when 'object' + OpenApi::Schema.new(type: type, properties: Hash[field.map { |k, v| [k, get_schema(v)] }]) + when 'array' + OpenApi::Schema.new(type: type, items: get_schema(field[0])) + else + OpenApi::Schema.new(type: type, example: field, nullable: field.nil? || nil) + end + end + def extract_parameters(example) - parameters = example.extended_parameters.uniq { |parameter| parameter[:name] } + known_parameters = extract_known_parameters(example.extended_parameters.reject { |p| p[:in].nil? }) + known_param_names = known_parameters.map { |p| p.name } + unknown_parameters = extract_unknown_parameters(example).reject { |p| known_param_names.include?(p.name) } + known_parameters + unknown_parameters + end + + def extract_request_body(example) + if example.respond_to?(:request_body) + OpenApi::RequestBody.new( + content: { + example.request_body[:type] || 'application/json' => OpenApi::Media.new( + schema: OpenApi::Schema.new(example.request_body[:schema]), + example: example.request_body[:example] + ) + } + ) + else + body = example.requests.map { |req| JSON.parse(req[:request_body]) rescue nil }.compact.reduce({}, :merge) + return nil if body.empty? + + schema = get_schema(body) + example.extended_parameters.select { |p| p[:in].nil? }.each do |parameter| + inject_body_parameter(schema, parameter) + end - extract_known_parameters(parameters.select { |p| !p[:in].nil? }) + - extract_unknown_parameters(example, parameters.select { |p| p[:in].nil? }) + OpenApi::RequestBody.new( + content: { + 'application/json' => OpenApi::Media.new(schema: schema, example: body) + } + ) + end end def extract_parameter(opts) @@ -171,53 +227,56 @@ def extract_parameter(opts) in: opts[:in], description: opts[:description], required: opts[:required], - type: opts[:type] || OpenApi::Helper.extract_type(opts[:value]), - value: opts[:value], - with_example: opts[:with_example], - default: opts[:default], - example: opts[:example], - ).tap do |elem| - if elem.type == :array - elem.items = opts[:items] || OpenApi::Helper.extract_items(opts[:value][0], { minimum: opts[:minimum], maximum: opts[:maximum], enum: opts[:enum] }) - else - elem.minimum = opts[:minimum] - elem.maximum = opts[:maximum] - elem.enum = opts[:enum] - end - end + deprecated: opts[:deprecated], + schema: opts[:schema] || get_schema(opts[:value]), + example: opts[:value] + ) end - def extract_unknown_parameters(example, parameters) - if example.http_method == :get - parameters.map { |parameter| extract_parameter(parameter.merge(in: :query)) } - elsif parameters.any? { |parameter| !parameter[:scope].nil? } - [OpenApi::Parameter.new( - name: :body, - in: :body, - description: '', - schema: extract_schema(parameters) - )] - else - parameters.map { |parameter| extract_parameter(parameter.merge(in: :formData)) } + def extract_unknown_parameters(example) + parameters = [] + example.requests.each do |req| + req[:request_query_parameters].each do |name, value| + parameters.push(OpenApi::Parameter.new( + name: name, + in: :query, + schema: get_schema(value), + example: value + )) + end + req[:request_headers].each do |name, value| + parameters.push(OpenApi::Parameter.new( + name: name, + in: :header, + schema: get_schema(value), + example: value + )) + end end + parameters end def extract_known_parameters(parameters) - result = parameters.select { |parameter| %w(query path header formData).include?(parameter[:in].to_s) } - .map { |parameter| extract_parameter(parameter) } + parameters.select { |parameter| %w(query path header cookie).include?(parameter[:in].to_s) } + .map { |parameter| extract_parameter(parameter) } + end - body = parameters.select { |parameter| %w(body).include?(parameter[:in].to_s) } + def inject_body_parameter(schema, parameter) + scope = schema + parameter[:scope] && Array(parameter[:scope]).each do |curr| + scope.properties[curr.to_s] ||= OpenApi::Schema.new(type: 'object', properties: {}) + scope = scope.properties[curr.to_s] + end - result.unshift( - OpenApi::Parameter.new( - name: :body, - in: :body, - description: '', - schema: extract_schema(body) - ) - ) unless body.empty? + return unless scope.properties + + if parameter[:required] + scope.required ||= [] + scope.required << parameter[:name].to_s + end - result + scope.properties[parameter[:name].to_s] ||= get_schema(parameter[:value]) + scope.properties[parameter[:name].to_s].description = parameter[:description] if parameter[:description] end end diff --git a/rspec_api_documentation.gemspec b/rspec_api_documentation.gemspec index 9590586a..978b3bf0 100644 --- a/rspec_api_documentation.gemspec +++ b/rspec_api_documentation.gemspec @@ -18,20 +18,25 @@ Gem::Specification.new do |s| s.add_runtime_dependency "activesupport", ">= 3.0.0" s.add_runtime_dependency "mustache", "~> 1.0", ">= 0.99.4" - s.add_development_dependency "bundler", "~> 1.0" - s.add_development_dependency "fakefs", "~> 0.4" - s.add_development_dependency "sinatra", "~> 1.4", ">= 1.4.4" - s.add_development_dependency "aruba", "~> 0.5" - s.add_development_dependency "capybara", "~> 2.2" - s.add_development_dependency "rake", "~> 10.1" - s.add_development_dependency "rack-test", "~> 0.6.2" - s.add_development_dependency "rack-oauth2", "~> 1.2.2", ">= 1.0.7" - s.add_development_dependency "webmock", "~> 1.7" - s.add_development_dependency "rspec-its", "~> 1.0" - s.add_development_dependency "faraday", "~> 0.9", ">= 0.9.0" - s.add_development_dependency "thin", "~> 1.6", ">= 1.6.3" - s.add_development_dependency "nokogiri", "~> 1.8", ">= 1.8.2" - s.add_development_dependency "yard", ">= 0.9.11" + s.add_development_dependency "bundler", ">= 1.16" + s.add_development_dependency "fakefs", "~> 0.6.0" + s.add_development_dependency "sinatra", "~> 2.0.8" + s.add_development_dependency "aruba", "~> 0.14.14" + s.add_development_dependency "capybara", "~> 3.39.2" + s.add_development_dependency "rake", "~> 13.2.1" + s.add_development_dependency "rack-test", "~> 0.6.3" + s.add_development_dependency "rack-oauth2", "~> 1.12.0" + s.add_development_dependency "webmock", "~> 3.23.0" + s.add_development_dependency "rspec-its", "~> 1.3.0" + s.add_development_dependency "faraday", "~> 1.0.0" + s.add_development_dependency "nokogiri", "~> 1.8.4" + s.add_development_dependency "yard", "~> 0.9.15" + s.add_development_dependency "inch", "~> 0.8.0" + s.add_development_dependency "minitest", "~> 5.8.4" + s.add_development_dependency "contracts", "~> 0.17" + s.add_development_dependency "gherkin", "~> 9.0.0" + s.add_development_dependency "multi_json", "~> 1.15.0" + s.add_development_dependency "rspec", "~> 3.0" s.files = Dir.glob("lib/**/*") + Dir.glob("templates/**/*") s.require_path = "lib" diff --git a/spec/http_test_client_spec.rb b/spec/http_test_client_spec.rb index e2282371..fd77dc0f 100644 --- a/spec/http_test_client_spec.rb +++ b/spec/http_test_client_spec.rb @@ -3,19 +3,12 @@ require 'capybara' require 'capybara/server' require 'sinatra/base' -require 'webmock' +require 'webmock/rspec' require 'support/stub_app' describe RspecApiDocumentation::HttpTestClient do before(:all) do WebMock.allow_net_connect! - - Capybara.server do |app, port| - require 'rack/handler/thin' - Thin::Logging.silent = true - Rack::Handler::Thin.run(app, :Port => port) - end - server = Capybara::Server.new(StubApp.new, 8888) server.boot end diff --git a/spec/open_api/root_spec.rb b/spec/open_api/root_spec.rb index 52debbbf..1c2c91e0 100644 --- a/spec/open_api/root_spec.rb +++ b/spec/open_api/root_spec.rb @@ -8,7 +8,6 @@ describe "default settings" do class RspecApiDocumentation::OpenApi::Info; end - class RspecApiDocumentation::OpenApi::Paths; end its(:swagger) { should == '2.0' } its(:info) { should be_a(RspecApiDocumentation::OpenApi::Info) } @@ -17,10 +16,10 @@ class RspecApiDocumentation::OpenApi::Paths; end its(:schemes) { should == %w(http https) } its(:consumes) { should == %w(application/json application/xml) } its(:produces) { should == %w(application/json application/xml) } - its(:paths) { should be_a(RspecApiDocumentation::OpenApi::Paths) } its(:definitions) { should be_nil } its(:parameters) { should be_nil } its(:responses) { should be_nil } + its(:paths) { should == {} } its(:securityDefinitions) { should be_nil } its(:security) { should be_nil } its(:tags) { should == [] } diff --git a/spec/views/api_blueprint_index_spec.rb b/spec/views/api_blueprint_index_spec.rb index 1d526597..e923abf2 100644 --- a/spec/views/api_blueprint_index_spec.rb +++ b/spec/views/api_blueprint_index_spec.rb @@ -142,7 +142,7 @@ properties_description: "required, string" }, { name: "option", - description: nil, + description: 'Option', properties_description: 'optional' }] expect(post_route_with_optionals[:has_attributes?]).to eq false @@ -158,7 +158,7 @@ expect(posts_route[:attributes]).to eq [{ required: false, name: "description", - description: nil, + description: 'Description', properties_description: "optional" }] end diff --git a/templates/rspec_api_documentation/slate_example.mustache b/templates/rspec_api_documentation/slate_example.mustache index f07233a4..f6d9aff6 100644 --- a/templates/rspec_api_documentation/slate_example.mustache +++ b/templates/rspec_api_documentation/slate_example.mustache @@ -20,8 +20,8 @@ {{# requests }} ```plaintext -{{ request_method }} {{ request_path }} -{{ request_headers_text }} +{{ request_method }} {{{ request_path }}} +{{{ request_headers_text }}} ``` {{/ requests }} @@ -29,8 +29,8 @@ {{# requests }} {{# request_query_parameters_text }} -```json -{{ request_query_parameters_text }} +```plaintext +{{{ request_query_parameters_text }}} ``` {{/ request_query_parameters_text }}