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..fc31f1e3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,66 @@ +--- + +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.3' + - 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 a0190963..e4bf1377 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +doc tmp .rvmrc .ruby-version @@ -7,3 +8,5 @@ example/public/docs *.gem *.swp /html/ +/.idea +Gemfile.lock diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index d1f69b8a..00000000 --- a/.travis.yml +++ /dev/null @@ -1,14 +0,0 @@ -language: ruby -sudo: false -rvm: - - 2.0.0-p648 - - 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 37254918..00000000 --- a/Gemfile.lock +++ /dev/null @@ -1,160 +0,0 @@ -PATH - remote: . - specs: - rspec_api_documentation (5.0.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.0) - 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.7.0) - pry - sparkr (>= 0.2.0) - term-ansicolor - yard (~> 0.8.7.5) - json (1.8.3) - method_source (0.8.2) - mime-types (3.0) - mime-types-data (~> 3.2015) - mime-types-data (3.2015.1120) - mini_portile2 (2.0.0) - minitest (5.8.4) - multi_json (1.11.2) - multi_test (0.1.2) - multipart-post (2.0.0) - mustache (1.0.3) - nokogiri (1.6.7.2) - mini_portile2 (~> 2.0.0.rc2) - pry (0.10.3) - coderay (~> 1.1.0) - method_source (~> 0.8.1) - slop (~> 3.4) - 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) - slop (3.6.0) - sparkr (0.4.1) - term-ansicolor (1.3.2) - 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.8.2) - 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.8.7.6) - -PLATFORMS - ruby - -DEPENDENCIES - aruba (~> 0.5) - bundler (~> 1.0) - capybara (~> 2.2) - fakefs (~> 0.4) - faraday (~> 0.9, >= 0.9.0) - inch - 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) - -BUNDLED WITH - 1.13.6 diff --git a/README.md b/README.md index 332f3bd6..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) @@ -38,7 +36,7 @@ resource "Orders" do example "Listing orders" do do_request - status.should == 200 + expect(status).to eq 200 end end end @@ -60,6 +58,10 @@ Consider adding a viewer to enhance the generated documentation. By itself rspec gem 'raddocs' + or + + gem 'apitome' + #### spec/spec_helper.rb ```ruby @@ -68,9 +70,106 @@ RspecApiDocumentation.configure do |config| end ``` +#### +For both raddocs and apitome, start rails server. Then + + open http://localhost:3000/docs for raddocs + + or + + http://localhost:3000/api/docs for apitome + ## Sample App -See the `example` folder for a sample Rails app that has been documented. +See the `example` folder for a sample Rails app that has been documented. The sample app demonstrates the :open_api format. + +## Example of spec file + +```ruby + # spec/acceptance/orders_spec.rb + require 'rails_helper' + require 'rspec_api_documentation/dsl' + resource 'Orders' do + explanation "Orders resource" + + header "Content-Type", "application/json" + + get '/orders' do + # This is manual way to describe complex parameters + parameter :one_level_array, type: :array, items: {type: :string, enum: ['string1', 'string2']}, default: ['string1'] + parameter :two_level_array, type: :array, items: {type: :array, items: {type: :string}} + + let(:one_level_array) { ['string1', 'string2'] } + let(:two_level_array) { [['123', '234'], ['111']] } + + # This is automatic way + # It's possible because we extract parameters definitions from the values + parameter :one_level_arr, with_example: true + parameter :two_level_arr, with_example: true + + let(:one_level_arr) { ['value1', 'value2'] } + let(:two_level_arr) { [[5.1, 3.0], [1.0, 4.5]] } + + context '200' do + example_request 'Getting a list of orders' do + expect(status).to eq(200) + end + end + end + + put '/orders/:id' do + + with_options scope: :data, with_example: true do + parameter :name, 'The order name', required: true + parameter :amount + parameter :description, 'The order description' + end + + context "200" do + let(:id) { 1 } + + example 'Update an order' do + request = { + data: { + name: 'order', + amount: 1, + description: 'fast order' + } + } + + # It's also possible to extract types of parameters when you pass data through `do_request` method. + do_request(request) + + expected_response = { + data: { + name: 'order', + amount: 1, + description: 'fast order' + } + } + expect(status).to eq(200) + expect(response_body).to eq(expected_response) + end + end + + context "400" do + let(:id) { "a" } + + example_request 'Invalid request' do + expect(status).to eq(400) + end + end + + context "404" do + let(:id) { 0 } + + example_request 'Order is not found' do + expect(status).to eq(404) + end + end + end + end +``` ## Configuration options @@ -80,13 +179,17 @@ RspecApiDocumentation.configure do |config| # Set the application that Rack::Test uses config.app = Rails.application + # Used to provide a configuration for the specification (supported only by 'open_api' format for now) + 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). # Possible values are :json, :html, :combined_text, :combined_json, # :json_iodocs, :textile, :markdown, :append_json, :slate, - # :api_blueprint + # :api_blueprint, :open_api config.format = [:html] # Location of templates @@ -134,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 @@ -146,7 +250,8 @@ RspecApiDocumentation.configure do |config| # Change how the response body is formatted by default # Is proc that will be called with the response_content_type & response_body - # by default response_content_type of `application/json` are pretty formated. + # by default, a response body that is likely to be binary is replaced with the string + # "[binary data]" regardless of the media type. Otherwise, a response_content_type of `application/json` is pretty formatted. config.response_body_formatter = Proc.new { |response_content_type, response_body| response_body } # Change the embedded style for HTML output. This file will not be processed by @@ -154,6 +259,7 @@ RspecApiDocumentation.configure do |config| config.html_embedded_css_file = nil # Removes the DSL method `status`, this is required if you have a parameter named status + # In this case you can assert response status with `expect(response_status).to eq 200` config.disable_dsl_status! # Removes the DSL method `method`, this is required if you have a parameter named method @@ -172,6 +278,8 @@ end * **markdown**: Generates an index file and example files in Markdown. * **api_blueprint**: Generates an index file and example files in [APIBlueprint](https://apiblueprint.org). * **append_json**: Lets you selectively run specs without destroying current documentation. See section below. +* **slate**: Builds markdown files that can be used with [Slate](https://github.com/lord/slate), a beautiful static documentation builder. +* **open_api**: Generates [OpenAPI Specification](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md) (OAS) (Current supported version is 2.0). Can be used for [Swagger-UI](https://swagger.io/tools/swagger-ui/) ### append_json @@ -228,6 +336,172 @@ This [format](https://apiblueprint.org) (APIB) has additional functions: * `attribute`: APIB has attributes besides parameters. Use attributes exactly like you'd use `parameter` (see documentation below). + +### open_api + +This [format](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md) (OAS) has additional functions: + +* `authentication(type, value, opts = {})` ([Security schema object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#security-scheme-object)) + + The values will be passed through header of the request. Option `name` has to be provided for `apiKey`. + + * `authentication :basic, 'Basic Key'` + * `authentication :apiKey, 'Api Key', name: 'API_AUTH', description: 'Some description'` + + You could pass `Symbol` as value. In this case you need to define a `let` with the same name. + + ``` + authentication :apiKey, :api_key + let(:api_key) { some_value } + ``` + +* `route_summary(text)` and `route_description(text)`. ([Operation object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#operation-object)) + + These two simplest methods accept `String`. + It will be used for route's `summary` and `description`. + +* Several new options on `parameter` helper. + + - `with_example: true`. This option will adjust your example of the parameter with the passed value. + - `example: `. Will provide a example value for the parameter. + - `default: `. Will provide a default value for the parameter. + - `minimum: `. Will setup upper limit for your parameter. + - `maximum: `. Will setup lower limit for your parameter. + - `enum: [, , ..]`. Will provide a pre-defined list of possible values for your parameter. + - `type: [:file, :array, :object, :boolean, :integer, :number, :string]`. Will set a type for the parameter. Most of the type you don't need to provide this option manually. We extract types from values automatically. + + +You also can provide a configuration file in YAML or JSON format with some manual configs. +The file should be placed in `configurations_dir` folder with the name `open_api.yml` or `open_api.json`. +In this file you able to manually **hide** some endpoints/resources you want to hide from generated API specification but still want to test. +It's also possible to pass almost everything to the specification builder manually. + +#### Example of configuration file + +```yaml +swagger: '2.0' +info: + title: OpenAPI App + description: This is a sample server. + termsOfService: 'http://open-api.io/terms/' + contact: + name: API Support + url: 'http://www.open-api.io/support' + email: support@open-api.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' + version: 1.0.0 +host: 'localhost:3000' +schemes: + - http + - https +consumes: + - application/json + - application/xml +produces: + - application/json + - application/xml +paths: + /orders: + hide: true + /instructions: + hide: false + get: + description: This description came from configuration file + hide: true +``` +#### Example of spec file with :open_api format +```ruby + resource 'Orders' do + explanation "Orders resource" + + authentication :apiKey, :api_key, description: 'Private key for API access', name: 'HEADER_KEY' + header "Content-Type", "application/json" + + let(:api_key) { generate_api_key } + + get '/orders' do + route_summary "This URL allows users to interact with all orders." + route_description "Long description." + + # This is manual way to describe complex parameters + parameter :one_level_array, type: :array, items: {type: :string, enum: ['string1', 'string2']}, default: ['string1'] + parameter :two_level_array, type: :array, items: {type: :array, items: {type: :string}} + + let(:one_level_array) { ['string1', 'string2'] } + let(:two_level_array) { [['123', '234'], ['111']] } + + # This is automatic way + # It's possible because we extract parameters definitions from the values + parameter :one_level_arr, with_example: true + parameter :two_level_arr, with_example: true + + let(:one_level_arr) { ['value1', 'value2'] } + let(:two_level_arr) { [[5.1, 3.0], [1.0, 4.5]] } + + context '200' do + example_request 'Getting a list of orders' do + expect(status).to eq(200) + expect(response_body).to eq() + end + end + end + + put '/orders/:id' do + route_summary "This is used to update orders." + + with_options scope: :data, with_example: true do + parameter :name, 'The order name', required: true + parameter :amount + parameter :description, 'The order description' + end + + context "200" do + let(:id) { 1 } + + example 'Update an order' do + request = { + data: { + name: 'order', + amount: 1, + description: 'fast order' + } + } + + # It's also possible to extract types of parameters when you pass data through `do_request` method. + do_request(request) + + expected_response = { + data: { + name: 'order', + amount: 1, + description: 'fast order' + } + } + expect(status).to eq(200) + expect(response_body).to eq() + end + end + + context "400" do + let(:id) { "a" } + + example_request 'Invalid request' do + expect(status).to eq(400) + end + end + + context "404" do + let(:id) { 0 } + + example_request 'Order is not found' do + expect(status).to eq(404) + end + end + end + end +``` ## Filtering and Exclusion @@ -244,18 +518,18 @@ resource "Account" do # default :document is :all example "Get a list of all accounts" do do_request - status.should == 200 + expect(status).to eq 200 end # Don't actually document this example, purely for testing purposes example "Get a list on page 2", :document => false do do_request(:page => 2) - status.should == 404 + expect(status).to eq 404 end # With example_request, you can't change the :document example_request "Get a list on page 3", :page => 3 do - status.should == 404 + expect(status).to eq 404 end end @@ -264,12 +538,12 @@ resource "Account" do example "Creating an account", :document => :private do do_request(:email => "eric@example.com") - status.should == 201 + expect(status).to eq 201 end example "Creating an account - errors", :document => [:private, :developers] do do_request - status.should == 422 + expect(status).to eq 422 end end end @@ -338,7 +612,7 @@ resource "Orders" do let(:id) { order.id } example "Get an order" do - path.should == "/orders/1" # `:id` is replaced with the value of `id` + expect(path).to eq "/orders/1" # `:id` is replaced with the value of `id` end end @@ -428,7 +702,7 @@ resource "Orders" do get "/orders" do example_request "Headers" do - headers.should == { "Accept" => "application/json", "X-Custom" => "dynamic" } + expect(headers).to eq { "Accept" => "application/json", "X-Custom" => "dynamic" } end end end @@ -467,7 +741,7 @@ resource "Orders" do # OR let(:order_item_item_id) { 1 } example "Creating an order" do - params.should eq({ + expect(params).to eq({ :order => { :name => "My Order", :item => { @@ -556,7 +830,7 @@ resource "Order" do do_request - status.should == 200 + expect(status).to eq 200 end end end @@ -576,7 +850,7 @@ resource "Order" do example "Listing orders" do do_request - status.should == 200 + expect(status).to eq 200 end end end @@ -597,7 +871,7 @@ resource "Order" do do_request - status.should == 200 + expect(status).to eq 200 end end end @@ -619,7 +893,7 @@ resource "Orders" do get "/orders" do example_request "Headers" do - headers.should == { "Accept" => "application/json" } + expect(headers).to eq { "Accept" => "application/json" } end end end @@ -639,7 +913,7 @@ resource "Order" do example "Listing orders" do do_request - response_body.should == [{ :name => "Order 1" }].to_json + expect(response_body).to eq [{ :name => "Order 1" }].to_json end end end @@ -655,7 +929,7 @@ resource "Order" do example "Listing orders" do do_request - response_headers["Content-Type"].should == "application/json" + expect(response_headers["Content-Type"]).to eq "application/json" end end end @@ -671,8 +945,8 @@ resource "Order" do example "Listing orders" do do_request - status.should == 200 - response_status.should == 200 + expect(status).to eq 200 + expect(response_status).to eq 200 end end end @@ -690,7 +964,7 @@ resource "Orders" do get "/orders" do example "List orders" do - query_string.should == "name=My+Orders" + expect(query_string).to eq "name=My+Orders" end end end diff --git a/example/Gemfile b/example/Gemfile index 7596b39b..cacf5aff 100644 --- a/example/Gemfile +++ b/example/Gemfile @@ -2,12 +2,15 @@ source 'https://rubygems.org' ruby '2.3.3' +gem 'rack-cors', :require => 'rack/cors' gem 'rails', '4.2.5.1' gem 'sqlite3' gem 'spring', group: :development -gem 'raddocs', :github => "smartlogic/raddocs" +gem 'raddocs', '0.4.0' group :test, :development do + gem 'byebug' + gem 'awesome_print' gem 'rspec-rails' gem 'rspec_api_documentation', :path => "../" end diff --git a/example/Gemfile.lock b/example/Gemfile.lock index c1a4f808..fd2e4eef 100644 --- a/example/Gemfile.lock +++ b/example/Gemfile.lock @@ -1,20 +1,10 @@ -GIT - remote: git://github.com/smartlogic/raddocs.git - revision: 9cf49c1ef3b3d7dc3bf8e19ef75021040df04652 - specs: - raddocs (0.4.0) - haml (~> 4.0, >= 4.0.4) - json (~> 1.8, >= 1.8.1) - sinatra (~> 1.3, >= 1.3.0) - PATH - remote: ../ + remote: .. specs: - rspec_api_documentation (4.7.0) + rspec_api_documentation (5.1.0) activesupport (>= 3.0.0) - json (~> 1.4, >= 1.4.6) - mustache (~> 0.99, >= 0.99.4) - rspec (>= 3.0.0) + mustache (~> 1.0, >= 0.99.4) + rspec (~> 3.0) GEM remote: https://rubygems.org/ @@ -55,13 +45,15 @@ GEM thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) arel (6.0.3) + awesome_print (1.7.0) builder (3.2.2) + byebug (9.0.6) concurrent-ruby (1.0.0) diff-lcs (1.2.5) erubis (2.7.0) globalid (0.3.6) activesupport (>= 4.1.0) - haml (4.0.5) + haml (4.0.7) tilt i18n (0.7.0) json (1.8.3) @@ -72,14 +64,19 @@ GEM mime-types (2.99) mini_portile2 (2.0.0) minitest (5.8.4) - mustache (0.99.8) + mustache (1.0.5) nokogiri (1.6.7.2) mini_portile2 (~> 2.0.0.rc2) rack (1.6.4) - rack-protection (1.5.3) + rack-cors (0.4.1) + rack-protection (1.5.5) rack rack-test (0.6.3) rack (>= 1.0) + raddocs (0.4.0) + haml (~> 4.0, >= 4.0.4) + json (~> 1.8, >= 1.8.1) + sinatra (~> 1.3, >= 1.3.0) rails (4.2.5.1) actionmailer (= 4.2.5.1) actionpack (= 4.2.5.1) @@ -125,10 +122,10 @@ GEM rspec-mocks (~> 3.0.0) rspec-support (~> 3.0.0) rspec-support (3.0.4) - sinatra (1.4.5) - rack (~> 1.4) + sinatra (1.4.8) + rack (~> 1.5) rack-protection (~> 1.4) - tilt (~> 1.3, >= 1.3.4) + tilt (>= 1.3, < 3) spring (1.1.3) sprockets (3.5.2) concurrent-ruby (~> 1.0) @@ -140,7 +137,7 @@ GEM sqlite3 (1.3.9) thor (0.19.1) thread_safe (0.3.5) - tilt (1.4.1) + tilt (2.0.8) tzinfo (1.2.2) thread_safe (~> 0.1) @@ -148,12 +145,18 @@ PLATFORMS ruby DEPENDENCIES - raddocs! + awesome_print + byebug + rack-cors + raddocs (= 0.4.0) rails (= 4.2.5.1) rspec-rails rspec_api_documentation! spring sqlite3 +RUBY VERSION + ruby 2.3.3p222 + BUNDLED WITH - 1.11.2 + 1.16.3 diff --git a/example/app/controllers/application_controller.rb b/example/app/controllers/application_controller.rb index d83690e1..840f64a8 100644 --- a/example/app/controllers/application_controller.rb +++ b/example/app/controllers/application_controller.rb @@ -1,5 +1,6 @@ class ApplicationController < ActionController::Base # Prevent CSRF attacks by raising an exception. # For APIs, you may want to use :null_session instead. - protect_from_forgery with: :exception + # protect_from_forgery with: :exception + protect_from_forgery with: :null_session end diff --git a/example/app/controllers/orders_controller.rb b/example/app/controllers/orders_controller.rb index a91572cb..9ec2f703 100644 --- a/example/app/controllers/orders_controller.rb +++ b/example/app/controllers/orders_controller.rb @@ -1,10 +1,19 @@ class OrdersController < ApplicationController + before_action only: :index do + head :unauthorized unless request.headers['HTTP_AUTH_TOKEN'] =~ /\AAPI_TOKEN$/ + end + def index render :json => Order.all end def show - render :json => Order.find(params[:id]) + order = Order.find_by(id: params[:id]) + if order + render json: order + else + head :not_found + end end def create diff --git a/example/app/controllers/uploads_controller.rb b/example/app/controllers/uploads_controller.rb index 34b7f276..8855a415 100644 --- a/example/app/controllers/uploads_controller.rb +++ b/example/app/controllers/uploads_controller.rb @@ -1,4 +1,6 @@ class UploadsController < ApplicationController + http_basic_authenticate_with name: 'user', password: 'password' + def create head 201 end diff --git a/example/config/application.rb b/example/config/application.rb index ee6bc294..26647b9d 100644 --- a/example/config/application.rb +++ b/example/config/application.rb @@ -15,6 +15,14 @@ module Example class Application < Rails::Application + + config.middleware.insert_before 0, 'Rack::Cors' do + allow do + origins '*' + resource '*', :headers => :any, :methods => [:get, :post, :options, :put, :patch, :delete, :head] + end + end + # 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. diff --git a/example/config/open_api.yml b/example/config/open_api.yml new file mode 100644 index 00000000..0be381d2 --- /dev/null +++ b/example/config/open_api.yml @@ -0,0 +1,23 @@ +swagger: '2.0' +info: + title: OpenAPI App + description: This is a sample server Petstore server. + termsOfService: 'http://open-api.io/terms/' + contact: + name: API Support + url: 'http://www.open-api.io/support' + email: support@open-api.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' + version: 1.0.1 +host: 'localhost:3000' +schemes: + - http + - https +consumes: + - application/json + - application/xml +produces: + - application/json + - application/xml diff --git a/example/db/schema.rb b/example/db/schema.rb index 2cbe495b..a3b2ef4b 100644 --- a/example/db/schema.rb +++ b/example/db/schema.rb @@ -13,7 +13,7 @@ ActiveRecord::Schema.define(version: 20140616151047) do - create_table "orders", force: true do |t| + create_table "orders", force: :cascade do |t| t.string "name" t.boolean "paid" t.string "email" diff --git a/example/spec/acceptance/orders_spec.rb b/example/spec/acceptance/orders_spec.rb index a6596191..4505aabc 100644 --- a/example/spec/acceptance/orders_spec.rb +++ b/example/spec/acceptance/orders_spec.rb @@ -4,10 +4,13 @@ header "Accept", "application/json" header "Content-Type", "application/json" + explanation "Orders are top-level business objects" + let(:order) { Order.create(:name => "Old Name", :paid => true, :email => "email@example.com") } get "/orders" do - parameter :page, "Current page of orders" + authentication :apiKey, "API_TOKEN", :name => "AUTH_TOKEN" + parameter :page, "Current page of orders", with_example: true let(:page) { 1 } @@ -24,23 +27,31 @@ end head "/orders" do + authentication :apiKey, "API_TOKEN", :name => "AUTH_TOKEN" + example_request "Getting the headers" do expect(response_headers["Cache-Control"]).to eq("max-age=0, private, must-revalidate") end end post "/orders" do - parameter :name, "Name of order", :required => true, :scope => :order - parameter :paid, "If the order has been paid for", :required => true, :scope => :order - parameter :email, "Email of user that placed the order", :scope => :order + with_options :scope => :order, :with_example => true do + parameter :name, "Name of order", :required => true + parameter :paid, "If the order has been paid for", :required => true + parameter :email, "Email of user that placed the order" + parameter :data, "Array of string", :type => :array, :items => {:type => :string} + end - response_field :name, "Name of order", :scope => :order, "Type" => "String" - response_field :paid, "If the order has been paid for", :scope => :order, "Type" => "Boolean" - response_field :email, "Email of user that placed the order", :scope => :order, "Type" => "String" + with_options :scope => :order do + response_field :name, "Name of order", :type => :string + response_field :paid, "If the order has been paid for", :type => :boolean + response_field :email, "Email of user that placed the order", :type => :string + end let(:name) { "Order 1" } let(:paid) { true } let(:email) { "email@example.com" } + let(:data) { ["good", "bad"] } let(:raw_post) { params.to_json } @@ -61,18 +72,31 @@ end get "/orders/:id" do - let(:id) { order.id } + context "when id is valid" do + let(:id) { order.id } - example_request "Getting a specific order" do - expect(response_body).to eq(order.to_json) - expect(status).to eq(200) + example_request "Getting a specific order" do + expect(response_body).to eq(order.to_json) + expect(status).to eq(200) + end + end + + context "when id is invalid" do + let(:id) { "a" } + + example_request "Getting an error" do + expect(response_body).to eq "" + expect(status).to eq 404 + end end end put "/orders/:id" do - parameter :name, "Name of order", :scope => :order - parameter :paid, "If the order has been paid for", :scope => :order - parameter :email, "Email of user that placed the order", :scope => :order + with_options :scope => :order, with_example: true do + parameter :name, "Name of order" + parameter :paid, "If the order has been paid for" + parameter :email, "Email of user that placed the order" + end let(:id) { order.id } let(:name) { "Updated Name" } diff --git a/example/spec/acceptance/uploads_spec.rb b/example/spec/acceptance/uploads_spec.rb index 8c07d531..b242ca54 100644 --- a/example/spec/acceptance/uploads_spec.rb +++ b/example/spec/acceptance/uploads_spec.rb @@ -1,6 +1,10 @@ require 'acceptance_helper' resource "Uploads" do + authentication :basic, :api_key, :description => "Api Key description" + + let(:api_key) { "Basic #{Base64.encode64('user:password')}" } + post "/uploads" do parameter :file, "New file to upload" diff --git a/example/spec/acceptance_helper.rb b/example/spec/acceptance_helper.rb index 621342fe..7b4e69f4 100644 --- a/example/spec/acceptance_helper.rb +++ b/example/spec/acceptance_helper.rb @@ -3,7 +3,7 @@ require 'rspec_api_documentation/dsl' RspecApiDocumentation.configure do |config| - config.format = [:json, :combined_text, :html] + config.format = [:open_api, :html] config.curl_host = 'http://localhost:3000' config.api_name = "Example App API" config.api_explanation = "API Example Description" diff --git a/features/api_blueprint_documentation.feature b/features/api_blueprint_documentation.feature index 2ac78462..59884c2e 100644 --- a/features/api_blueprint_documentation.feature +++ b/features/api_blueprint_documentation.feature @@ -115,6 +115,11 @@ Feature: Generate API Blueprint documentation from test examples attribute :name, 'The order name', required: true, :example => 'a name' attribute :amount, required: false attribute :description, 'The order description', type: 'string', required: false, example: "a description" + attribute :category, 'The order category', type: 'string', required: false, default: 'normal', enum: %w[normal priority] + attribute :metadata, 'The order metadata', type: 'json', required: false, annotation: <<-MARKDOWN + + instructions (optional, string) + + notes (optional, string) + MARKDOWN get 'Returns a single order' do explanation "This is used to return orders." @@ -248,7 +253,9 @@ Feature: Generate API Blueprint documentation from test examples Scenario: Index file should look like we expect Then the file "doc/api/index.apib" should contain exactly: """ - FORMAT: A1 + FORMAT: 1A + # Example API + Example API Description # Group Instructions @@ -262,18 +269,17 @@ Feature: Generate API Blueprint documentation from test examples + Headers - Host: example.org + Host: example.org + Response 200 (text/html;charset=utf-8) + Headers - Content-Type: text/html;charset=utf-8 - Content-Length: 57 + Content-Length: 57 + Body - {"data":{"id":"1","type":"instructions","attributes":{}}} + {"data":{"id":"1","type":"instructions","attributes":{}}} # Group Orders @@ -287,38 +293,36 @@ Feature: Generate API Blueprint documentation from test examples + Headers - Content-Type: application/json - Host: example.org + Host: example.org + Body - { - "data": { - "type": "order", - "attributes": { - "name": "Order 1", - "amount": 100.0, - "description": "A description" + { + "data": { + "type": "order", + "attributes": { + "name": "Order 1", + "amount": 100.0, + "description": "A description" + } + } } - } - } + Response 201 (application/json) + Headers - Content-Type: application/json - Content-Length: 73 + Content-Length: 73 + Body - { - "order": { - "name": "Order 1", - "amount": 100.0, - "description": "A great order" - } - } + { + "order": { + "name": "Order 1", + "amount": 100.0, + "description": "A great order" + } + } ### Return all orders [GET] @@ -326,43 +330,50 @@ Feature: Generate API Blueprint documentation from test examples + Headers - Host: example.org + Host: example.org + Response 200 (application/vnd.api+json) + Headers - Content-Type: application/vnd.api+json - Content-Length: 137 + Content-Length: 137 + Body - { - "page": 1, - "orders": [ - { - "name": "Order 1", - "amount": 9.99, - "description": null - }, { - "name": "Order 2", - "amount": 100.0, - "description": "A great order" + "page": 1, + "orders": [ + { + "name": "Order 1", + "amount": 9.99, + "description": null + }, + { + "name": "Order 2", + "amount": 100.0, + "description": "A great order" + } + ] } - ] - } - ## Single Order [/orders/:id{?optional=:optional}] + ## Single Order [/orders/{id}{?optional=:optional}] + Parameters + id: 1 (required, string) - Order id - + optional + + optional (optional) + Attributes (object) + name: a name (required) - The order name - + amount - + description: a description (string) - The order description + + amount (optional) + + description: a description (optional, string) - The order description + + category (optional, string) - The order category + + Default: `normal` + + Members + + `normal` + + `priority` + + metadata (optional, json) - The order metadata + + instructions (optional, string) + + notes (optional, string) ### Deletes a specific order [DELETE] @@ -370,15 +381,13 @@ Feature: Generate API Blueprint documentation from test examples + Headers - Host: example.org - Content-Type: application/x-www-form-urlencoded + Host: example.org + Response 200 (text/html;charset=utf-8) + Headers - Content-Type: text/html;charset=utf-8 - Content-Length: 0 + Content-Length: 0 ### Returns a single order [GET] @@ -386,24 +395,23 @@ Feature: Generate API Blueprint documentation from test examples + Headers - Host: example.org + Host: example.org + Response 200 (application/json) + Headers - Content-Type: application/json - Content-Length: 73 + Content-Length: 73 + Body - { - "order": { - "name": "Order 1", - "amount": 100.0, - "description": "A great order" - } - } + { + "order": { + "name": "Order 1", + "amount": 100.0, + "description": "A great order" + } + } ### Updates a single order [PUT] @@ -411,55 +419,51 @@ Feature: Generate API Blueprint documentation from test examples + Headers - Content-Type: application/json; charset=utf-16 - Host: example.org + Host: example.org + Response 400 (application/json) + Headers - Content-Type: application/json - Content-Length: 0 + Content-Length: 0 + Request Update an order (application/json; charset=utf-16) + Headers - Content-Type: application/json; charset=utf-16 - Host: example.org + Host: example.org + Body - { - "data": { - "id": "1", - "type": "order", - "attributes": { - "name": "Order 1" + { + "data": { + "id": "1", + "type": "order", + "attributes": { + "name": "Order 1" + } + } } - } - } + Response 200 (application/json) + Headers - Content-Type: application/json - Content-Length: 111 + Content-Length: 111 + Body - { - "data": { - "id": "1", - "type": "order", - "attributes": { - "name": "Order 1", - "amount": 100.0, - "description": "A description" + { + "data": { + "id": "1", + "type": "order", + "attributes": { + "name": "Order 1", + "amount": 100.0, + "description": "A description" + } + } } - } - } """ Scenario: Example 'Deleting an order' file should not be created diff --git a/features/markdown_documentation.feature b/features/markdown_documentation.feature index a6cf4957..d77e10bd 100644 --- a/features/markdown_documentation.feature +++ b/features/markdown_documentation.feature @@ -146,7 +146,7 @@ Feature: Generate Markdown documentation from test examples And the exit status should be 0 Scenario: Index file should look like we expect - Then the file "doc/api/index.markdown" should contain exactly: + Then the file "doc/api/index.md" should contain exactly: """ # Example API Example API Description @@ -155,19 +155,19 @@ Feature: Generate Markdown documentation from test examples Getting help - * [Getting welcome message](help/getting_welcome_message.markdown) + * [Getting welcome message](help/getting_welcome_message.md) ## Orders - * [Creating an order](orders/creating_an_order.markdown) - * [Deleting an order](orders/deleting_an_order.markdown) - * [Getting a list of orders](orders/getting_a_list_of_orders.markdown) - * [Getting a specific order](orders/getting_a_specific_order.markdown) - * [Updating an order](orders/updating_an_order.markdown) + * [Creating an order](orders/creating_an_order.md) + * [Deleting an order](orders/deleting_an_order.md) + * [Getting a list of orders](orders/getting_a_list_of_orders.md) + * [Getting a specific order](orders/getting_a_specific_order.md) + * [Updating an order](orders/updating_an_order.md) """ Scenario: Example 'Getting a list of orders' file should look like we expect - Then the file "doc/api/orders/getting_a_list_of_orders.markdown" should contain exactly: + Then the file "doc/api/orders/getting_a_list_of_orders.md" should contain exactly: """ # Orders API @@ -222,7 +222,7 @@ Feature: Generate Markdown documentation from test examples """ Scenario: Example 'Creating an order' file should look like we expect - Then the file "doc/api/orders/creating_an_order.markdown" should contain exactly: + Then the file "doc/api/orders/creating_an_order.md" should contain exactly: """ # Orders API @@ -266,16 +266,16 @@ Feature: Generate Markdown documentation from test examples """ Scenario: Example 'Deleting an order' file should be created - Then a file named "doc/api/orders/deleting_an_order.markdown" should exist + Then a file named "doc/api/orders/deleting_an_order.md" should exist Scenario: Example 'Getting a list of orders' file should be created - Then a file named "doc/api/orders/getting_a_list_of_orders.markdown" should exist + Then a file named "doc/api/orders/getting_a_list_of_orders.md" should exist Scenario: Example 'Getting a specific order' file should be created - Then a file named "doc/api/orders/getting_a_specific_order.markdown" should exist + Then a file named "doc/api/orders/getting_a_specific_order.md" should exist Scenario: Example 'Updating an order' file should be created - Then a file named "doc/api/orders/updating_an_order.markdown" should exist + Then a file named "doc/api/orders/updating_an_order.md" should exist Scenario: Example 'Getting welcome message' file should be created - Then a file named "doc/api/help/getting_welcome_message.markdown" should exist + Then a file named "doc/api/help/getting_welcome_message.md" should exist 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/open_api.feature b/features/open_api.feature new file mode 100644 index 00000000..cece521e --- /dev/null +++ b/features/open_api.feature @@ -0,0 +1,810 @@ +@ruby27_required +Feature: Generate Open API Specification from test examples + + Background: + Given a file named "app.rb" with: + """ + require 'sinatra' + + class App < Sinatra::Base + get '/orders' do + content_type "application/vnd.api+json" + + [200, { + :page => 1, + :orders => [ + { name: 'Order 1', amount: 9.99, description: nil }, + { name: 'Order 2', amount: 100.0, description: 'A great order' } + ] + }.to_json] + end + + get '/orders/:id' do + content_type :json + + [200, { order: { name: 'Order 1', amount: 100.0, description: 'A great order' } }.to_json] + end + + post '/orders' do + content_type :json + + [201, { order: { name: 'Order 1', amount: 100.0, description: 'A great order' } }.to_json] + end + + put '/orders/:id' do + content_type :json + + if params[:id].to_i > 0 + [200, request.body.read] + else + [400, ""] + end + end + + delete '/orders/:id' do + 200 + end + + get '/instructions' do + response_body = { + data: { + id: "1", + type: "instructions", + attributes: {} + } + } + [200, response_body.to_json] + end + end + """ + And a file named "open_api.json" with: + """ + { + "swagger": "2.0", + "info": { + "title": "OpenAPI App", + "description": "This is a sample of OpenAPI specification.", + "termsOfService": "http://open-api.io/terms/", + "contact": { + "name": "API Support", + "url": "http://www.open-api.io/support", + "email": "support@open-api.io" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0.1" + }, + "host": "localhost:3000", + "schemes": [ + "http" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + { + "name": "Orders", + "description": "Order's tag description" + } + ], + "paths": { + "/should_be_hided": { + "hide": true + }, + "/not_hided": { + "hide": false, + "get": { + "hide": true + } + }, + "/instructions": { + "get": { + "description": "This description came from config.yml 1" + } + }, + "/orders": { + "post": { + "description": "This description came from config.yml 2" + } + } + } + } + """ + And a file named "app_spec.rb" with: + """ + require "rspec_api_documentation" + require "rspec_api_documentation/dsl" + + RspecApiDocumentation.configure do |config| + config.app = App + config.api_name = "Example API" + config.format = :open_api + config.configurations_dir = "." + config.request_body_formatter = :json + config.request_headers_to_include = %w[Content-Type Host] + config.response_headers_to_include = %w[Content-Type Content-Length] + end + + resource 'Orders' do + explanation "Orders resource" + + get '/orders' do + route_summary "This URL allows users to interact with all orders." + route_description "Long description." + + parameter :one_level_array, type: :array, items: {type: :string, enum: ['string1', 'string2']}, default: ['string1'] + parameter :two_level_array, type: :array, items: {type: :array, items: {type: :string}} + + parameter :one_level_arr, with_example: true + parameter :two_level_arr, with_example: true + + let(:one_level_arr) { ['value1', 'value2'] } + let(:two_level_arr) { [[5.1, 3.0], [1.0, 4.5]] } + + example_request 'Getting a list of orders' do + expect(status).to eq(200) + expect(response_body).to eq('{"page":1,"orders":[{"name":"Order 1","amount":9.99,"description":null},{"name":"Order 2","amount":100.0,"description":"A great order"}]}') + end + end + + post '/orders' do + route_summary "This is used to create orders." + + header "Content-Type", "application/json" + + parameter :name, scope: :data, with_example: true, default: 'name' + parameter :description, scope: :data, with_example: true + parameter :amount, scope: :data, with_example: true, minimum: 0, maximum: 100 + parameter :values, scope: :data, with_example: true, enum: [1, 2, 3, 5] + + example 'Creating an order' do + request = { + data: { + name: "Order 1", + amount: 100.0, + description: "A description", + values: [5.0, 1.0] + } + } + do_request(request) + expect(status).to eq(201) + end + end + + get '/orders/:id' do + route_summary "This is used to return orders." + route_description "Returns a specific order." + + let(:id) { 1 } + + example_request 'Getting a specific order' do + expect(status).to eq(200) + expect(response_body).to eq('{"order":{"name":"Order 1","amount":100.0,"description":"A great order"}}') + end + end + + put '/orders/:id' do + route_summary "This is used to update orders." + + parameter :name, 'The order name', required: true, scope: :data, with_example: true + parameter :amount, required: false, scope: :data, with_example: true + parameter :description, 'The order description', required: true, scope: :data, with_example: true + + header "Content-Type", "application/json" + + context "with a valid id" do + let(:id) { 1 } + + example 'Update an order' do + request = { + data: { + name: 'order', + amount: 1, + description: 'fast order' + } + } + do_request(request) + expected_response = { + data: { + name: 'order', + amount: 1, + description: 'fast order' + } + } + expect(status).to eq(200) + expect(response_body).to eq(expected_response.to_json) + end + end + + context "with an invalid id" do + let(:id) { "a" } + + example_request 'Invalid request' do + expect(status).to eq(400) + expect(response_body).to eq("") + end + end + end + + delete '/orders/:id' do + route_summary "This is used to delete orders." + + let(:id) { 1 } + + example_request "Deleting an order" do + expect(status).to eq(200) + expect(response_body).to eq('') + end + end + end + + resource 'Instructions' do + explanation 'Instructions help the users use the app.' + + get '/instructions' do + route_summary 'This should be used to get all instructions.' + + example_request 'List all instructions' do + expected_response = { + data: { + id: "1", + type: "instructions", + attributes: {} + } + } + expect(status).to eq(200) + expect(response_body).to eq(expected_response.to_json) + end + end + end + """ + When I run `rspec app_spec.rb --require ./app.rb --format RspecApiDocumentation::ApiFormatter` + + Scenario: Output helpful progress to the console + Then the output should contain: + """ + Generating API Docs + Orders + GET /orders + * Getting a list of orders + POST /orders + * Creating an order + GET /orders/:id + * Getting a specific order + PUT /orders/:id + with a valid id + * Update an order + with an invalid id + * Invalid request + DELETE /orders/:id + * Deleting an order + Instructions + GET /instructions + * List all instructions + """ + And the output should contain "7 examples, 0 failures" + And the exit status should be 0 + + Scenario: Index file should look like we expect + Then the file "doc/api/open_api.json" should contain JSON exactly like: + """ +{ + "swagger": "2.0", + "info": { + "title": "OpenAPI App", + "description": "This is a sample of OpenAPI specification.", + "termsOfService": "http://open-api.io/terms/", + "contact": { + "name": "API Support", + "url": "http://www.open-api.io/support", + "email": "support@open-api.io" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0.1" + }, + "host": "localhost:3000", + "schemes": [ + "http" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": { + "/not_hided": {}, + "/instructions": { + "get": { + "tags": [ + "Instructions" + ], + "summary": "This should be used to get all instructions.", + "description": "This description came from config.yml 1", + "consumes": [], + "produces": [ + "text/html" + ], + "parameters": [], + "responses": { + "200": { + "description": "List all instructions", + "schema": { + "type": "object", + "properties": {} + }, + "headers": { + "Content-Type": { + "type": "string", + "x-example-value": "text/html;charset=utf-8" + }, + "Content-Length": { + "type": "string", + "x-example-value": "57" + } + }, + "examples": { + "text/html": { + "data": { + "id": "1", + "type": "instructions", + "attributes": {} + } + } + } + } + }, + "deprecated": false, + "security": [] + } + }, + "/orders": { + "get": { + "tags": [ + "Orders" + ], + "summary": "This URL allows users to interact with all orders.", + "description": "Long description.", + "consumes": [], + "produces": [ + "application/vnd.api+json" + ], + "parameters": [ + { + "name": "one_level_array", + "in": "query", + "description": "One level array", + "required": false, + "type": "array", + "items": { + "type": "string", + "enum": [ + "string1", + "string2" + ] + }, + "default": [ + "string1" + ] + }, + { + "name": "two_level_array", + "in": "query", + "description": "Two level array", + "required": false, + "type": "array", + "items": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "one_level_arr", + "in": "query", + "description": "One level arr", + "required": false, + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "value1", + "value2" + ] + }, + { + "name": "two_level_arr", + "in": "query", + "description": "Two level arr", + "required": false, + "type": "array", + "items": { + "type": "array", + "items": { + "type": "number" + } + }, + "example": [ + [ + 5.1, + 3.0 + ], + [ + 1.0, + 4.5 + ] + ] + } + ], + "responses": { + "200": { + "description": "Getting a list of orders", + "schema": { + "type": "object", + "properties": {} + }, + "headers": { + "Content-Type": { + "type": "string", + "x-example-value": "application/vnd.api+json" + }, + "Content-Length": { + "type": "string", + "x-example-value": "137" + } + }, + "examples": { + "application/vnd.api+json": { + "page": 1, + "orders": [ + { + "name": "Order 1", + "amount": 9.99, + "description": null + }, + { + "name": "Order 2", + "amount": 100.0, + "description": "A great order" + } + ] + } + } + } + }, + "deprecated": false, + "security": [] + }, + "post": { + "tags": [ + "Orders" + ], + "summary": "This is used to create orders.", + "description": "This description came from config.yml 2", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "body", + "in": "body", + "description": "", + "required": false, + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "Order 1", + "default": "name", + "description": "Data name" + }, + "description": { + "type": "string", + "example": "A description", + "description": "Data description" + }, + "amount": { + "type": "number", + "example": 100.0, + "description": "Data amount", + "minimum": 0, + "maximum": 100 + }, + "values": { + "type": "array", + "example": [ + 5.0, + 1.0 + ], + "description": "Data values", + "items": { + "type": "number", + "enum": [ + 1, + 2, + 3, + 5 + ] + } + } + } + } + } + } + } + ], + "responses": { + "201": { + "description": "Creating an order", + "schema": { + "type": "object", + "properties": {} + }, + "headers": { + "Content-Type": { + "type": "string", + "x-example-value": "application/json" + }, + "Content-Length": { + "type": "string", + "x-example-value": "73" + } + }, + "examples": { + "application/json": { + "order": { + "name": "Order 1", + "amount": 100.0, + "description": "A great order" + } + } + } + } + }, + "deprecated": false, + "security": [] + } + }, + "/orders/{id}": { + "get": { + "tags": [ + "Orders" + ], + "summary": "This is used to return orders.", + "description": "Returns a specific order.", + "consumes": [], + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "integer" + } + ], + "responses": { + "200": { + "description": "Getting a specific order", + "schema": { + "type": "object", + "properties": {} + }, + "headers": { + "Content-Type": { + "type": "string", + "x-example-value": "application/json" + }, + "Content-Length": { + "type": "string", + "x-example-value": "73" + } + }, + "examples": { + "application/json": { + "order": { + "name": "Order 1", + "amount": 100.0, + "description": "A great order" + } + } + } + } + }, + "deprecated": false, + "security": [] + }, + "put": { + "tags": [ + "Orders" + ], + "summary": "This is used to update orders.", + "description": "", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "integer" + }, + { + "name": "body", + "in": "body", + "description": "", + "required": false, + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "order", + "description": "The order name" + }, + "amount": { + "type": "integer", + "example": 1, + "description": "Data amount" + }, + "description": { + "type": "string", + "example": "fast order", + "description": "The order description" + } + }, + "required": [ + "name", + "description" + ] + } + } + } + } + ], + "responses": { + "200": { + "description": "Update an order", + "schema": { + "type": "object", + "properties": {} + }, + "headers": { + "Content-Type": { + "type": "string", + "x-example-value": "application/json" + }, + "Content-Length": { + "type": "string", + "x-example-value": "63" + } + }, + "examples": { + "application/json": { + "data": { + "name": "order", + "amount": 1, + "description": "fast order" + } + } + } + }, + "400": { + "description": "Invalid request", + "schema": { + "type": "object", + "properties": {} + }, + "headers": { + "Content-Type": { + "type": "string", + "x-example-value": "application/json" + }, + "Content-Length": { + "type": "string", + "x-example-value": "0" + } + }, + "examples": {} + } + }, + "deprecated": false, + "security": [] + }, + "delete": { + "tags": [ + "Orders" + ], + "summary": "This is used to delete orders.", + "description": "", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "produces": [ + "text/html" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "integer" + } + ], + "responses": { + "200": { + "description": "Deleting an order", + "schema": { + "type": "object", + "properties": {} + }, + "headers": { + "Content-Type": { + "type": "string", + "x-example-value": "text/html;charset=utf-8" + }, + "Content-Length": { + "type": "string", + "x-example-value": "0" + } + }, + "examples": {} + } + }, + "deprecated": false, + "security": [] + } + } + }, + "tags": [ + { + "name": "Orders", + "description": "Order's tag description" + }, + { + "name": "Instructions", + "description": "Instructions help the users use the app." + } + ] +} + """ + + Scenario: Example 'Deleting an order' file should not be created + Then a file named "doc/api/orders/deleting_an_order.apib" should not exist + + Scenario: Example 'Getting a list of orders' file should be created + Then a file named "doc/api/orders/getting_a_list_of_orders.apib" should not exist + + Scenario: Example 'Getting a specific order' file should be created + Then a file named "doc/api/orders/getting_a_specific_order.apib" should not exist + + Scenario: Example 'Updating an order' file should be created + Then a file named "doc/api/orders/updating_an_order.apib" should not exist + + Scenario: Example 'Getting welcome message' file should be created + Then a file named "doc/api/help/getting_welcome_message.apib" should not exist 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/features/redefining_same_client.feature b/features/redefining_same_client.feature new file mode 100644 index 00000000..09f7a233 --- /dev/null +++ b/features/redefining_same_client.feature @@ -0,0 +1,33 @@ +Feature: Redefining the client method to the same method + Background: + Given a file named "app.rb" with: + """ + class App + def self.call(env) + [200, {}, ["Hello, world"]] + end + end + """ + And a file named "app_spec.rb" with: + """ + require "rspec_api_documentation" + require "rspec_api_documentation/dsl" + + RspecApiDocumentation.configure do |config| + config.app = App + config.client_method = :client + end + + resource "Example Request" do + get "/" do + example_request "Trying out get" do + expect(status).to eq(200) + end + end + end + """ + When I run `rspec app_spec.rb --require ./app.rb --format RspecApiDocumentation::ApiFormatter` + + Scenario: Output should have the correct error line + Then the output should contain "1 example, 0 failures" + And the exit status should be 0 diff --git a/features/slate_documentation.feature b/features/slate_documentation.feature index 0a3531b0..0dcd026c 100644 --- a/features/slate_documentation.feature +++ b/features/slate_documentation.feature @@ -49,7 +49,7 @@ Feature: Generate Slate documentation from test examples RspecApiDocumentation.configure do |config| config.app = App config.api_name = "Example API" - config.api_explanation = "Description" + config.api_explanation = "An explanation of the API" config.format = :slate config.curl_host = 'http://localhost:3000' config.request_headers_to_include = %w[Content-Type Host] @@ -57,6 +57,7 @@ Feature: Generate Slate documentation from test examples end resource 'Orders' do + explanation "An Order represents an amount of money to be paid" get '/orders' do response_field :page, "Current page" @@ -152,30 +153,36 @@ Feature: Generate Slate documentation from test examples ### Request + ```shell + curl -g "http://localhost:3000/orders" -X GET \ + -H "Host: example.org" \ + -H "Cookie: " + ``` + #### Endpoint + `GET /orders` + ```plaintext GET /orders Host: example.org ``` - `GET /orders` - #### Parameters - None known. + None known. ### Response + ```plaintext Content-Type: application/json Content-Length: 137 200 OK ``` - ```json { "page": 1, @@ -195,40 +202,38 @@ Feature: Generate Slate documentation from test examples ``` - #### Fields | Name | Description | |:-----------|:--------------------| | page | Current page | - - - ```shell - curl -g "http://localhost:3000/orders" -X GET \ - -H "Host: example.org" \ - -H "Cookie: " """ Scenario: Example 'Creating an order' docs should look like we expect Then the file "doc/api/index.html.md" should contain: """ - # Orders - ## Creating an order ### Request + + ```shell + curl "http://localhost:3000/orders" -d 'name=Order+3&amount=33.0' -X POST \ + -H "Host: example.org" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -H "Cookie: " + ``` #### Endpoint + `POST /orders` + ```plaintext POST /orders Host: example.org Content-Type: application/x-www-form-urlencoded ``` - `POST /orders` - #### Parameters @@ -236,17 +241,15 @@ Feature: Generate Slate documentation from test examples name=Order+3&amount=33.0 ``` - | Name | Description | |:-----|:------------| | name *required* | Name of order | | amount *required* | Amount paid | | description | Some comments on the order | - - ### Response + ```plaintext Content-Type: text/html;charset=utf-8 Content-Length: 0 @@ -255,13 +258,6 @@ Feature: Generate Slate documentation from test examples - - ```shell - curl "http://localhost:3000/orders" -d 'name=Order+3&amount=33.0' -X POST \ - -H "Host: example.org" \ - -H "Content-Type: application/x-www-form-urlencoded" \ - -H "Cookie: " - ``` """ Scenario: Example 'Deleting an order' docs should be created @@ -293,3 +289,9 @@ Feature: Generate Slate documentation from test examples """ ## Getting welcome message """ + + Scenario: API explanation should be included + Then the file "doc/api/index.html.md" should contain: + """ + An explanation of the API + """ diff --git a/features/step_definitions/json_steps.rb b/features/step_definitions/json_steps.rb index c04867ac..1b5733be 100644 --- a/features/step_definitions/json_steps.rb +++ b/features/step_definitions/json_steps.rb @@ -1,3 +1,5 @@ Then /^the file "(.*?)" should contain JSON exactly like:$/ do |file, exact_content| - expect(JSON.parse(read(file).join)).to eq(JSON.parse(exact_content)) + actual = JSON.dump(JSON.parse(read(file).join)) + expected = JSON.dump(JSON.parse(exact_content)) + expect(actual).to eq(expected) end diff --git a/features/support/env.rb b/features/support/env.rb index cfb550d6..86f6b8f4 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -9,3 +9,9 @@ config.match = :prefer_exact config.ignore_hidden_elements = false end + +Before('@ruby27_required') do |scenario| + if RUBY_VERSION < '2.7' + raise Cucumber::Pending, "Skipped on Ruby #{RUBY_VERSION} (requires >= 2.7)" + end +end diff --git a/lib/rspec_api_documentation.rb b/lib/rspec_api_documentation.rb index 6d6afdc2..5f07de9e 100644 --- a/lib/rspec_api_documentation.rb +++ b/lib/rspec_api_documentation.rb @@ -1,7 +1,10 @@ +require 'logger' # Ensure Logger is loaded for ActiveSupport 6.1+ compatibility with Ruby <= 2.6 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' @@ -37,6 +40,7 @@ module Writers autoload :HtmlWriter autoload :TextileWriter autoload :MarkdownWriter + autoload :JSONWriter autoload :JsonWriter autoload :AppendJsonWriter autoload :JsonIodocsWriter @@ -45,6 +49,31 @@ module Writers autoload :CombinedJsonWriter autoload :SlateWriter autoload :ApiBlueprintWriter + autoload :OpenApiWriter + end + + module OpenApi + extend ActiveSupport::Autoload + + autoload :Helper + autoload :Node + autoload :Root + autoload :Info + autoload :Contact + autoload :License + autoload :Paths + autoload :Path + autoload :Tag + autoload :Operation + autoload :Parameter + autoload :Responses + autoload :Response + autoload :Example + autoload :Headers + autoload :Header + autoload :Schema + autoload :SecurityDefinitions + autoload :SecuritySchema end module Views diff --git a/lib/rspec_api_documentation/api_documentation.rb b/lib/rspec_api_documentation/api_documentation.rb index 2503f594..34ad0320 100644 --- a/lib/rspec_api_documentation/api_documentation.rb +++ b/lib/rspec_api_documentation/api_documentation.rb @@ -1,3 +1,5 @@ +require 'rspec_api_documentation/writers/json_iodocs_writer' + module RspecApiDocumentation class ApiDocumentation attr_reader :configuration, :index diff --git a/lib/rspec_api_documentation/api_formatter.rb b/lib/rspec_api_documentation/api_formatter.rb index 7a9f97f3..117df936 100644 --- a/lib/rspec_api_documentation/api_formatter.rb +++ b/lib/rspec_api_documentation/api_formatter.rb @@ -2,7 +2,7 @@ module RspecApiDocumentation class ApiFormatter < RSpec::Core::Formatters::BaseTextFormatter - RSpec::Core::Formatters.register self, :example_passed, :example_failed, :stop + RSpec::Core::Formatters.register self, :example_passed, :example_failed, :stop, :example_group_started def initialize(output) super @@ -19,7 +19,7 @@ def start(notification) def example_group_started(notification) super - output.puts " #{@example_group.description}" + output.puts " #{notification.group.description}" end def example_passed(example_notification) diff --git a/lib/rspec_api_documentation/client_base.rb b/lib/rspec_api_documentation/client_base.rb index d234391b..34ccbdb1 100644 --- a/lib/rspec_api_documentation/client_base.rb +++ b/lib/rspec_api_documentation/client_base.rb @@ -45,7 +45,8 @@ def process(method, path, params = {}, headers ={}) def read_request_body input = last_request.env["rack.input"] - input.rewind + return "" unless input + input.rewind if input.respond_to?(:rewind) input.read end @@ -87,12 +88,15 @@ def headers(method, path, params, request_headers) def record_response_body(response_content_type, response_body) return nil if response_body.empty? - if response_body.encoding == Encoding::ASCII_8BIT - "[binary data]" - else - formatter = RspecApiDocumentation.configuration.response_body_formatter - return formatter.call(response_content_type, response_body) + + formatter = RspecApiDocumentation.configuration.response_body_formatter + # Only force UTF-8 for text-based content types + if response_body.respond_to?(:encoding) && response_body.encoding == Encoding::ASCII_8BIT + if response_content_type && (response_content_type.include?('json') || response_content_type.include?('text')) + response_body = response_body.force_encoding(Encoding::UTF_8) + end end + formatter.call(response_content_type, response_body) end def clean_out_uploaded_data(params, request_body) diff --git a/lib/rspec_api_documentation/configuration.rb b/lib/rspec_api_documentation/configuration.rb index edb46e69..cd50524f 100644 --- a/lib/rspec_api_documentation/configuration.rb +++ b/lib/rspec_api_documentation/configuration.rb @@ -51,6 +51,14 @@ def self.add_setting(name, opts = {}) end end + add_setting :configurations_dir, :default => lambda { |config| + if defined?(Rails) + Rails.root.join('doc', 'configurations', 'api') + else + Pathname.new('doc/configurations/api') + end + } + add_setting :docs_dir, :default => lambda { |config| if defined?(Rails) Rails.root.join("doc", "api") @@ -110,7 +118,9 @@ def self.add_setting(name, opts = {}) # See RspecApiDocumentation::DSL::Endpoint#do_request add_setting :response_body_formatter, default: Proc.new { |_, _| Proc.new do |content_type, response_body| - if content_type =~ /application\/json/ + if response_body.encoding == Encoding::ASCII_8BIT + "[binary data]" + elsif content_type =~ /application\/.*json/ JSON.pretty_generate(JSON.parse(response_body)) else response_body @@ -119,6 +129,8 @@ def self.add_setting(name, opts = {}) } def client_method=(new_client_method) + return if new_client_method == client_method + RspecApiDocumentation::DSL::Resource.module_eval <<-RUBY alias :#{new_client_method} #{client_method} undef #{client_method} diff --git a/lib/rspec_api_documentation/curl.rb b/lib/rspec_api_documentation/curl.rb index 817c4f19..09eea076 100644 --- a/lib/rspec_api_documentation/curl.rb +++ b/lib/rspec_api_documentation/curl.rb @@ -69,7 +69,12 @@ def format_header(header) end def format_full_header(header, value) - formatted_value = value ? value.gsub(/"/, "\\\"") : '' + formatted_value = if value.is_a?(Numeric) + value + else + value ? value.gsub(/"/, "\\\"") : '' + end + "#{format_header(header)}: #{formatted_value}" end diff --git a/lib/rspec_api_documentation/dsl/endpoint.rb b/lib/rspec_api_documentation/dsl/endpoint.rb index dcfc4888..1b221914 100644 --- a/lib/rspec_api_documentation/dsl/endpoint.rb +++ b/lib/rspec_api_documentation/dsl/endpoint.rb @@ -38,6 +38,9 @@ def do_request(extra_params = {}) params_or_body = nil path_or_query = path + extended_parameters + extract_route_parameters! + if http_method == :get && !query_string.blank? path_or_query += "?#{query_string}" else @@ -74,6 +77,36 @@ def header(name, value) example.metadata[:headers][name] = value end + def authentication(type, value, opts = {}) + name, new_opts = + case type + when :basic then ['Authorization', opts.merge(type: type)] + when :apiKey then [opts[:name], opts.merge(type: type, in: :header)] + else raise 'Not supported type for authentication' + end + header(name, value) + example.metadata[:authentications] ||= {} + example.metadata[:authentications][name] = new_opts + end + + def extract_route_parameters! + example.metadata[:route].gsub(URL_PARAMS_REGEX) do |match| + value = + if extra_params.keys.include?($1) + extra_params[$1] + elsif respond_to?($1) + send($1) + else + match + end + extended_parameters << {name: match[1..-1], value: value, in: :path} + end + end + + def extended_parameters + example.metadata[:extended_parameters] ||= Params.new(self, example, extra_params).extended + end + def headers return unless example.metadata[:headers] example.metadata[:headers].inject({}) do |hash, (header, value)| @@ -144,6 +177,5 @@ def extra_params def delete_extra_param(key) @extra_params.delete(key.to_sym) || @extra_params.delete(key.to_s) end - end end diff --git a/lib/rspec_api_documentation/dsl/endpoint/params.rb b/lib/rspec_api_documentation/dsl/endpoint/params.rb index 037788b8..63d7d752 100644 --- a/lib/rspec_api_documentation/dsl/endpoint/params.rb +++ b/lib/rspec_api_documentation/dsl/endpoint/params.rb @@ -13,11 +13,37 @@ def initialize(example_group, example, extra_params) end def call - parameters = example.metadata.fetch(:parameters, {}).inject({}) do |hash, param| + set_param = -> hash, param { SetParam.new(self, hash, param).call + } + + example.metadata.fetch(:parameters, {}).inject({}, &set_param) + .deep_merge( + example.metadata.fetch(:attributes, {}).inject({}, &set_param) + ).deep_merge(extra_params) + end + + def extended + example.metadata.fetch(:parameters, {}).map do |param| + p = Marshal.load(Marshal.dump(param)) + p[:value] = SetParam.new(self, nil, p).value + unless p[:value] + cur = extra_params + [*p[:scope]].each { |scope| cur = cur && (cur[scope.to_sym] || cur[scope.to_s]) } + + # When the current parameter is an array of objects, we use the + # first one for the value and add a scope indicator. The + # resulting parameter name looks like +props[pictures][][id]+ + # this. + if cur.is_a?(Array) && cur.first.is_a?(Hash) + cur = cur.first + param[:scope] << '' + end + + p[:value] = cur && (cur[p[:name].to_s] || cur[p[:name].to_sym]) + end + p end - parameters.deep_merge!(extra_params) - parameters end private diff --git a/lib/rspec_api_documentation/dsl/endpoint/set_param.rb b/lib/rspec_api_documentation/dsl/endpoint/set_param.rb index f2927658..1a52c692 100644 --- a/lib/rspec_api_documentation/dsl/endpoint/set_param.rb +++ b/lib/rspec_api_documentation/dsl/endpoint/set_param.rb @@ -15,6 +15,10 @@ def call hash.deep_merge build_param_hash(key_scope || [key]) end + def value + example_group.send(method_name) if method_name + end + private attr_reader :parent, :hash, :param @@ -36,6 +40,10 @@ def custom_method_name param[:method] end + def set_value + param[:value] + end + def path_name scoped_key || key end @@ -45,15 +53,20 @@ def path_params end def method_name - @method_name ||= begin - [custom_method_name, scoped_key, key].find do |name| - name && example_group.respond_to?(name) - end + if custom_method_name + custom_method_name if example_group.respond_to?(custom_method_name) + elsif scoped_key && example_group.respond_to?(scoped_key) + scoped_key + elsif key && example_group.respond_to?(key) + key + elsif key && set_value + key end end def build_param_hash(keys) - value = keys[1] ? build_param_hash(keys[1..-1]) : example_group.send(method_name) + value = param[:value] if param.has_key?(:value) + value ||= keys[1] ? build_param_hash(keys[1..-1]) : example_group.send(method_name) { keys[0].to_s => value } end end diff --git a/lib/rspec_api_documentation/dsl/resource.rb b/lib/rspec_api_documentation/dsl/resource.rb index 1e45d4e2..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 @@ -70,6 +70,25 @@ def header(name, value) headers[name] = value end + def authentication(type, value, opts = {}) + name, new_opts = + case type + when :basic then ['Authorization', opts.merge(type: type)] + when :apiKey then [opts[:name], opts.merge(type: type, in: :header)] + else raise 'Not supported type for authentication' + end + header(name, value) + authentications[name] = new_opts + end + + def route_summary(text) + safe_metadata(:route_summary, text) + end + + def route_description(text) + safe_metadata(:route_description, text) + end + def explanation(text) safe_metadata(:resource_explanation, text) end @@ -107,6 +126,10 @@ def headers safe_metadata(:headers, {}) end + def authentications + safe_metadata(:authentications, {}) + end + def parameter_keys parameters.map { |param| param[:name] } end diff --git a/lib/rspec_api_documentation/headers.rb b/lib/rspec_api_documentation/headers.rb index d3041cde..465fe5cf 100644 --- a/lib/rspec_api_documentation/headers.rb +++ b/lib/rspec_api_documentation/headers.rb @@ -6,7 +6,7 @@ def env_to_headers(env) headers = {} env.each do |key, value| # HTTP_ACCEPT_CHARSET => Accept-Charset - if key =~ /^(HTTP_|CONTENT_TYPE)/ + if key =~ /^(HTTP_|CONTENT_TYPE)/ && key != "HTTP_VERSION" header = key.gsub(/^HTTP_/, '').split('_').map{|s| s.titleize}.join("-") headers[header] = value end diff --git a/lib/rspec_api_documentation/oauth2_mac_client.rb b/lib/rspec_api_documentation/oauth2_mac_client.rb index e5ebcb26..51aaf9e1 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 @@ -25,7 +25,13 @@ def request_headers end def response_headers - last_response.headers + if last_response.respond_to?(:headers) + last_response.headers + elsif last_response.respond_to?(:env) && last_response.env.respond_to?(:response_headers) + last_response.env.response_headers + else + {} + end end def query_string @@ -33,7 +39,11 @@ def query_string end def status - last_response.status + if last_response.respond_to?(:status) + last_response.status + else + last_response.env.status if last_response.respond_to?(:env) + end end def response_body @@ -45,7 +55,13 @@ def request_content_type end def response_content_type - last_response.content_type + if last_response.respond_to?(:content_type) + last_response.content_type + elsif last_response.respond_to?(:headers) + last_response.headers['Content-Type'] || last_response.headers['content-type'] + else + nil + end end protected @@ -71,7 +87,13 @@ def access_token @access_token ||= begin app = ProxyApp.new(self, context.app) stub_request(:any, %r{http://example\.com}).to_rack(app) - Rack::OAuth2::Client.new(options.merge(:host => "example.com", :scheme => "http")).access_token! + + # Create a Bearer access token as MAC is no longer supported + access_token = Rack::OAuth2::AccessToken::Bearer.new( + :access_token => options[:identifier] || "1" + ) + + access_token end end end diff --git a/lib/rspec_api_documentation/open_api/contact.rb b/lib/rspec_api_documentation/open_api/contact.rb new file mode 100644 index 00000000..9bd942bb --- /dev/null +++ b/lib/rspec_api_documentation/open_api/contact.rb @@ -0,0 +1,9 @@ +module RspecApiDocumentation + module OpenApi + class Contact < Node + add_setting :name + add_setting :url + add_setting :email + end + end +end diff --git a/lib/rspec_api_documentation/open_api/example.rb b/lib/rspec_api_documentation/open_api/example.rb new file mode 100644 index 00000000..c641b191 --- /dev/null +++ b/lib/rspec_api_documentation/open_api/example.rb @@ -0,0 +1,7 @@ +module RspecApiDocumentation + module OpenApi + class Example < Node + CHILD_CLASS = true + end + end +end diff --git a/lib/rspec_api_documentation/open_api/header.rb b/lib/rspec_api_documentation/open_api/header.rb new file mode 100644 index 00000000..7bf25883 --- /dev/null +++ b/lib/rspec_api_documentation/open_api/header.rb @@ -0,0 +1,12 @@ +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' + end + end +end diff --git a/lib/rspec_api_documentation/open_api/headers.rb b/lib/rspec_api_documentation/open_api/headers.rb new file mode 100644 index 00000000..6a073a14 --- /dev/null +++ b/lib/rspec_api_documentation/open_api/headers.rb @@ -0,0 +1,7 @@ +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 new file mode 100644 index 00000000..0e25ad65 --- /dev/null +++ b/lib/rspec_api_documentation/open_api/helper.rb @@ -0,0 +1,29 @@ +module RspecApiDocumentation + module OpenApi + module Helper + module_function + + def extract_type(value) + case value + when Rack::Test::UploadedFile then :file + when Array then :array + when Hash then :object + when TrueClass, FalseClass then :boolean + when Integer then :integer + when Float then :number + else :string + end + end + + def extract_items(value, opts = {}) + result = {type: extract_type(value)} + if result[:type] == :array + result[:items] = extract_items(value[0], opts) + else + opts.each { |k, v| result[k] = v if v } + end + result + end + end + end +end diff --git a/lib/rspec_api_documentation/open_api/info.rb b/lib/rspec_api_documentation/open_api/info.rb new file mode 100644 index 00000000..4c295d65 --- /dev/null +++ b/lib/rspec_api_documentation/open_api/info.rb @@ -0,0 +1,12 @@ +module RspecApiDocumentation + module OpenApi + class Info < Node + add_setting :title, :default => 'OpenAPI Specification', :required => true + add_setting :description + add_setting :termsOfService + add_setting :contact, :schema => Contact + add_setting :license, :schema => License + add_setting :version, :default => '1.0.0', :required => true + end + end +end diff --git a/lib/rspec_api_documentation/open_api/license.rb b/lib/rspec_api_documentation/open_api/license.rb new file mode 100644 index 00000000..84537526 --- /dev/null +++ b/lib/rspec_api_documentation/open_api/license.rb @@ -0,0 +1,8 @@ +module RspecApiDocumentation + module OpenApi + class License < Node + add_setting :name, :default => 'Apache 2.0', :required => true + add_setting :url, :default => 'http://www.apache.org/licenses/LICENSE-2.0.html' + end + end +end diff --git a/lib/rspec_api_documentation/open_api/node.rb b/lib/rspec_api_documentation/open_api/node.rb new file mode 100644 index 00000000..47669ab0 --- /dev/null +++ b/lib/rspec_api_documentation/open_api/node.rb @@ -0,0 +1,113 @@ +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 + + def self.add_setting(name, opts = {}) + class_settings << name + + define_method("#{name}_schema") { opts[:schema] || NilClass } + define_method("#{name}=") { |value| settings[name] = value } + define_method("#{name}") do + if settings.has_key?(name) + settings[name] + elsif !opts[:default].nil? + if opts[:default].respond_to?(:call) + opts[:default].call(self) + else + opts[:default] + end + elsif opts[:required] + raise "setting: #{name} required in #{self}" + end + end + end + + def initialize(opts = {}) + return unless 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) + else + value + end + assign_setting(name, converted) + else + public_send("#{name}=", value) if respond_to?("#{name}=") + end + end + end + + def assign_setting(name, value); public_send("#{name}=", value) unless value.nil? end + def safe_assign_setting(name, value); assign_setting(name, value) unless settings.has_key?(name) end + def setting(name); public_send(name) end + def setting_schema(name); public_send("#{name}_schema") end + def setting_exist?(name); existing_settings.include?(name) end + def existing_settings; self.class.class_settings + instance_settings end + + def add_setting(name, opts = {}) + return false if setting_exist?(name) + + instance_settings << name + + 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 + if settings.has_key?(name) + settings[name] + elsif !opts[:default].nil? + if opts[:default].respond_to?(:call) + opts[:default].call(self) + else + opts[:default] + end + elsif opts[:required] + raise "setting: #{name} required in #{self}" + end + end + end + + def as_json + existing_settings.inject({}) do |hash, name| + value = setting(name) + case + when value.is_a?(Node) + hash[name] = value.as_json unless value.hide + 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? + else + hash[name] = value + end unless value.nil? + + hash + end + end + + def settings; @settings ||= {} end + + private + + def instance_settings; @instance_settings ||= [] end + def self.class_settings; @class_settings ||= [] end + end + end +end diff --git a/lib/rspec_api_documentation/open_api/operation.rb b/lib/rspec_api_documentation/open_api/operation.rb new file mode 100644 index 00000000..deb0c797 --- /dev/null +++ b/lib/rspec_api_documentation/open_api/operation.rb @@ -0,0 +1,18 @@ +module RspecApiDocumentation + module OpenApi + 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 :parameters, :default => [], :schema => [Parameter] + add_setting :responses, :required => true, :schema => Responses + add_setting :schemes + add_setting :deprecated, :default => false + add_setting :security + end + end +end diff --git a/lib/rspec_api_documentation/open_api/parameter.rb b/lib/rspec_api_documentation/open_api/parameter.rb new file mode 100644 index 00000000..af93fb2c --- /dev/null +++ b/lib/rspec_api_documentation/open_api/parameter.rb @@ -0,0 +1,24 @@ +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 + end + end +end diff --git a/lib/rspec_api_documentation/open_api/path.rb b/lib/rspec_api_documentation/open_api/path.rb new file mode 100644 index 00000000..241bba8c --- /dev/null +++ b/lib/rspec_api_documentation/open_api/path.rb @@ -0,0 +1,13 @@ +module RspecApiDocumentation + module OpenApi + class Path < Node + add_setting :get, :schema => Operation + add_setting :put, :schema => Operation + add_setting :post, :schema => Operation + add_setting :delete, :schema => Operation + add_setting :options, :schema => Operation + add_setting :head, :schema => Operation + add_setting :patch, :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 new file mode 100644 index 00000000..b3a9efb1 --- /dev/null +++ b/lib/rspec_api_documentation/open_api/paths.rb @@ -0,0 +1,7 @@ +module RspecApiDocumentation + module OpenApi + class Paths < Node + CHILD_CLASS = Path + end + end +end diff --git a/lib/rspec_api_documentation/open_api/response.rb b/lib/rspec_api_documentation/open_api/response.rb new file mode 100644 index 00000000..6584db6f --- /dev/null +++ b/lib/rspec_api_documentation/open_api/response.rb @@ -0,0 +1,10 @@ +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 + end + end +end diff --git a/lib/rspec_api_documentation/open_api/responses.rb b/lib/rspec_api_documentation/open_api/responses.rb new file mode 100644 index 00000000..4b8c7025 --- /dev/null +++ b/lib/rspec_api_documentation/open_api/responses.rb @@ -0,0 +1,9 @@ +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 new file mode 100644 index 00000000..edaeae96 --- /dev/null +++ b/lib/rspec_api_documentation/open_api/root.rb @@ -0,0 +1,21 @@ +module RspecApiDocumentation + module OpenApi + class Root < Node + add_setting :swagger, :default => '2.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 :security + add_setting :tags, :default => [], :schema => [Tag] + add_setting :externalDocs + end + end +end diff --git a/lib/rspec_api_documentation/open_api/schema.rb b/lib/rspec_api_documentation/open_api/schema.rb new file mode 100644 index 00000000..5dd11a31 --- /dev/null +++ b/lib/rspec_api_documentation/open_api/schema.rb @@ -0,0 +1,15 @@ +module RspecApiDocumentation + module OpenApi + class Schema < Node + add_setting :format + add_setting :title + add_setting :description + add_setting :required + add_setting :enum + add_setting :type + add_setting :items + add_setting :properties + add_setting :example + 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 new file mode 100644 index 00000000..e1ddc136 --- /dev/null +++ b/lib/rspec_api_documentation/open_api/security_definitions.rb @@ -0,0 +1,7 @@ +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_schema.rb new file mode 100644 index 00000000..25218498 --- /dev/null +++ b/lib/rspec_api_documentation/open_api/security_schema.rb @@ -0,0 +1,14 @@ +module RspecApiDocumentation + module OpenApi + class SecuritySchema < 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 + end + end +end diff --git a/lib/rspec_api_documentation/open_api/tag.rb b/lib/rspec_api_documentation/open_api/tag.rb new file mode 100644 index 00000000..4d70fca8 --- /dev/null +++ b/lib/rspec_api_documentation/open_api/tag.rb @@ -0,0 +1,9 @@ +module RspecApiDocumentation + module OpenApi + class Tag < Node + add_setting :name, :required => true + add_setting :description + add_setting :externalDocs + end + end +end diff --git a/lib/rspec_api_documentation/views/api_blueprint_example.rb b/lib/rspec_api_documentation/views/api_blueprint_example.rb index 45f815a9..35e8c810 100644 --- a/lib/rspec_api_documentation/views/api_blueprint_example.rb +++ b/lib/rspec_api_documentation/views/api_blueprint_example.rb @@ -1,7 +1,7 @@ module RspecApiDocumentation module Views class ApiBlueprintExample < MarkupExample - TOTAL_SPACES_INDENTATION = 8.freeze + TOTAL_SPACES_INDENTATION = 12.freeze def initialize(example, configuration) super @@ -20,14 +20,14 @@ def parameters def requests super.map do |request| - request[:request_headers_text] = remove_utf8_for_json(request[:request_headers_text]) + request[:request_headers_text] = remove_utf8_for_json(remove_content_type(request[:request_headers_text])) request[:request_headers_text] = indent(request[:request_headers_text]) request[:request_content_type] = content_type(request[:request_headers]) request[:request_content_type] = remove_utf8_for_json(request[:request_content_type]) request[:request_body] = body_to_json(request, :request) request[:request_body] = indent(request[:request_body]) - request[:response_headers_text] = remove_utf8_for_json(request[:response_headers_text]) + request[:response_headers_text] = remove_utf8_for_json(remove_content_type(request[:response_headers_text])) request[:response_headers_text] = indent(request[:response_headers_text]) request[:response_content_type] = content_type(request[:response_headers]) request[:response_content_type] = remove_utf8_for_json(request[:response_content_type]) @@ -46,6 +46,18 @@ def extension private + # `Content-Type` header is removed because the information would be duplicated + # since it's already present in `request[:request_content_type]`. + def remove_content_type(headers) + return unless headers + headers + .split("\n") + .reject { |header| + header.start_with?('Content-Type:') + } + .join("\n") + end + def has_request?(metadata) metadata.any? do |key, value| [:request_body, :request_headers, :request_content_type].include?(key) && value diff --git a/lib/rspec_api_documentation/views/api_blueprint_index.rb b/lib/rspec_api_documentation/views/api_blueprint_index.rb index aa7a4d50..0289d349 100644 --- a/lib/rspec_api_documentation/views/api_blueprint_index.rb +++ b/lib/rspec_api_documentation/views/api_blueprint_index.rb @@ -8,7 +8,7 @@ def initialize(index, configuration) def sections super.map do |section| - routes = section[:examples].group_by { |e| "#{e.route_uri}#{e.route_optionals}" }.map do |route, examples| + routes = section[:examples].group_by { |e| "#{e.route_uri}#{e.route_optionals}#{e.route_name}" }.map do |route, examples| attrs = fields(:attributes, examples) params = fields(:parameters, examples) @@ -23,7 +23,7 @@ def sections { "has_attributes?".to_sym => attrs.size > 0, "has_parameters?".to_sym => params.size > 0, - route: route, + route: format_route(examples[0]), route_name: examples[0][:route_name], attributes: attrs, parameters: params, @@ -45,6 +45,17 @@ def examples private + # APIB follows the RFC 6570 to format URI templates. + # According to it, simple string expansion (used to perform variable + # expansion) should be represented by `{var}` and not by `/:var` + # For example `/posts/:id` should become `/posts/{id}` + # cf. https://github.com/apiaryio/api-blueprint/blob/format-1A/API%20Blueprint%20Specification.md#431-resource-section + # cf. https://tools.ietf.org/html/rfc6570#section-3.2.6 + def format_route(example) + route_uri = example[:route_uri].gsub(/:(.*?)([.\/?{]|$)/, '{\1}\2') + "#{route_uri}#{example[:route_optionals]}" + end + # APIB has both `parameters` and `attributes`. This generates a hash # with all of its properties, like name, description, required. # { @@ -63,7 +74,11 @@ def fields(property_name, examples) .uniq { |property| property[:name] } .map do |property| properties = [] - properties << "required" if property[:required] + if property[:required] == true + properties << 'required' + else + properties << 'optional' + end properties << property[:type] if property[:type] if properties.count > 0 property[:properties_description] = properties.join(", ") @@ -71,6 +86,11 @@ def fields(property_name, examples) property[:properties_description] = nil end + property[:has_default?] = true if property[:default] + property[:has_enum?] = true if property[:enum] + + property[:annotations] = property[:annotation].lines.map(&:chomp) if property[:annotation] + property[:description] = nil if description_blank?(property) property end @@ -83,7 +103,8 @@ def fields(property_name, examples) # equals the name, I assume it is blank. def description_blank?(property) !property[:description] || - property[:description].to_s.strip == property[:name].to_s.strip + property[:description].to_s.strip == property[:name].to_s.strip || + property[:description].to_s.strip == property[:name].to_s.humanize end end end diff --git a/lib/rspec_api_documentation/views/markdown_example.rb b/lib/rspec_api_documentation/views/markdown_example.rb index 3976dd9b..db16ea98 100644 --- a/lib/rspec_api_documentation/views/markdown_example.rb +++ b/lib/rspec_api_documentation/views/markdown_example.rb @@ -1,7 +1,7 @@ module RspecApiDocumentation module Views class MarkdownExample < MarkupExample - EXTENSION = 'markdown' + EXTENSION = 'md' def initialize(example, configuration) super diff --git a/lib/rspec_api_documentation/views/markup_example.rb b/lib/rspec_api_documentation/views/markup_example.rb index df2c1fdd..682075bf 100644 --- a/lib/rspec_api_documentation/views/markup_example.rb +++ b/lib/rspec_api_documentation/views/markup_example.rb @@ -3,6 +3,8 @@ module RspecApiDocumentation module Views class MarkupExample < Mustache + SPECIAL_CHARS = /[<>:"\/\\|?*]/.freeze + def initialize(example, configuration) @example = example @host = configuration.curl_host @@ -19,12 +21,11 @@ def respond_to?(method, include_private = false) end def dirname - resource_name.to_s.downcase.gsub(/\s+/, '_').gsub(":", "_") + sanitize(resource_name.to_s.downcase) end def filename - special_chars = /[<>:"\/\\|?*]/ - basename = description.downcase.gsub(/\s+/, '_').gsub(special_chars, '') + basename = sanitize(description.downcase) basename = Digest::MD5.new.update(description).to_s if basename.blank? "#{basename}.#{extension}" end @@ -87,6 +88,10 @@ def format_scope(unformatted_scope) def content_type(headers) headers && headers.fetch("Content-Type", nil) end + + def sanitize(name) + name.gsub(/\s+/, '_').gsub(SPECIAL_CHARS, '') + end end end end diff --git a/lib/rspec_api_documentation/views/slate_index.rb b/lib/rspec_api_documentation/views/slate_index.rb index f3d518ef..ea288729 100644 --- a/lib/rspec_api_documentation/views/slate_index.rb +++ b/lib/rspec_api_documentation/views/slate_index.rb @@ -1,6 +1,10 @@ module RspecApiDocumentation module Views class SlateIndex < MarkdownIndex + def initialize(index, configuration) + super + self.template_name = "rspec_api_documentation/slate_index" + end end end end diff --git a/lib/rspec_api_documentation/writers/append_json_writer.rb b/lib/rspec_api_documentation/writers/append_json_writer.rb index 5eae1f7b..69f943e7 100644 --- a/lib/rspec_api_documentation/writers/append_json_writer.rb +++ b/lib/rspec_api_documentation/writers/append_json_writer.rb @@ -5,7 +5,7 @@ module Writers class AppendJsonWriter < JsonWriter def write index_file = docs_dir.join("index.json") - if File.exists?(index_file) && (output = File.read(index_file)).length >= 2 + if File.exist?(index_file) && (output = File.read(index_file)).length >= 2 existing_index_hash = JSON.parse(output) end File.open(index_file, "w+") do |f| diff --git a/lib/rspec_api_documentation/writers/combined_json_writer.rb b/lib/rspec_api_documentation/writers/combined_json_writer.rb index 2e4111c2..fe78bb64 100644 --- a/lib/rspec_api_documentation/writers/combined_json_writer.rb +++ b/lib/rspec_api_documentation/writers/combined_json_writer.rb @@ -7,7 +7,7 @@ def write File.open(configuration.docs_dir.join("combined.json"), "w+") do |f| examples = [] index.examples.each do |rspec_example| - examples << Formatter.to_json(JsonExample.new(rspec_example, configuration)) + examples << Formatter.to_json(JSONExample.new(rspec_example, configuration)) end f.write "[" diff --git a/lib/rspec_api_documentation/writers/formatter.rb b/lib/rspec_api_documentation/writers/formatter.rb index 11c70dd8..a7d35da0 100644 --- a/lib/rspec_api_documentation/writers/formatter.rb +++ b/lib/rspec_api_documentation/writers/formatter.rb @@ -3,9 +3,9 @@ module Writers module Formatter def self.to_json(object) - JSON.pretty_generate(object.as_json) + JSON.pretty_generate(object.as_json) + "\n" end end end -end \ No newline at end of file +end diff --git a/lib/rspec_api_documentation/writers/json_iodocs_writer.rb b/lib/rspec_api_documentation/writers/json_iodocs_writer.rb index fd2f5c1d..b8414045 100644 --- a/lib/rspec_api_documentation/writers/json_iodocs_writer.rb +++ b/lib/rspec_api_documentation/writers/json_iodocs_writer.rb @@ -32,7 +32,7 @@ def sections end def examples - @index.examples.map { |example| JsonExample.new(example, @configuration) } + @index.examples.map { |example| JsonIodocsExample.new(example, @configuration) } end def as_json(opts = nil) @@ -48,7 +48,7 @@ def as_json(opts = nil) end end - class JsonExample + class JsonIodocsExample def initialize(example, configuration) @example = example end diff --git a/lib/rspec_api_documentation/writers/json_writer.rb b/lib/rspec_api_documentation/writers/json_writer.rb index 022240ad..c61c3008 100644 --- a/lib/rspec_api_documentation/writers/json_writer.rb +++ b/lib/rspec_api_documentation/writers/json_writer.rb @@ -2,19 +2,19 @@ module RspecApiDocumentation module Writers - class JsonWriter < Writer + class JSONWriter < Writer delegate :docs_dir, :to => :configuration def write File.open(docs_dir.join("index.json"), "w+") do |f| - f.write Formatter.to_json(JsonIndex.new(index, configuration)) + f.write Formatter.to_json(JSONIndex.new(index, configuration)) end write_examples end def write_examples index.examples.each do |example| - json_example = JsonExample.new(example, configuration) + json_example = JSONExample.new(example, configuration) FileUtils.mkdir_p(docs_dir.join(json_example.dirname)) File.open(docs_dir.join(json_example.dirname, json_example.filename), "w+") do |f| f.write Formatter.to_json(json_example) @@ -23,7 +23,11 @@ def write_examples end end - class JsonIndex + # https://github.com/zipmark/rspec_api_documentation/issues/382 + # backward compatibilty json for configuration of config.format + class JsonWriter < JSONWriter; end + + class JSONIndex def initialize(index, configuration) @index = index @configuration = configuration @@ -34,7 +38,7 @@ def sections end def examples - @index.examples.map { |example| JsonExample.new(example, @configuration) } + @index.examples.map { |example| JSONExample.new(example, @configuration) } end def as_json(opts = nil) @@ -61,7 +65,7 @@ def section_hash(section) end end - class JsonExample + class JSONExample def initialize(example, configuration) @example = example @host = configuration.curl_host diff --git a/lib/rspec_api_documentation/writers/markdown_writer.rb b/lib/rspec_api_documentation/writers/markdown_writer.rb index a4231cbf..9b636c56 100644 --- a/lib/rspec_api_documentation/writers/markdown_writer.rb +++ b/lib/rspec_api_documentation/writers/markdown_writer.rb @@ -1,7 +1,7 @@ module RspecApiDocumentation module Writers class MarkdownWriter < GeneralMarkupWriter - EXTENSION = 'markdown' + EXTENSION = 'md' def markup_index_class RspecApiDocumentation::Views::MarkdownIndex diff --git a/lib/rspec_api_documentation/writers/open_api_writer.rb b/lib/rspec_api_documentation/writers/open_api_writer.rb new file mode 100644 index 00000000..bb8a871f --- /dev/null +++ b/lib/rspec_api_documentation/writers/open_api_writer.rb @@ -0,0 +1,250 @@ +require 'rspec_api_documentation/writers/formatter' +require 'yaml' + +module RspecApiDocumentation + module Writers + class OpenApiWriter < Writer + FILENAME = 'open_api' + + delegate :docs_dir, :configurations_dir, to: :configuration + + def write + File.open(docs_dir.join("#{FILENAME}.json"), 'w+') do |f| + f.write Formatter.to_json(OpenApiIndex.new(index, configuration, load_config)) + end + end + + private + + def load_config + return JSON.parse(File.read("#{configurations_dir}/open_api.json")) if File.exist?("#{configurations_dir}/open_api.json") + YAML.load_file("#{configurations_dir}/open_api.yml") if File.exist?("#{configurations_dir}/open_api.yml") + end + end + + class OpenApiIndex + attr_reader :index, :configuration, :init_config + + def initialize(index, configuration, init_config) + @index = index + @configuration = configuration + @init_config = init_config + end + + def as_json + @specs = OpenApi::Root.new(init_config) + add_tags! + add_paths! + add_security_definitions! + specs.as_json + end + + private + + attr_reader :specs + + def examples + index.examples.map { |example| OpenApiExample.new(example) } + end + + def add_security_definitions! + security_definitions = OpenApi::SecurityDefinitions.new + + arr = examples.map do |example| + example.respond_to?(:authentications) ? example.authentications : nil + end.compact + + arr.each do |securities| + securities.each do |security, opts| + schema = OpenApi::SecuritySchema.new( + name: opts[:name], + description: opts[:description], + type: opts[:type], + in: opts[:in] + ) + security_definitions.add_setting security, :value => schema + end + end + specs.securityDefinitions = security_definitions unless arr.empty? + end + + def add_tags! + tags = {} + examples.each do |example| + tags[example.resource_name] ||= example.resource_explanation + end + specs.safe_assign_setting(:tags, []) + tags.each do |name, desc| + specs.tags << OpenApi::Tag.new(name: name, description: desc) unless specs.tags.any? { |tag| tag.name == name } + end + end + + def add_paths! + specs.safe_assign_setting(:paths, OpenApi::Paths.new) + examples.each do |example| + specs.paths.add_setting example.route, :value => OpenApi::Path.new + + operation = specs.paths.setting(example.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(: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(: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) + end + end + + def process_responses(responses, example) + schema = extract_schema(example.respond_to?(:response_fields) ? example.response_fields : []) + example.requests.each do |request| + response = OpenApi::Response.new( + description: example.description, + schema: schema + ) + + if request[:response_headers] + response.safe_assign_setting(:headers, OpenApi::Headers.new) + request[:response_headers].each do |header, value| + response.headers.add_setting header, :value => OpenApi::Header.new('x-example-value' => value) + end + end + + 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 if response_body + end + responses.add_setting "#{request[:response_status]}", :value => response + end + end + + def extract_schema(fields) + 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 = current[:properties][scope] + end + end + 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]} + + 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 extract_parameters(example) + parameters = example.extended_parameters.uniq { |parameter| parameter[:name] } + + extract_known_parameters(parameters.select { |p| !p[:in].nil? }) + + extract_unknown_parameters(example, parameters.select { |p| p[:in].nil? }) + end + + def extract_parameter(opts) + OpenApi::Parameter.new( + name: opts[:name], + 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 + 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)) } + end + 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) } + + body = parameters.select { |parameter| %w(body).include?(parameter[:in].to_s) } + + result.unshift( + OpenApi::Parameter.new( + name: :body, + in: :body, + description: '', + schema: extract_schema(body) + ) + ) unless body.empty? + + result + end + end + + class OpenApiExample + def initialize(example) + @example = example + end + + def method_missing(method, *args, &block) + @example.send(method, *args, &block) + end + + def respond_to?(method, include_private = false) + super || @example.respond_to?(method, include_private) + end + + def http_method + metadata[:method] + end + + def requests + super.select { |request| request[:request_method].to_s.downcase == http_method.to_s.downcase } + end + + def route + super.gsub(/:(?[^\/]+)/, '{\k}') + end + end + end +end diff --git a/lib/rspec_api_documentation/writers/slate_writer.rb b/lib/rspec_api_documentation/writers/slate_writer.rb index 61929873..58a96ce4 100644 --- a/lib/rspec_api_documentation/writers/slate_writer.rb +++ b/lib/rspec_api_documentation/writers/slate_writer.rb @@ -21,17 +21,11 @@ def markup_example_class def write File.open(configuration.docs_dir.join("#{FILENAME}.#{extension}"), 'w+') do |file| - file.write %Q{---\n} - file.write %Q{title: "#{configuration.api_name}"\n} - file.write %Q{language_tabs:\n} - file.write %Q{ - json: JSON\n} - file.write %Q{ - shell: cURL\n} - file.write %Q{---\n\n} + file.write markup_index_class.new(index, configuration).render IndexHelper.sections(index.examples, @configuration).each do |section| - file.write "# #{section[:resource_name]}\n\n" - section[:examples].sort_by!(&:description) unless configuration.keep_source_order + file.write "#{section[:resource_explanation]}\n\n" section[:examples].each do |example| markup_example = markup_example_class.new(example, configuration) diff --git a/lib/rspec_api_documentation/writers/writer.rb b/lib/rspec_api_documentation/writers/writer.rb index d70b1f2a..a00c1715 100644 --- a/lib/rspec_api_documentation/writers/writer.rb +++ b/lib/rspec_api_documentation/writers/writer.rb @@ -14,7 +14,7 @@ def self.write(index, configuration) end def self.clear_docs(docs_dir) - if File.exists?(docs_dir) + if File.exist?(docs_dir) FileUtils.rm_rf(docs_dir, :secure => true) end FileUtils.mkdir_p(docs_dir) diff --git a/rspec_api_documentation.gemspec b/rspec_api_documentation.gemspec index a7440bdc..b96fcdf7 100644 --- a/rspec_api_documentation.gemspec +++ b/rspec_api_documentation.gemspec @@ -3,7 +3,7 @@ $:.unshift lib unless $:.include?(lib) Gem::Specification.new do |s| s.name = "rspec_api_documentation" - s.version = "5.0.0" + s.version = "6.1.0" s.platform = Gem::Platform::RUBY s.authors = ["Chris Cahoon", "Sam Goldman", "Eric Oestrich"] s.email = ["chris@smartlogicsolutions.com", "sam@smartlogicsolutions.com", "eric@smartlogicsolutions.com"] @@ -15,21 +15,52 @@ Gem::Specification.new do |s| s.required_rubygems_version = ">= 1.3.6" s.add_runtime_dependency "rspec", "~> 3.0" + s.add_development_dependency "rspec", "~> 3.0" 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" + if RUBY_VERSION < '2.7' + s.add_development_dependency "bundler", ">= 1.16" + s.add_development_dependency "fakefs", "~> 0.6.0" + s.add_development_dependency "sinatra", "~> 1.4.7" + s.add_development_dependency "aruba", "~> 0.13.0" + s.add_development_dependency "capybara", "~> 2.6.2" + s.add_development_dependency "rake", "~> 10.5.0" + s.add_development_dependency "rack-test", "~> 0.6.3" + s.add_development_dependency "rack-oauth2", "~> 1.2.2" + s.add_development_dependency "webmock", "~> 3.8.3" + s.add_development_dependency "rspec-its", "~> 1.2.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.13.0" + s.add_development_dependency "gherkin", "~> 3.2.0" + s.add_development_dependency "multi_json", "~> 1.11.2" + else + s.add_development_dependency "bundler", ">= 1.16" + s.add_development_dependency "fakefs" + s.add_development_dependency "sinatra", "~> 2.0" + s.add_development_dependency "aruba" + s.add_development_dependency "capybara" + s.add_development_dependency "rake" + s.add_development_dependency "rack", "~> 2.2" + s.add_development_dependency "rack-test" + s.add_development_dependency "rack-oauth2" + s.add_development_dependency "webmock" + s.add_development_dependency "rspec-its" + s.add_development_dependency "faraday" + s.add_development_dependency "nokogiri" + s.add_development_dependency "yard" + s.add_development_dependency "inch" + s.add_development_dependency "minitest" + s.add_development_dependency "contracts" + s.add_development_dependency "gherkin" + s.add_development_dependency "multi_json" + s.add_development_dependency "webrick" + s.add_development_dependency "rackup" + end s.files = Dir.glob("lib/**/*") + Dir.glob("templates/**/*") s.require_path = "lib" diff --git a/spec/api_documentation_spec.rb b/spec/api_documentation_spec.rb index 39930cc4..dc295567 100644 --- a/spec/api_documentation_spec.rb +++ b/spec/api_documentation_spec.rb @@ -19,7 +19,7 @@ subject.clear_docs expect(File.directory?(configuration.docs_dir)).to be_truthy - expect(File.exists?(test_file)).to be_falsey + expect(File.exist?(test_file)).to be_falsey end end diff --git a/spec/api_formatter_spec.rb b/spec/api_formatter_spec.rb index 266cad8d..d0fc442a 100644 --- a/spec/api_formatter_spec.rb +++ b/spec/api_formatter_spec.rb @@ -2,7 +2,18 @@ describe RspecApiDocumentation::ApiFormatter do let(:metadata) { {} } - let(:group) { RSpec::Core::ExampleGroup.describe("Orders", metadata) } + let(:group) { + # Create an anonymous class that inherits from ExampleGroup but doesn't auto-register + Class.new(RSpec::Core::ExampleGroup) do + def self.description + "Orders" + end + + def self.metadata + {} + end + end + } let(:output) { StringIO.new } let(:formatter) { RspecApiDocumentation::ApiFormatter.new(output) } diff --git a/spec/dsl_spec.rb b/spec/dsl_spec.rb index ec6cff5f..46430107 100644 --- a/spec/dsl_spec.rb +++ b/spec/dsl_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' require 'rspec_api_documentation/dsl' require 'net/http' +require "rack/test" describe "Non-api documentation specs" do it "should not be polluted by the rspec api dsl" do |example| @@ -61,6 +62,7 @@ parameter :size, "The size of drink you want.", :required => true parameter :note, "Any additional notes about your order.", method: :custom_note parameter :name, :scope => :order, method: :custom_order_name + parameter :quantity, "The quantity of drinks you want.", value: '3' response_field :type, "The type of drink you ordered.", :scope => :order response_field :size, "The size of drink you ordered.", :scope => :order @@ -87,6 +89,7 @@ { :name => "size", :description => "The size of drink you want.", :required => true }, { :name => "note", :description => "Any additional notes about your order.", method: :custom_note }, { :name => "name", :description => "Order name", :scope => :order, method: :custom_order_name }, + { :name => "quantity", :description => "The quantity of drinks you want.", value: '3' } ] ) end @@ -113,7 +116,8 @@ "type" => "coffee", "size" => "medium", "note" => "Made in India", - "order" => { "name" => "Jakobz" } + "order" => { "name" => "Jakobz" }, + "quantity" => "3" }) end end @@ -360,9 +364,22 @@ context "#explanation" do post "/orders" do + route_summary "Route summary" + route_description "Route description" + example "Creating an order" do |example| explanation "By creating an order..." expect(example.metadata[:explanation]).to eq("By creating an order...") + expect(example.metadata[:route_summary]).to eq("Route summary") + expect(example.metadata[:route_description]).to eq("Route description") + end + + context "Nested context" do + example "Inner example" do |example| + expect(example.metadata[:explanation]).to be_nil + expect(example.metadata[:route_summary]).to eq("Route summary") + expect(example.metadata[:route_description]).to eq("Route description") + end end end end @@ -396,6 +413,36 @@ do_request end end + + context "with reserved name parameter" do + context "without custom method name" do + parameter :status, "Filter order by status" + + example "does not work as expected" do + expect { do_request }.to raise_error Rack::Test::Error, /No response yet/ + end + end + + context "with custom method name" do + parameter :status, "Filter order by status", method: :status_param + + context "when parameter value is not specified" do + example "does not serialize param" do + expect(client).to receive(method).with("/orders", anything, anything) + do_request + end + end + + context "when parameter value is specified" do + let(:status_param) { "pending" } + + example "serializes param" do + expect(client).to receive(method).with("/orders?status=pending", anything, anything) + do_request + end + end + end + end end end @@ -539,6 +586,77 @@ end end + context "authentications" do + put "/orders" do + authentication :apiKey, "Api Key", :name => "API_AUTH" + authentication :basic, "Api Key" + + it "should be sent with the request" do |example| + expect(example.metadata[:authentications]).to eq( + { + "API_AUTH" => { + :in => :header, + :type => :apiKey, + :name => "API_AUTH" + }, + "Authorization" => { + :type => :basic + } + }) + end + + context "nested authentications" do + authentication :apiKey, "Api Key", :name => "API_AUTH" + + it "does not affect the outer context's assertions" do + # pass + end + end + end + + put "/orders" do + context "setting authentication in example level" do + before do + authentication :apiKey, "Api Key", :name => "API_AUTH" + end + + it "adds to headers" do |example| + expect(example.metadata[:authentications]).to eq({"API_AUTH" => { + :in => :header, + :type => :apiKey, + :name => "API_AUTH" + }}) + end + end + end + + put "/orders" do + authentication :apiKey, :api_key, :name => "API_AUTH" + + let(:api_key) { "API_KEY_TOKEN" } + + it "should be sent with the request" do |example| + expect(example.metadata[:authentications]).to eq({"API_AUTH" => { + :in => :header, + :type => :apiKey, + :name => "API_AUTH" + }}) + end + + it "should fill out into the headers" do + expect(headers).to eq({ "API_AUTH" => "API_KEY_TOKEN" }) + end + + context "nested authentications" do + authentication :apiKey, :api_key, :name => "API_AUTH" + + it "does not affect the outer context's assertions" do + expect(headers).to eq({ "API_AUTH" => "API_KEY_TOKEN" }) + end + end + end + end + context "post body formatter" do after do RspecApiDocumentation.instance_variable_set(:@configuration, RspecApiDocumentation::Configuration.new) @@ -637,8 +755,30 @@ end end end + + get "parameter with custom method only" do + parameter :custom, "Custom name parameter", method: :custom_method, scope: :some + + context do + let(:custom) { "Should not be taken" } + let(:some_custom) { "Should not be taken" } + + it "not uses custom as value" do + expect(params).to eq({}) + end + end + + context do + let(:custom_method) { "Should be taken" } + + it "uses custom_method as value" do + expect(params).to eq("some" => {"custom" => "Should be taken"}) + end + end + end end + resource "top level parameters" do parameter :page, "Current page" diff --git a/spec/example_spec.rb b/spec/example_spec.rb index 1aa94610..3d78b885 100644 --- a/spec/example_spec.rb +++ b/spec/example_spec.rb @@ -64,7 +64,7 @@ end context "when the example is pending" do - let(:rspec_example) { rspec_example_group.pending(description, metadata) {} } + let(:rspec_example) { rspec_example_group.pending(description, metadata) { raise "Pending example" } } it { should be_falsey } end diff --git a/spec/fixtures/open_api.yml b/spec/fixtures/open_api.yml new file mode 100644 index 00000000..6ba6ab9d --- /dev/null +++ b/spec/fixtures/open_api.yml @@ -0,0 +1,296 @@ +swagger: '2.0' +info: + title: OpenAPI App + description: This is a sample server Petstore server. + termsOfService: 'http://open-api.io/terms/' + contact: + name: API Support + url: 'http://www.open-api.io/support' + email: support@open-api.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' + version: 1.0.1 +host: 'localhost:3000' +schemes: + - http + - https +consumes: + - application/json + - application/xml +produces: + - application/json + - application/xml +paths: + /orders: + get: + tags: + - Orders + summary: Getting a list of orders + description: '' + consumes: + - application/json + produces: + - application/json + parameters: + - name: page + in: query + description: Current page of orders + required: false + type: integer + responses: + '200': + description: OK + schema: + description: '' + type: object + properties: {} + headers: {} + examples: + application/json: + - id: 1 + name: Old Name + paid: true + email: email@example.com + created_at: '2017-06-12T14:14:50.481Z' + updated_at: '2017-06-12T14:14:50.481Z' + - id: 2 + name: Old Name + paid: true + email: email@example.com + created_at: '2017-06-12T14:14:56.938Z' + updated_at: '2017-06-12T14:14:56.938Z' + - id: 3 + name: Order 0 + paid: true + email: email0@example.com + created_at: '2017-06-13T13:17:38.719Z' + updated_at: '2017-06-13T13:17:38.719Z' + - id: 4 + name: Order 1 + paid: true + email: email1@example.com + created_at: '2017-06-13T13:17:38.729Z' + updated_at: '2017-06-13T13:17:38.729Z' + deprecated: false + security: + - AUTH_TOKEN: [] + post: + tags: + - Orders + summary: Creating an order + description: '' + consumes: + - application/json + produces: + - application/json + parameters: + - name: body + in: body + description: '' + required: false + schema: + description: '' + type: object + properties: + order: + type: object + properties: + name: + type: string + paid: + type: boolean + email: + type: string + required: + - name + responses: + '201': + description: Created + schema: + description: '' + type: object + properties: + order: + type: object + properties: + name: + type: string + paid: + type: boolean + email: + type: string + headers: {} + examples: + application/json: + id: 3 + name: Order 1 + paid: true + email: email@example.com + created_at: '2017-06-13T13:17:38.825Z' + updated_at: '2017-06-13T13:17:38.825Z' + deprecated: false + security: [] + head: + tags: + - Orders + summary: Getting the headers + description: '' + consumes: + - application/json + produces: + - application/json + parameters: [] + responses: + '200': + description: OK + schema: + description: '' + type: object + properties: {} + headers: {} + examples: {} + deprecated: false + security: + - AUTH_TOKEN: [] + '/orders/{id}': + get: + tags: + - Orders + summary: Getting a specific order + description: '' + consumes: + - application/json + produces: + - application/json + parameters: + - name: id + in: path + description: '' + required: true + type: integer + responses: + '200': + description: OK + schema: + description: '' + type: object + properties: {} + headers: {} + examples: + application/json: + id: 3 + name: Old Name + paid: true + email: email@example.com + created_at: '2017-06-13T13:17:38.862Z' + updated_at: '2017-06-13T13:17:38.862Z' + deprecated: false + security: [] + put: + tags: + - Orders + summary: Updating an order + description: '' + consumes: + - application/json + produces: [] + parameters: + - name: id + in: path + description: '' + required: true + type: integer + - name: body + in: body + description: '' + required: false + schema: + description: '' + type: object + properties: + order: + type: object + properties: + name: + type: string + paid: + type: string + email: + type: string + responses: + '204': + description: No Content + schema: + description: '' + type: object + properties: {} + headers: {} + deprecated: false + security: [] + delete: + tags: + - Orders + summary: Deleting an order + description: '' + consumes: + - application/json + produces: [] + parameters: + - name: id + in: path + description: '' + required: true + type: integer + responses: + '204': + description: No Content + schema: + description: '' + type: object + properties: {} + headers: {} + deprecated: false + security: [] + /uploads: + post: + tags: + - Uploads + summary: Uploading a new file + description: '' + consumes: + - multipart/form-data + produces: + - text/html + parameters: + - name: file + in: formData + description: New file to upload + required: false + type: file + responses: + '201': + description: Created + schema: + description: '' + type: object + properties: {} + headers: {} + examples: {} + deprecated: false + security: + - Authorization: [] +securityDefinitions: + AUTH_TOKEN: + type: apiKey + description: '' + name: AUTH_TOKEN + in: header + Authorization: + type: basic + description: Api Key description +tags: + - name: Orders + description: Orders are top-level business objects + - name: Uploads + description: '' diff --git a/spec/http_test_client_spec.rb b/spec/http_test_client_spec.rb index e2282371..a95e54dc 100644 --- a/spec/http_test_client_spec.rb +++ b/spec/http_test_client_spec.rb @@ -2,21 +2,21 @@ require 'rack/test' require 'capybara' require 'capybara/server' -require 'sinatra/base' -require 'webmock' +require 'webmock/rspec' require 'support/stub_app' describe RspecApiDocumentation::HttpTestClient do before(:all) do + if RUBY_VERSION < '2.7' + skip("Skipped on Ruby #{RUBY_VERSION} (requires >= 2.7)") + end WebMock.allow_net_connect! - - Capybara.server do |app, port| - require 'rack/handler/thin' - Thin::Logging.silent = true - Rack::Handler::Thin.run(app, :Port => port) + # Capybara.server= was introduced in later versions + # For older versions, we use the Capybara::Server directly with webrick + if Capybara.respond_to?(:server=) + Capybara.server = :webrick end - - server = Capybara::Server.new(StubApp.new, 8888) + server = Capybara::Server.new(StubApp.new, port: 8888) server.boot end diff --git a/spec/open_api/info_spec.rb b/spec/open_api/info_spec.rb new file mode 100644 index 00000000..38360fb2 --- /dev/null +++ b/spec/open_api/info_spec.rb @@ -0,0 +1,11 @@ +require 'spec_helper' + +describe RspecApiDocumentation::OpenApi::Info do + let(:node) { RspecApiDocumentation::OpenApi::Info.new } + subject { node } + + describe "default settings" do + its(:title) { should == 'OpenAPI Specification' } + its(:version) { should == '1.0.0' } + end +end diff --git a/spec/open_api/license_spec.rb b/spec/open_api/license_spec.rb new file mode 100644 index 00000000..7fb887e3 --- /dev/null +++ b/spec/open_api/license_spec.rb @@ -0,0 +1,11 @@ +require 'spec_helper' + +describe RspecApiDocumentation::OpenApi::License do + let(:node) { RspecApiDocumentation::OpenApi::License.new } + subject { node } + + describe "default settings" do + its(:name) { should == 'Apache 2.0' } + its(:url) { should == 'http://www.apache.org/licenses/LICENSE-2.0.html' } + end +end diff --git a/spec/open_api/node_spec.rb b/spec/open_api/node_spec.rb new file mode 100644 index 00000000..1e6db70e --- /dev/null +++ b/spec/open_api/node_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +describe RspecApiDocumentation::OpenApi::Node do + let(:node) { RspecApiDocumentation::OpenApi::Node.new } + its(:settings) { should == {} } + + describe ".add_setting" do + it "should allow creating a new setting" do + RspecApiDocumentation::OpenApi::Node.add_setting :new_setting + expect(node).to respond_to(:new_setting) + expect(node).to respond_to(:new_setting=) + end + + it "should allow setting a default" do + RspecApiDocumentation::OpenApi::Node.add_setting :new_setting, :default => "default" + expect(node.new_setting).to eq("default") + end + + it "should allow the default setting to be a lambda" do + RspecApiDocumentation::OpenApi::Node.add_setting :another_setting, :default => lambda { |config| config.new_setting } + expect(node.another_setting).to eq("default") + end + + it "should allow setting a schema" do + RspecApiDocumentation::OpenApi::Node.add_setting :schema_setting, :schema => String + expect(node.schema_setting_schema).to eq(String) + end + + context "setting can be required" do + it "should raise error without value and default option" do + RspecApiDocumentation::OpenApi::Node.add_setting :required_setting, :required => true + expect { node.required_setting }.to raise_error RuntimeError + end + + it "should not raise error with default option" do + RspecApiDocumentation::OpenApi::Node.add_setting :required_setting, :required => true, :default => "value" + expect(node.required_setting).to eq("value") + end + + it "should not raise error with value and without default option" do + RspecApiDocumentation::OpenApi::Node.add_setting :required_setting, :required => true + node.required_setting = "value" + expect(node.required_setting).to eq("value") + end + end + end +end diff --git a/spec/open_api/root_spec.rb b/spec/open_api/root_spec.rb new file mode 100644 index 00000000..52debbbf --- /dev/null +++ b/spec/open_api/root_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' +require 'yaml' +require 'json' + +describe RspecApiDocumentation::OpenApi::Root do + let(:node) { RspecApiDocumentation::OpenApi::Root.new } + subject { node } + + 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) } + its(:host) { should == 'localhost:3000' } + its(:basePath) { should be_nil } + 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(:securityDefinitions) { should be_nil } + its(:security) { should be_nil } + its(:tags) { should == [] } + its(:externalDocs) { should be_nil } + end + + describe ".new" do + it "should allow initializing from hash" do + hash = YAML.load_file(File.expand_path('../../fixtures/open_api.yml', __FILE__)) + root = described_class.new(hash) + + expect(JSON.parse(JSON.generate(root.as_json))).to eq(hash) + end + end +end diff --git a/spec/rack_test_client_spec.rb b/spec/rack_test_client_spec.rb index e3a9b53c..f1b57b84 100644 --- a/spec/rack_test_client_spec.rb +++ b/spec/rack_test_client_spec.rb @@ -1,10 +1,9 @@ require 'spec_helper' require 'rack/test' -require 'sinatra/base' require 'support/stub_app' describe RspecApiDocumentation::RackTestClient do - let(:context) { |example| double(:app => StubApp, :example => example) } + let(:context) { |example| double(:app => StubApp.new, :example => example) } let(:test_client) { RspecApiDocumentation::RackTestClient.new(context, {}) } subject { test_client } diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 918dd620..95fd852d 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -4,4 +4,16 @@ require 'pry' RSpec.configure do |config| + config.before(:all) do + if self.class.metadata[:api_doc_dsl] || self.respond_to?(:app) + begin + require 'support/stub_app' + RspecApiDocumentation.configure do |config| + config.app = StubApp.new unless config.app + end + rescue LoadError + # StubApp not available, skip + end + end + end end diff --git a/spec/support/stub_app.rb b/spec/support/stub_app.rb index 35226be2..8f2a334f 100644 --- a/spec/support/stub_app.rb +++ b/spec/support/stub_app.rb @@ -1,31 +1,29 @@ -class StubApp < Sinatra::Base - get "/" do - content_type :json +class StubApp + def call(env) + req = Rack::Request.new(env) - { :hello => "world" }.to_json - end + case "#{req.request_method} #{req.path_info}" + when "GET /" + [200, {'Content-Type' => 'application/json'}, [{ :hello => "world" }.to_json]] + when "POST /greet" + body = req.body.read + req.body.rewind if req.body.respond_to?(:rewind) - post "/greet" do - content_type :json + begin + data = JSON.parse(body) if body && !body.empty? + rescue JSON::ParserError + data = nil + end - request.body.rewind - begin - data = JSON.parse request.body.read - rescue JSON::ParserError - request.body.rewind - data = request.body.read + target = data.is_a?(Hash) ? data["target"] : "nurse" + [200, {'Content-Type' => 'application/json', 'Content-Length' => '17'}, [{ :hello => target }.to_json]] + when "GET /xml" + [200, {'Content-Type' => 'application/xml'}, ["World"]] + when "GET /binary" + [200, {'Content-Type' => 'application/octet-stream'}, ["\x01\x02\x03".force_encoding(Encoding::ASCII_8BIT)]] + else + [404, {'Content-Type' => 'text/plain'}, ["Not Found"]] end - { :hello => data["target"] }.to_json - end - - get "/xml" do - content_type :xml - - "World" - end - - get '/binary' do - content_type 'application/octet-stream' - "\x01\x02\x03".force_encoding(Encoding::ASCII_8BIT) end end + diff --git a/spec/views/api_blueprint_example_spec.rb b/spec/views/api_blueprint_example_spec.rb index 427ef7d0..fb70a5da 100644 --- a/spec/views/api_blueprint_example_spec.rb +++ b/spec/views/api_blueprint_example_spec.rb @@ -57,17 +57,9 @@ describe 'request_headers_text' do subject { view.requests[0][:request_headers_text] } - context 'when charset=utf-8 is present' do - it "just strips that because it's the default for json" do - expect(subject).to eq "Content-Type: application/json\n Another: header; charset=utf-8" - end - end - - context 'when charset=utf-16 is present' do - let(:content_type) { "application/json; charset=utf-16" } - - it "keeps that because it's NOT the default for json" do - expect(subject).to eq "Content-Type: application/json; charset=utf-16\n Another: header; charset=utf-8" + context 'when Content-Type is present' do + it "removes it" do + expect(subject).to eq "Another: header; charset=utf-8" end end end @@ -93,17 +85,9 @@ describe 'response_headers_text' do subject { view.requests[0][:response_headers_text] } - context 'when charset=utf-8 is present' do - it "just strips that because it's the default for json" do - expect(subject).to eq "Content-Type: application/json\n Another: header; charset=utf-8" - end - end - - context 'when charset=utf-16 is present' do - let(:content_type) { "application/json; charset=utf-16" } - - it "keeps that because it's NOT the default for json" do - expect(subject).to eq "Content-Type: application/json; charset=utf-16\n Another: header; charset=utf-8" + context 'when Content-Type is present' do + it "removes it" do + expect(subject).to eq "Another: header; charset=utf-8" end end end diff --git a/spec/views/api_blueprint_index_spec.rb b/spec/views/api_blueprint_index_spec.rb index 92b4e21c..1d526597 100644 --- a/spec/views/api_blueprint_index_spec.rb +++ b/spec/views/api_blueprint_index_spec.rb @@ -109,7 +109,7 @@ post_examples = post_route[:http_methods].map { |http_method| http_method[:examples] }.flatten expect(post_examples.size).to eq 2 - expect(post_route[:route]).to eq "/posts/:id" + expect(post_route[:route]).to eq "/posts/{id}" expect(post_route[:route_name]).to eq "Single Post" expect(post_route[:has_parameters?]).to eq true expect(post_route[:parameters]).to eq [{ @@ -130,7 +130,7 @@ post_w_optionals_examples = post_route_with_optionals[:http_methods].map { |http_method| http_method[:examples] }.flatten expect(post_w_optionals_examples.size).to eq 1 - expect(post_route_with_optionals[:route]).to eq "/posts/:id{?option=:option}" + expect(post_route_with_optionals[:route]).to eq "/posts/{id}{?option=:option}" expect(post_route_with_optionals[:route_name]).to eq "Single Post" expect(post_route_with_optionals[:has_parameters?]).to eq true expect(post_route_with_optionals[:parameters]).to eq [{ @@ -143,7 +143,7 @@ }, { name: "option", description: nil, - properties_description: nil + properties_description: 'optional' }] expect(post_route_with_optionals[:has_attributes?]).to eq false expect(post_route_with_optionals[:attributes]).to eq [] @@ -159,7 +159,7 @@ required: false, name: "description", description: nil, - properties_description: nil + properties_description: "optional" }] end end diff --git a/spec/views/html_example_spec.rb b/spec/views/html_example_spec.rb index 5f3bb1e3..55bb2e2f 100644 --- a/spec/views/html_example_spec.rb +++ b/spec/views/html_example_spec.rb @@ -28,6 +28,14 @@ end end + context "when resource name contains special characters for Windows OS" do + let(:metadata) { { :resource_name => 'foo<>:"/\|?*bar' } } + + it "removes them" do + expect(html_example.dirname).to eq("foobar") + end + end + describe "multi-character example name" do let(:metadata) { { :resource_name => "オーダ" } } let(:label) { "Coffee / Teaが順番で並んでいること" } diff --git a/spec/writers/html_writer_spec.rb b/spec/writers/html_writer_spec.rb index 72dc5615..76db414e 100644 --- a/spec/writers/html_writer_spec.rb +++ b/spec/writers/html_writer_spec.rb @@ -27,7 +27,7 @@ writer.write index_file = File.join(configuration.docs_dir, "index.html") - expect(File.exists?(index_file)).to be_truthy + expect(File.exist?(index_file)).to be_truthy end end end diff --git a/spec/writers/json_example_spec.rb b/spec/writers/json_example_spec.rb index edcf1f68..0bbeb0c3 100644 --- a/spec/writers/json_example_spec.rb +++ b/spec/writers/json_example_spec.rb @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- require 'spec_helper' +require 'rspec_api_documentation/writers/json_writer' -describe RspecApiDocumentation::Writers::JsonExample do +describe RspecApiDocumentation::Writers::JSONExample do let(:configuration) { RspecApiDocumentation::Configuration.new } describe "#dirname" do @@ -9,7 +10,7 @@ example = double(resource_name: "/test_string") json_example = - RspecApiDocumentation::Writers::JsonExample.new(example, configuration) + RspecApiDocumentation::Writers::JSONExample.new(example, configuration) expect(json_example.dirname).to eq "test_string" end @@ -18,7 +19,7 @@ example = double(resource_name: "test_string/test") json_example = - RspecApiDocumentation::Writers::JsonExample.new(example, configuration) + RspecApiDocumentation::Writers::JSONExample.new(example, configuration) expect(json_example.dirname).to eq "test_string/test" end diff --git a/spec/writers/json_iodocs_writer_spec.rb b/spec/writers/json_iodocs_writer_spec.rb index bfee639c..116ccab1 100644 --- a/spec/writers/json_iodocs_writer_spec.rb +++ b/spec/writers/json_iodocs_writer_spec.rb @@ -25,7 +25,7 @@ it "should write the index" do writer.write index_file = File.join(configuration.docs_dir, "apiconfig.json") - expect(File.exists?(index_file)).to be_truthy + expect(File.exist?(index_file)).to be_truthy end end end diff --git a/spec/writers/json_writer_spec.rb b/spec/writers/json_writer_spec.rb index 4f36d506..973b52b1 100644 --- a/spec/writers/json_writer_spec.rb +++ b/spec/writers/json_writer_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe RspecApiDocumentation::Writers::JsonWriter do +describe RspecApiDocumentation::Writers::JSONWriter do let(:index) { RspecApiDocumentation::Index.new } let(:configuration) { RspecApiDocumentation::Configuration.new } @@ -24,7 +24,7 @@ it "should write the index" do writer.write index_file = File.join(configuration.docs_dir, "index.json") - expect(File.exists?(index_file)).to be_truthy + expect(File.exist?(index_file)).to be_truthy end end end diff --git a/spec/writers/markdown_writer_spec.rb b/spec/writers/markdown_writer_spec.rb index 95950898..313a51d9 100644 --- a/spec/writers/markdown_writer_spec.rb +++ b/spec/writers/markdown_writer_spec.rb @@ -26,8 +26,8 @@ FileUtils.mkdir_p(configuration.docs_dir) writer.write - index_file = File.join(configuration.docs_dir, "index.markdown") - expect(File.exists?(index_file)).to be_truthy + index_file = File.join(configuration.docs_dir, "index.md") + expect(File.exist?(index_file)).to be_truthy end end end diff --git a/spec/writers/open_api_writer_spec.rb b/spec/writers/open_api_writer_spec.rb new file mode 100644 index 00000000..f288c5fc --- /dev/null +++ b/spec/writers/open_api_writer_spec.rb @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +require 'spec_helper' + +describe RspecApiDocumentation::Writers::OpenApiWriter do + let(:index) { RspecApiDocumentation::Index.new } + let(:configuration) { RspecApiDocumentation::Configuration.new } + + describe '.write' do + let(:writer) { double(:writer) } + + it 'should build a new writer and write the docs' do + allow(described_class).to receive(:new).with(index, configuration).and_return(writer) + expect(writer).to receive(:write) + described_class.write(index, configuration) + end + end +end diff --git a/spec/writers/slate_writer_spec.rb b/spec/writers/slate_writer_spec.rb index 603be2ef..acd91047 100644 --- a/spec/writers/slate_writer_spec.rb +++ b/spec/writers/slate_writer_spec.rb @@ -22,12 +22,12 @@ FakeFS do template_dir = File.join(configuration.template_path, "rspec_api_documentation") FileUtils.mkdir_p(template_dir) - File.open(File.join(template_dir, "markdown_index.mustache"), "w+") { |f| f << "{{ mustache }}" } + File.open(File.join(template_dir, "slate_index.mustache"), "w+") { |f| f << "{{ mustache }}" } FileUtils.mkdir_p(configuration.docs_dir) writer.write index_file = File.join(configuration.docs_dir, "index.html.md") - expect(File.exists?(index_file)).to be_truthy + expect(File.exist?(index_file)).to be_truthy end end end diff --git a/spec/writers/textile_writer_spec.rb b/spec/writers/textile_writer_spec.rb index 1531f7ad..2e10cb7d 100644 --- a/spec/writers/textile_writer_spec.rb +++ b/spec/writers/textile_writer_spec.rb @@ -27,7 +27,7 @@ writer.write index_file = File.join(configuration.docs_dir, "index.textile") - expect(File.exists?(index_file)).to be_truthy + expect(File.exist?(index_file)).to be_truthy end end end diff --git a/templates/rspec_api_documentation/api_blueprint_index.mustache b/templates/rspec_api_documentation/api_blueprint_index.mustache index 2955a22d..8100200e 100644 --- a/templates/rspec_api_documentation/api_blueprint_index.mustache +++ b/templates/rspec_api_documentation/api_blueprint_index.mustache @@ -1,4 +1,6 @@ -FORMAT: A1 +FORMAT: 1A +# {{ api_name }} +{{ api_explanation }} {{# sections }} # Group {{ resource_name }} @@ -26,6 +28,18 @@ explanation: {{ explanation }} + Parameters {{# parameters }} + {{ name }}{{# example }}: {{ example }}{{/ example }}{{# properties_description }} ({{ properties_description }}){{/ properties_description }}{{# description }} - {{ description }}{{/ description }} + {{# has_default?}} + + Default: `{{default}}` + {{/ has_default?}} + {{# has_enum?}} + + Members + {{# enum}} + + `{{.}}` + {{/ enum}} + {{/ has_enum?}} + {{# annotations }} + {{ . }} + {{/ annotations }} {{/ parameters }} {{/ has_parameters? }} {{# has_attributes? }} @@ -33,6 +47,18 @@ explanation: {{ explanation }} + Attributes (object) {{# attributes }} + {{ name }}{{# example }}: {{ example }}{{/ example }}{{# properties_description }} ({{ properties_description }}){{/ properties_description }}{{# description }} - {{ description }}{{/ description }} + {{# has_default?}} + + Default: `{{default}}` + {{/ has_default?}} + {{# has_enum?}} + + Members + {{# enum}} + + `{{.}}` + {{/ enum}} + {{/ has_enum?}} + {{# annotations }} + {{ . }} + {{/ annotations }} {{/ attributes }} {{/ has_attributes? }} {{# http_methods }} @@ -48,13 +74,13 @@ explanation: {{ explanation }} + Headers - {{{ request_headers_text }}} + {{{ request_headers_text }}} {{/ request_headers_text }} {{# request_body }} + Body - {{{ request_body }}} + {{{ request_body }}} {{/ request_body }} {{# has_response? }} @@ -64,13 +90,13 @@ explanation: {{ explanation }} + Headers - {{{ response_headers_text }}} + {{{ response_headers_text }}} {{/ response_headers_text }} {{# response_body }} + Body - {{{ response_body }}} + {{{ response_body }}} {{/ response_body }} {{/ requests }} {{/ examples }} diff --git a/templates/rspec_api_documentation/slate_example.mustache b/templates/rspec_api_documentation/slate_example.mustache index 01fafb9f..f07233a4 100644 --- a/templates/rspec_api_documentation/slate_example.mustache +++ b/templates/rspec_api_documentation/slate_example.mustache @@ -6,66 +6,69 @@ ### Request +{{# requests }} +{{# curl }} +```shell +{{{ curl }}} +``` +{{/ curl }} +{{/ requests }} + #### Endpoint -{{# requests}} +`{{ http_method }} {{ route }}` + +{{# requests }} ```plaintext {{ request_method }} {{ request_path }} {{ request_headers_text }} ``` -{{/ requests}} - -`{{ http_method }} {{ route }}` +{{/ requests }} #### Parameters -{{# requests}} +{{# requests }} {{# request_query_parameters_text }} - ```json {{ request_query_parameters_text }} ``` {{/ request_query_parameters_text }} -{{# request_body }} +{{# request_body }} ```json {{{ request_body }}} ``` {{/ request_body }} {{# has_parameters? }} - | Name | Description | |:-----|:------------| {{# parameters }} | {{#scope}}{{scope}}[{{/scope}}{{ name }}{{#scope}}]{{/scope}} {{# required }}*required*{{/ required }} | {{{ description }}} | {{/ parameters }} - {{/ has_parameters? }} {{^ has_parameters? }} None known. {{/ has_parameters? }} -{{# response_status}} - ### Response + +{{# response_status }} ```plaintext {{ response_headers_text }} {{ response_status }} {{ response_status_text}} ``` {{# response_body}} - ```json {{{ response_body }}} ``` {{/response_body}} - {{/ response_status}} -{{# has_response_fields? }} +{{# has_response_fields? }} #### Fields | Name | Description | @@ -73,12 +76,5 @@ None known. {{# response_fields }} | {{#scope}}{{scope}}[{{/scope}}{{ name }}{{#scope}}]{{/scope}} | {{{ description }}} | {{/ response_fields }} - {{/ has_response_fields? }} - -{{# curl }} -```shell -{{{ curl }}} -``` -{{/ curl }} -{{/ requests}} +{{/ requests }} \ No newline at end of file diff --git a/templates/rspec_api_documentation/slate_index.mustache b/templates/rspec_api_documentation/slate_index.mustache new file mode 100644 index 00000000..1592f604 --- /dev/null +++ b/templates/rspec_api_documentation/slate_index.mustache @@ -0,0 +1,7 @@ +--- +title: {{ api_name }} +language_tabs: + - shell: cURL +--- + +{{{ api_explanation }}}