├── example ├── log │ └── .keep ├── lib │ ├── tasks │ │ └── .keep │ └── assets │ │ └── .keep ├── app │ ├── mailers │ │ └── .keep │ ├── models │ │ ├── .keep │ │ ├── concerns │ │ │ └── .keep │ │ └── order.rb │ ├── assets │ │ ├── images │ │ │ └── .keep │ │ ├── javascripts │ │ │ └── application.js │ │ └── stylesheets │ │ │ └── application.css │ ├── controllers │ │ ├── concerns │ │ │ └── .keep │ │ ├── uploads_controller.rb │ │ ├── application_controller.rb │ │ └── orders_controller.rb │ ├── helpers │ │ └── application_helper.rb │ └── views │ │ └── layouts │ │ └── application.html.erb ├── public │ ├── favicon.ico │ ├── robots.txt │ ├── 500.html │ ├── 422.html │ └── 404.html ├── vendor │ └── assets │ │ ├── javascripts │ │ └── .keep │ │ └── stylesheets │ │ └── .keep ├── .rspec ├── spec │ ├── fixtures │ │ └── file.png │ ├── acceptance │ │ ├── uploads_spec.rb │ │ └── orders_spec.rb │ ├── acceptance_helper.rb │ ├── rails_helper.rb │ └── spec_helper.rb ├── bin │ ├── bundle │ ├── rake │ ├── rails │ └── spring ├── config.ru ├── config │ ├── initializers │ │ ├── cookies_serializer.rb │ │ ├── session_store.rb │ │ ├── mime_types.rb │ │ ├── filter_parameter_logging.rb │ │ ├── backtrace_silencers.rb │ │ ├── wrap_parameters.rb │ │ └── inflections.rb │ ├── environment.rb │ ├── routes.rb │ ├── boot.rb │ ├── database.yml │ ├── locales │ │ └── en.yml │ ├── secrets.yml │ ├── application.rb │ └── environments │ │ ├── development.rb │ │ ├── test.rb │ │ └── production.rb ├── Rakefile ├── db │ ├── migrate │ │ └── 20140616151047_create_orders.rb │ ├── seeds.rb │ └── schema.rb ├── Gemfile ├── .gitignore ├── README.rdoc └── Gemfile.lock ├── .rspec ├── Gemfile ├── features ├── fixtures │ └── file.png ├── step_definitions │ ├── json_steps.rb │ ├── image_steps.rb │ ├── curl_steps.rb │ └── html_steps.rb ├── support │ ├── env.rb │ └── capybara.rb ├── folder_structure.feature ├── redefining_client.feature ├── patch.feature ├── headers.feature ├── readme.md ├── curl.feature ├── disable_dsl.feature ├── example_request.feature ├── callbacks.feature ├── json_iodocs.feature ├── combined_text.feature ├── html_documentation.feature └── oauth2_mac_client.feature ├── .gitignore ├── lib ├── rspec_api_documentation │ ├── index.rb │ ├── views │ │ ├── slate_index.rb │ │ ├── textile_example.rb │ │ ├── textile_index.rb │ │ ├── markdown_index.rb │ │ ├── slate_example.rb │ │ ├── markup_index.rb │ │ ├── markdown_example.rb │ │ ├── html_example.rb │ │ ├── html_index.rb │ │ ├── markup_example.rb │ │ ├── api_blueprint_index.rb │ │ └── api_blueprint_example.rb │ ├── railtie.rb │ ├── writers │ │ ├── formatter.rb │ │ ├── html_writer.rb │ │ ├── textile_writer.rb │ │ ├── markdown_writer.rb │ │ ├── combined_json_writer.rb │ │ ├── writer.rb │ │ ├── api_blueprint_writer.rb │ │ ├── index_helper.rb │ │ ├── general_markup_writer.rb │ │ ├── slate_writer.rb │ │ ├── append_json_writer.rb │ │ ├── json_iodocs_writer.rb │ │ ├── combined_text_writer.rb │ │ └── json_writer.rb │ ├── dsl │ │ ├── callback.rb │ │ ├── endpoint │ │ │ ├── params.rb │ │ │ └── set_param.rb │ │ ├── resource.rb │ │ └── endpoint.rb │ ├── headers.rb │ ├── test_server.rb │ ├── api_documentation.rb │ ├── api_formatter.rb │ ├── dsl.rb │ ├── rack_test_client.rb │ ├── assets │ │ └── stylesheets │ │ │ └── rspec_api_documentation │ │ │ └── styles.css │ ├── oauth2_mac_client.rb │ ├── curl.rb │ ├── example.rb │ ├── http_test_client.rb │ └── client_base.rb ├── tasks │ └── docs.rake └── rspec_api_documentation.rb ├── spec ├── spec_helper.rb ├── index_spec.rb ├── views │ ├── slate_example_spec.rb │ ├── html_example_spec.rb │ └── api_blueprint_example_spec.rb ├── headers_spec.rb ├── support │ └── stub_app.rb ├── rspec_api_documentation_spec.rb ├── writers │ ├── json_writer_spec.rb │ ├── json_example_spec.rb │ ├── json_iodocs_writer_spec.rb │ ├── html_writer_spec.rb │ ├── textile_writer_spec.rb │ ├── markdown_writer_spec.rb │ ├── slate_writer_spec.rb │ ├── index_helper_spec.rb │ └── combined_text_example_spec.rb ├── api_formatter_spec.rb ├── api_documentation_spec.rb └── configuration_spec.rb ├── .travis.yml ├── templates └── rspec_api_documentation │ ├── markdown_index.mustache │ ├── textile_index.mustache │ ├── html_index.mustache │ ├── textile_example.mustache │ ├── slate_example.mustache │ ├── markdown_example.mustache │ ├── api_blueprint_index.mustache │ └── html_example.mustache ├── Rakefile ├── LICENSE.md ├── rspec_api_documentation.gemspec └── Gemfile.lock /example/log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --colour 2 | -------------------------------------------------------------------------------- /example/lib/tasks/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/app/mailers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/app/models/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/lib/assets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/app/assets/images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/vendor/assets/javascripts/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/vendor/assets/stylesheets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /example/app/models/order.rb: -------------------------------------------------------------------------------- 1 | class Order < ActiveRecord::Base 2 | end 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | gemspec 4 | 5 | gem 'inch' 6 | -------------------------------------------------------------------------------- /example/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /features/fixtures/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/augment/rspec_api_documentation/master/features/fixtures/file.png -------------------------------------------------------------------------------- /example/spec/fixtures/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/augment/rspec_api_documentation/master/example/spec/fixtures/file.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tmp 2 | .rvmrc 3 | .ruby-version 4 | .ruby-gemset 5 | example/docs 6 | example/public/docs 7 | *.gem 8 | *.swp 9 | /html/ 10 | -------------------------------------------------------------------------------- /example/app/controllers/uploads_controller.rb: -------------------------------------------------------------------------------- 1 | class UploadsController < ApplicationController 2 | def create 3 | head 201 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /example/bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /lib/rspec_api_documentation/index.rb: -------------------------------------------------------------------------------- 1 | module RspecApiDocumentation 2 | class Index 3 | def examples 4 | @examples ||= [] 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/rspec_api_documentation/views/slate_index.rb: -------------------------------------------------------------------------------- 1 | module RspecApiDocumentation 2 | module Views 3 | class SlateIndex < MarkdownIndex 4 | end 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rspec_api_documentation' 2 | require 'fakefs/spec_helpers' 3 | require 'rspec/its' 4 | require 'pry' 5 | 6 | RSpec.configure do |config| 7 | end 8 | -------------------------------------------------------------------------------- /example/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Rails.application 5 | -------------------------------------------------------------------------------- /example/config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.action_dispatch.cookies_serializer = :json -------------------------------------------------------------------------------- /example/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.session_store :cookie_store, key: '_example_session' 4 | -------------------------------------------------------------------------------- /example/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /example/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | resources :orders 3 | 4 | resources :uploads, :only => :create 5 | 6 | mount Raddocs::App => "/docs", :anchor => false 7 | end 8 | -------------------------------------------------------------------------------- /example/config/boot.rb: -------------------------------------------------------------------------------- 1 | # Set up gems listed in the Gemfile. 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | 4 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 5 | -------------------------------------------------------------------------------- /example/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path("../spring", __FILE__) 4 | rescue LoadError 5 | end 6 | require_relative '../config/boot' 7 | require 'rake' 8 | Rake.application.run 9 | -------------------------------------------------------------------------------- /example/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /features/step_definitions/json_steps.rb: -------------------------------------------------------------------------------- 1 | Then /^the file "(.*?)" should contain JSON exactly like:$/ do |file, exact_content| 2 | expect(JSON.parse(read(file).join)).to eq(JSON.parse(exact_content)) 3 | end 4 | -------------------------------------------------------------------------------- /lib/rspec_api_documentation/railtie.rb: -------------------------------------------------------------------------------- 1 | module RspecApiDocumentation 2 | class Railtie < Rails::Railtie 3 | rake_tasks do 4 | load File.join(File.dirname(__FILE__), '../tasks/docs.rake') 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | sudo: false 3 | rvm: 4 | - 2.0.0-p648 5 | - 2.1.8 6 | - 2.2.4 7 | - 2.3.0 8 | gemfile: 9 | - Gemfile 10 | script: 11 | - bundle exec rake 12 | branches: 13 | only: 14 | - master 15 | -------------------------------------------------------------------------------- /example/public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /example/config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /example/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | # Prevent CSRF attacks by raising an exception. 3 | # For APIs, you may want to use :null_session instead. 4 | protect_from_forgery with: :exception 5 | end 6 | -------------------------------------------------------------------------------- /example/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path("../spring", __FILE__) 4 | rescue LoadError 5 | end 6 | APP_PATH = File.expand_path('../../config/application', __FILE__) 7 | require_relative '../config/boot' 8 | require 'rails/commands' 9 | -------------------------------------------------------------------------------- /lib/rspec_api_documentation/writers/formatter.rb: -------------------------------------------------------------------------------- 1 | module RspecApiDocumentation 2 | module Writers 3 | module Formatter 4 | 5 | def self.to_json(object) 6 | JSON.pretty_generate(object.as_json) 7 | end 8 | 9 | end 10 | end 11 | end -------------------------------------------------------------------------------- /features/support/env.rb: -------------------------------------------------------------------------------- 1 | require "aruba/cucumber" 2 | require "capybara" 3 | 4 | Before do 5 | @aruba_timeout_seconds = 5 6 | end 7 | 8 | Capybara.configure do |config| 9 | config.match = :prefer_exact 10 | config.ignore_hidden_elements = false 11 | end 12 | -------------------------------------------------------------------------------- /example/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /example/db/migrate/20140616151047_create_orders.rb: -------------------------------------------------------------------------------- 1 | class CreateOrders < ActiveRecord::Migration 2 | def change 3 | create_table :orders do |t| 4 | t.string :name 5 | t.boolean :paid 6 | t.string :email 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /example/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | ruby '2.3.3' 4 | 5 | gem 'rails', '4.2.5.1' 6 | gem 'sqlite3' 7 | gem 'spring', group: :development 8 | gem 'raddocs', :github => "smartlogic/raddocs" 9 | 10 | group :test, :development do 11 | gem 'rspec-rails' 12 | gem 'rspec_api_documentation', :path => "../" 13 | end 14 | -------------------------------------------------------------------------------- /example/db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }]) 7 | # Mayor.create(name: 'Emanuel', city: cities.first) 8 | -------------------------------------------------------------------------------- /features/step_definitions/image_steps.rb: -------------------------------------------------------------------------------- 1 | Given /^I move the sample image into the workspace$/ do 2 | FileUtils.cp("features/fixtures/file.png", expand_path(".")) 3 | end 4 | 5 | Then /^the generated documentation should be encoded correctly$/ do 6 | file = File.read(File.join(expand_path("."), "doc", "api", "foobars", "uploading_a_file.html")) 7 | expect(file).to match(/file\.png/) 8 | end 9 | -------------------------------------------------------------------------------- /templates/rspec_api_documentation/markdown_index.mustache: -------------------------------------------------------------------------------- 1 | # {{ api_name }} 2 | {{{ api_explanation }}} 3 | 4 | {{# sections }} 5 | ## {{ resource_name }} 6 | {{# resource_explanation }} 7 | 8 | {{{ resource_explanation }}} 9 | {{/ resource_explanation }} 10 | 11 | {{# examples }} 12 | * [{{ description }}]({{ dirname }}/{{ filename }}) 13 | {{/ examples }} 14 | 15 | {{/ sections }} 16 | -------------------------------------------------------------------------------- /templates/rspec_api_documentation/textile_index.mustache: -------------------------------------------------------------------------------- 1 | h1. {{ api_name }} 2 | {{{ api_explanation }}} 3 | 4 | {{# sections }} 5 | h2. {{ resource_name }} 6 | {{# resource_explanation }} 7 | 8 | {{{ resource_explanation }}} 9 | {{/ resource_explanation }} 10 | 11 | {{# examples }} 12 | * "{{ description }}":{{ dirname }}/{{ filename }} 13 | {{/ examples }} 14 | 15 | {{/ sections }} 16 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | 3 | require "cucumber/rake/task" 4 | Cucumber::Rake::Task.new(:cucumber) 5 | 6 | require "rspec/core/rake_task" 7 | RSpec::Core::RakeTask.new(:spec) 8 | 9 | task :default => [:spec, :cucumber] 10 | 11 | require 'rdoc/task' 12 | Rake::RDocTask.new do |rd| 13 | rd.main = "README.md" 14 | rd.rdoc_files.include("README.md", "lib/**/*.rb") 15 | end 16 | -------------------------------------------------------------------------------- /example/spec/acceptance/uploads_spec.rb: -------------------------------------------------------------------------------- 1 | require 'acceptance_helper' 2 | 3 | resource "Uploads" do 4 | post "/uploads" do 5 | parameter :file, "New file to upload" 6 | 7 | let(:file) { Rack::Test::UploadedFile.new("spec/fixtures/file.png", "image/png") } 8 | 9 | example_request "Uploading a new file" do 10 | expect(status).to eq(201) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /example/spec/acceptance_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | require 'rspec_api_documentation' 3 | require 'rspec_api_documentation/dsl' 4 | 5 | RspecApiDocumentation.configure do |config| 6 | config.format = [:json, :combined_text, :html] 7 | config.curl_host = 'http://localhost:3000' 8 | config.api_name = "Example App API" 9 | config.api_explanation = "API Example Description" 10 | end 11 | -------------------------------------------------------------------------------- /example/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |{{{ resource_explanation }}}
20 | {{/ resource_explanation }} 21 | 22 |{{ request_headers_text }}
39 |
40 | h4. Route
41 |
42 | {{ request_method }} {{ request_path }}
43 |
44 | {{# request_query_parameters_text }}
45 | h4. Query Parameters
46 |
47 | {{ request_query_parameters_text }}
48 |
49 | {{/ request_query_parameters_text }}
50 | {{# request_body }}
51 | h4. Body
52 |
53 | {{{ request_body }}}
54 |
55 | {{/ request_body }}
56 | {{# curl }}
57 | h4. cURL
58 |
59 | {{ curl }}
60 |
61 | {{/ curl }}
62 | {{# response_status }}
63 | h3. Response
64 |
65 | h4. Headers
66 |
67 | {{ response_headers_text }}
68 |
69 | h4. Status
70 |
71 | {{ response_status }} {{ response_status_text}}
72 |
73 | {{# response_body }}
74 | h4. Body
75 |
76 | {{{ response_body }}}
77 |
78 | {{/ response_body }}
79 | {{/ response_status }}
80 | {{/ requests }}
81 |
--------------------------------------------------------------------------------
/example/public/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | If you are the application owner check the logs for more information.
64 |Maybe you tried to change something you didn't have access to.
63 |If you are the application owner check the logs for more information.
65 |You may have mistyped the address or the page may have moved.
63 |If you are the application owner check the logs for more information.
65 |{{ request_headers_text }}
42 |
43 | #### Route
44 |
45 | {{ request_method }} {{ request_path }}
46 | {{# request_query_parameters_text }}
47 |
48 | #### Query Parameters
49 |
50 | {{ request_query_parameters_text }}
51 | {{/ request_query_parameters_text }}
52 | {{# request_body }}
53 |
54 | #### Body
55 |
56 | {{{ request_body }}}
57 | {{/ request_body }}
58 | {{# curl }}
59 |
60 | #### cURL
61 |
62 | {{ curl }}
63 | {{/ curl }}
64 |
65 | {{# response_status }}
66 | ### Response
67 |
68 | #### Headers
69 |
70 | {{ response_headers_text }}
71 |
72 | #### Status
73 |
74 | {{ response_status }} {{ response_status_text}}
75 |
76 | {{# response_body }}
77 | #### Body
78 |
79 | {{{ response_body }}}
80 | {{/ response_body }}
81 | {{/ response_status }}
82 | {{/ requests }}
83 |
--------------------------------------------------------------------------------
/features/example_request.feature:
--------------------------------------------------------------------------------
1 | Feature: Example Request
2 | Background:
3 | Given a file named "app.rb" with:
4 | """
5 | class App
6 | def self.call(env)
7 | [200, {}, ["Hello, world"]]
8 | end
9 | end
10 | """
11 |
12 | Scenario: Output should have the correct error line
13 | Given a file named "app_spec.rb" with:
14 | """
15 | require "rspec_api_documentation"
16 | require "rspec_api_documentation/dsl"
17 |
18 | RspecApiDocumentation.configure do |config|
19 | config.app = App
20 | end
21 |
22 | resource "Example Request" do
23 | get "/" do
24 | example_request "Greeting your favorite gem" do
25 | expect(status).to eq(201)
26 | end
27 | end
28 | end
29 | """
30 | When I run `rspec app_spec.rb --require ./app.rb --format RspecApiDocumentation::ApiFormatter`
31 | Then the output should contain "expected: 201"
32 | Then the output should not contain "endpoint.rb"
33 | Then the output should contain:
34 | """
35 | rspec ./app_spec.rb:10 # Example Request GET / Greeting your favorite gem
36 | """
37 |
38 | Scenario: should work with RSpec monkey patching disabled
39 | When a file named "app_spec.rb" with:
40 | """
41 | require "rspec_api_documentation/dsl"
42 |
43 | RSpec.configure do |config|
44 | config.disable_monkey_patching!
45 | end
46 |
47 | RspecApiDocumentation.configure do |config|
48 | config.app = App
49 | end
50 |
51 | RSpec.resource "Example Request" do
52 | get "/" do
53 | example_request "Greeting your favorite gem" do
54 | expect(status).to eq(200)
55 | end
56 | end
57 | end
58 | """
59 | Then I successfully run `rspec app_spec.rb --require ./app.rb`
60 |
--------------------------------------------------------------------------------
/lib/rspec_api_documentation/assets/stylesheets/rspec_api_documentation/styles.css:
--------------------------------------------------------------------------------
1 |
2 | body {
3 | font-family: Helvetica,Arial,sans-serif;
4 | font-size: 13px;
5 | font-weight: normal;
6 | line-height: 18px;
7 | color: #404040;
8 | }
9 |
10 | .container {
11 | width: 940px;
12 | margin-left: auto;
13 | margin-right: auto;
14 | zoom: 1;
15 | }
16 |
17 | pre {
18 | background-color: #f5f5f5;
19 | display: block;
20 | padding: 8.5px;
21 | margin: 0 0 18px;
22 | line-height: 18px;
23 | font-size: 12px;
24 | border: 1px solid #ccc;
25 | border: 1px solid rgba(0, 0, 0, 0.15);
26 | -webkit-border-radius: 3px;
27 | -moz-border-radius: 3px;
28 | border-radius: 3px;
29 | white-space: pre;
30 | white-space: pre-wrap;
31 | word-wrap: break-word;
32 | }
33 |
34 | td.required .name:after {
35 | float: right;
36 | content: "required";
37 | font-weight: normal;
38 | color: #F08080;
39 | }
40 |
41 | a{
42 | color: #0069d6;
43 | text-decoration: none;
44 | line-height: inherit;
45 | font-weight: inherit;
46 | }
47 |
48 | h1, h2, h3, h4, h5, h6 {
49 | font-weight: bold;
50 | color: #404040;
51 | }
52 |
53 | h1 {
54 | margin-bottom: 18px;
55 | font-size: 30px;
56 | line-height: 36px;
57 | }
58 | h2 {
59 | font-size: 24px;
60 | line-height: 36px;
61 | }
62 | h3{
63 | font-size: 18px;
64 | line-height: 36px;
65 | }
66 | h4 {
67 | font-size: 16px;
68 | line-height: 36px;
69 | }
70 |
71 | table{
72 | width: 100%;
73 | margin-bottom: 18px;
74 | padding: 0;
75 | border-collapse: separate;
76 | font-size: 13px;
77 | -webkit-border-radius: 4px;
78 | -moz-border-radius: 4px;
79 | border-radius: 4px;
80 | border-spacing: 0;
81 | border: 1px solid #ddd;
82 | }
83 |
84 | table th {
85 | padding-top: 9px;
86 | font-weight: bold;
87 | vertical-align: middle;
88 | border-bottom: 1px solid #ddd;
89 | }
90 | table th+th, table td+td {
91 | border-left: 1px solid #ddd;
92 | }
93 | table th, table td {
94 | padding: 10px 10px 9px;
95 | line-height: 18px;
96 | text-align: left;
97 | }
98 |
--------------------------------------------------------------------------------
/example/spec/rails_helper.rb:
--------------------------------------------------------------------------------
1 | # This file is copied to spec/ when you run 'rails generate rspec:install'
2 | ENV["RAILS_ENV"] ||= 'test'
3 | require 'spec_helper'
4 | require File.expand_path("../../config/environment", __FILE__)
5 | require 'rspec/rails'
6 |
7 | # Requires supporting ruby files with custom matchers and macros, etc, in
8 | # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
9 | # run as spec files by default. This means that files in spec/support that end
10 | # in _spec.rb will both be required and run as specs, causing the specs to be
11 | # run twice. It is recommended that you do not name files matching this glob to
12 | # end with _spec.rb. You can configure this pattern with with the --pattern
13 | # option on the command line or in ~/.rspec, .rspec or `.rspec-local`.
14 | Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }
15 |
16 | # Checks for pending migrations before tests are run.
17 | # If you are not using ActiveRecord, you can remove this line.
18 | ActiveRecord::Migration.maintain_test_schema!
19 |
20 | RSpec.configure do |config|
21 | # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
22 | config.fixture_path = "#{::Rails.root}/spec/fixtures"
23 |
24 | # If you're not using ActiveRecord, or you'd prefer not to run each of your
25 | # examples within a transaction, remove the following line or assign false
26 | # instead of true.
27 | config.use_transactional_fixtures = true
28 |
29 | # RSpec Rails can automatically mix in different behaviours to your tests
30 | # based on their file location, for example enabling you to call `get` and
31 | # `post` in specs under `spec/controllers`.
32 | #
33 | # You can disable this behaviour by removing the line below, and instead
34 | # explicitly tag your specs with their type, e.g.:
35 | #
36 | # RSpec.describe UsersController, :type => :controller do
37 | # # ...
38 | # end
39 | #
40 | # The different available types are documented in the features, such as in
41 | # https://relishapp.com/rspec/rspec-rails/docs
42 | config.infer_spec_type_from_file_location!
43 | end
44 |
--------------------------------------------------------------------------------
/templates/rspec_api_documentation/api_blueprint_index.mustache:
--------------------------------------------------------------------------------
1 | FORMAT: A1
2 | {{# sections }}
3 |
4 | # Group {{ resource_name }}
5 | {{# resource_explanation }}
6 |
7 | {{{ resource_explanation }}}
8 | {{/ resource_explanation }}
9 | {{# description }}
10 |
11 | {{ description }}
12 | {{/ description }}
13 | {{# routes }}
14 |
15 | ## {{ route_name }} [{{ route }}]
16 | {{# description }}
17 |
18 | description: {{ description }}
19 | {{/ description }}
20 | {{# explanation }}
21 |
22 | explanation: {{ explanation }}
23 | {{/ explanation }}
24 | {{# has_parameters? }}
25 |
26 | + Parameters
27 | {{# parameters }}
28 | + {{ name }}{{# example }}: {{ example }}{{/ example }}{{# properties_description }} ({{ properties_description }}){{/ properties_description }}{{# description }} - {{ description }}{{/ description }}
29 | {{/ parameters }}
30 | {{/ has_parameters? }}
31 | {{# has_attributes? }}
32 |
33 | + Attributes (object)
34 | {{# attributes }}
35 | + {{ name }}{{# example }}: {{ example }}{{/ example }}{{# properties_description }} ({{ properties_description }}){{/ properties_description }}{{# description }} - {{ description }}{{/ description }}
36 | {{/ attributes }}
37 | {{/ has_attributes? }}
38 | {{# http_methods }}
39 |
40 | ### {{ description }} [{{ http_method }}]
41 | {{# examples }}
42 | {{# requests }}
43 | {{# has_request? }}
44 |
45 | + Request {{ description }}{{# request_content_type }} ({{ request_content_type }}){{/ request_content_type }}
46 | {{/ has_request? }}
47 | {{# request_headers_text }}
48 |
49 | + Headers
50 |
51 | {{{ request_headers_text }}}
52 | {{/ request_headers_text }}
53 | {{# request_body }}
54 |
55 | + Body
56 |
57 | {{{ request_body }}}
58 | {{/ request_body }}
59 | {{# has_response? }}
60 |
61 | + Response {{ response_status }} ({{ response_content_type }})
62 | {{/ has_response? }}
63 | {{# response_headers_text }}
64 |
65 | + Headers
66 |
67 | {{{ response_headers_text }}}
68 | {{/ response_headers_text }}
69 | {{# response_body }}
70 |
71 | + Body
72 |
73 | {{{ response_body }}}
74 | {{/ response_body }}
75 | {{/ requests }}
76 | {{/ examples }}
77 | {{/ http_methods }}
78 | {{/ routes }}
79 | {{/ sections }}
80 |
--------------------------------------------------------------------------------
/lib/rspec_api_documentation/oauth2_mac_client.rb:
--------------------------------------------------------------------------------
1 | begin
2 | require "active_support/secure_random"
3 | rescue LoadError
4 | # ActiveSupport::SecureRandom not provided in activesupport >= 3.2
5 | end
6 | begin
7 | require "webmock"
8 | rescue LoadError
9 | raise "Webmock needs to be installed before using the OAuth2MACClient"
10 | end
11 | begin
12 | require "rack/oauth2"
13 | rescue LoadError
14 | raise "Rack OAuth2 needs to be installed before using the OAuth2MACClient"
15 | end
16 |
17 | module RspecApiDocumentation
18 | class OAuth2MACClient < ClientBase
19 | include WebMock::API
20 | attr_accessor :last_response, :last_request
21 | private :last_response, :last_request
22 |
23 | def request_headers
24 | env_to_headers(last_request.env)
25 | end
26 |
27 | def response_headers
28 | last_response.headers
29 | end
30 |
31 | def query_string
32 | last_request.env["QUERY_STRING"]
33 | end
34 |
35 | def status
36 | last_response.status
37 | end
38 |
39 | def response_body
40 | last_response.body
41 | end
42 |
43 | def request_content_type
44 | last_request.content_type
45 | end
46 |
47 | def response_content_type
48 | last_response.content_type
49 | end
50 |
51 | protected
52 |
53 | def do_request(method, path, params, request_headers)
54 | self.last_response = access_token.send(method, "http://example.com#{path}", :body => params, :header => headers(method, path, params, request_headers))
55 | end
56 |
57 | class ProxyApp < Struct.new(:client, :app)
58 | def call(env)
59 | env["QUERY_STRING"] = query_string_hack(env)
60 | client.last_request = Struct.new(:env, :content_type).new(env, env["CONTENT_TYPE"])
61 | app.call(env.merge("SCRIPT_NAME" => ""))
62 | end
63 |
64 | private
65 | def query_string_hack(env)
66 | env["QUERY_STRING"].gsub('%5B', '[').gsub('%5D', ']').gsub(/\[\d+/) { |s| "#{$1}[" }
67 | end
68 | end
69 |
70 | def access_token
71 | @access_token ||= begin
72 | app = ProxyApp.new(self, context.app)
73 | stub_request(:any, %r{http://example\.com}).to_rack(app)
74 | Rack::OAuth2::Client.new(options.merge(:host => "example.com", :scheme => "http")).access_token!
75 | end
76 | end
77 | end
78 | end
79 |
--------------------------------------------------------------------------------
/lib/rspec_api_documentation/curl.rb:
--------------------------------------------------------------------------------
1 | require 'active_support/core_ext/object/to_query'
2 | require 'base64'
3 |
4 | module RspecApiDocumentation
5 | class Curl < Struct.new(:method, :path, :data, :headers)
6 | attr_accessor :host
7 |
8 | def output(config_host, config_headers_to_filer = nil)
9 | self.host = config_host
10 | @config_headers_to_filer = Array(config_headers_to_filer)
11 | send(method.downcase)
12 | end
13 |
14 | def post
15 | "curl \"#{url}\" #{post_data} -X POST #{headers}"
16 | end
17 |
18 | def get
19 | "curl -g \"#{url}#{get_data}\" -X GET #{headers}"
20 | end
21 |
22 | def head
23 | "curl \"#{url}#{get_data}\" -X HEAD #{headers}"
24 | end
25 |
26 | def put
27 | "curl \"#{url}\" #{post_data} -X PUT #{headers}"
28 | end
29 |
30 | def delete
31 | "curl \"#{url}\" #{post_data} -X DELETE #{headers}"
32 | end
33 |
34 | def patch
35 | "curl \"#{url}\" #{post_data} -X PATCH #{headers}"
36 | end
37 |
38 | def url
39 | "#{host}#{path}"
40 | end
41 |
42 | def headers
43 | filter_headers(super).map do |k, v|
44 | if k =~ /authorization/i && v =~ /^Basic/
45 | "\\\n\t-u #{format_auth_header(v)}"
46 | else
47 | "\\\n\t-H \"#{format_full_header(k, v)}\""
48 | end
49 | end.join(" ")
50 | end
51 |
52 | def get_data
53 | "?#{data}" unless data.blank?
54 | end
55 |
56 | def post_data
57 | escaped_data = data.to_s.gsub("'", "\\u0027")
58 | "-d '#{escaped_data}'"
59 | end
60 |
61 | private
62 |
63 | def format_auth_header(value)
64 | ::Base64.decode64(value.split(' ', 2).last || '')
65 | end
66 |
67 | def format_header(header)
68 | header.gsub(/^HTTP_/, '').titleize.split.join("-")
69 | end
70 |
71 | def format_full_header(header, value)
72 | formatted_value = if value.is_a?(Numeric)
73 | value
74 | else
75 | value ? value.gsub(/"/, "\\\"") : ''
76 | end
77 |
78 | "#{format_header(header)}: #{formatted_value}"
79 | end
80 |
81 | def filter_headers(headers)
82 | if !@config_headers_to_filer.empty?
83 | headers.reject do |header|
84 | @config_headers_to_filer.map(&:downcase).include?(format_header(header).downcase)
85 | end
86 | else
87 | headers
88 | end
89 | end
90 | end
91 | end
92 |
--------------------------------------------------------------------------------
/lib/rspec_api_documentation.rb:
--------------------------------------------------------------------------------
1 | require 'active_support'
2 | require 'active_support/inflector'
3 | require 'active_support/core_ext/hash/conversions'
4 | require 'active_support/core_ext/hash/deep_merge'
5 | require 'cgi'
6 | require 'json'
7 |
8 | # Namespace for RspecApiDocumentation
9 | module RspecApiDocumentation
10 | extend ActiveSupport::Autoload
11 |
12 | require 'rspec_api_documentation/railtie' if defined?(Rails::Railtie)
13 | include ActiveSupport::JSON
14 |
15 | eager_autoload do
16 | autoload :Configuration
17 | autoload :ApiDocumentation
18 | autoload :ApiFormatter
19 | autoload :Example
20 | autoload :Index
21 | autoload :ClientBase
22 | autoload :Headers
23 | autoload :HttpTestClient
24 | end
25 |
26 | autoload :DSL
27 | autoload :RackTestClient
28 | autoload :OAuth2MACClient, "rspec_api_documentation/oauth2_mac_client"
29 | autoload :TestServer
30 | autoload :Curl
31 |
32 | module Writers
33 | extend ActiveSupport::Autoload
34 |
35 | autoload :Writer
36 | autoload :GeneralMarkupWriter
37 | autoload :HtmlWriter
38 | autoload :TextileWriter
39 | autoload :MarkdownWriter
40 | autoload :JsonWriter
41 | autoload :AppendJsonWriter
42 | autoload :JsonIodocsWriter
43 | autoload :IndexHelper
44 | autoload :CombinedTextWriter
45 | autoload :CombinedJsonWriter
46 | autoload :SlateWriter
47 | autoload :ApiBlueprintWriter
48 | end
49 |
50 | module Views
51 | extend ActiveSupport::Autoload
52 |
53 | autoload :MarkupIndex
54 | autoload :MarkupExample
55 | autoload :HtmlIndex
56 | autoload :HtmlExample
57 | autoload :TextileIndex
58 | autoload :TextileExample
59 | autoload :MarkdownIndex
60 | autoload :MarkdownExample
61 | autoload :SlateIndex
62 | autoload :SlateExample
63 | autoload :ApiBlueprintIndex
64 | autoload :ApiBlueprintExample
65 | end
66 |
67 | def self.configuration
68 | @configuration ||= Configuration.new
69 | end
70 |
71 | def self.documentations
72 | @documentations ||= configuration.map { |config| ApiDocumentation.new(config) }
73 | end
74 |
75 | # Configures RspecApiDocumentation
76 | #
77 | # See RspecApiDocumentation::Configuration for more information on configuring.
78 | #
79 | # RspecApiDocumentation.configure do |config|
80 | # config.docs_dir = "doc/api"
81 | # end
82 | def self.configure
83 | yield configuration if block_given?
84 | end
85 | end
86 |
--------------------------------------------------------------------------------
/features/step_definitions/html_steps.rb:
--------------------------------------------------------------------------------
1 | When /^I open the index$/ do
2 | visit "/index.html"
3 | end
4 |
5 | When /^I navigate to "([^"]*)"$/ do |example|
6 | click_link example
7 | end
8 |
9 | Then /^I should see the following resources:$/ do |table|
10 | expect(all("h2").map(&:text)).to eq(table.raw.flatten)
11 | end
12 |
13 | Then /^I should see the following parameters:$/ do |table|
14 | names = all(".parameters .name").map(&:text)
15 | descriptions = all(".parameters .description").map(&:text)
16 |
17 | expect(names.zip(descriptions)).to eq(table.rows)
18 | end
19 |
20 | Then(/^I should see the following response fields:$/) do |table|
21 | names = all(".response-fields .name").map(&:text)
22 | descriptions = all(".response-fields .description").map(&:text)
23 |
24 | expect(names.zip(descriptions)).to eq(table.rows)
25 | end
26 |
27 | Then /^I should see the following (request|response) headers:$/ do |part, table|
28 | actual_headers = page.find("pre.#{part}.headers").text
29 | expected_headers = table.raw.map { |row| row.join(": ") }
30 |
31 | expected_headers.each do |row|
32 | expect(actual_headers).to include(row.strip)
33 | end
34 | end
35 |
36 | Then /^I should not see the following (request|response) headers:$/ do |part, table|
37 | actual_headers = page.find("pre.#{part}.headers").text
38 | expected_headers = table.raw.map { |row| row.join(": ") }
39 |
40 | expected_headers.each do |row|
41 | expect(actual_headers).to_not include(row.strip)
42 | end
43 | end
44 |
45 | Then /^I should see the route is "([^"]*)"$/ do |route|
46 | expect(page).to have_css(".request.route", :text => route)
47 | end
48 |
49 | Then /^I should see the following query parameters:$/ do |table|
50 | text = page.find("pre.request.query_parameters").text
51 | actual = text.split("\n")
52 | expected = table.raw.map { |row| row.join(": ") }
53 |
54 | expect(actual).to match(expected)
55 | end
56 |
57 | Then /^I should see the response status is "([^"]*)"$/ do |status|
58 | expect(page).to have_css(".response.status", :text => status)
59 | end
60 |
61 | Then /^I should see the following request body:$/ do |request_body|
62 | expect(page).to have_css("pre.request.body", :text => request_body)
63 | end
64 |
65 | Then /^I should see the following response body:$/ do |response_body|
66 | expect(page).to have_css("pre.response.body", :text => response_body)
67 | end
68 |
69 | Then /^I should see the api name "(.*?)"$/ do |name|
70 | title = find("title").text
71 | header = find("h1").text
72 |
73 | expect(title).to eq(name)
74 | expect(header).to eq(name)
75 | end
76 |
--------------------------------------------------------------------------------
/lib/rspec_api_documentation/example.rb:
--------------------------------------------------------------------------------
1 | module RspecApiDocumentation
2 | class Example
3 | attr_reader :example, :configuration
4 |
5 | def initialize(example, configuration)
6 | @example = example
7 | @configuration = configuration
8 | end
9 |
10 | def method_missing(method_sym, *args, &block)
11 | if example.metadata.has_key?(method_sym)
12 | example.metadata[method_sym]
13 | else
14 | example.send(method_sym, *args, &block)
15 | end
16 | end
17 |
18 | def respond_to?(method_sym, include_private = false)
19 | super || example.metadata.has_key?(method_sym) || example.respond_to?(method_sym, include_private)
20 | end
21 |
22 | def http_method
23 | metadata[:method].to_s.upcase
24 | end
25 |
26 | def should_document?
27 | return false if pending? || !metadata[:resource_name] || !metadata[:document]
28 | return false if (Array(metadata[:document]) & Array(configuration.exclusion_filter)).length > 0
29 | return true if (Array(metadata[:document]) & Array(configuration.filter)).length > 0
30 | return true if configuration.filter == :all
31 | end
32 |
33 | def public?
34 | metadata[:public]
35 | end
36 |
37 | def has_parameters?
38 | respond_to?(:parameters) && parameters.present?
39 | end
40 |
41 | def has_attributes?
42 | respond_to?(:attributes) && attributes.present?
43 | end
44 |
45 | def has_response_fields?
46 | respond_to?(:response_fields) && response_fields.present?
47 | end
48 |
49 | def resource_explanation
50 | metadata[:resource_explanation] || nil
51 | end
52 |
53 | def explanation
54 | metadata[:explanation] || nil
55 | end
56 |
57 | def requests
58 | filter_headers(metadata[:requests]) || []
59 | end
60 |
61 | private
62 |
63 | def filter_headers(requests)
64 | requests = remap_headers(requests, :request_headers, configuration.request_headers_to_include)
65 | requests = remap_headers(requests, :response_headers, configuration.response_headers_to_include)
66 | requests
67 | end
68 |
69 | def remap_headers(requests, key, headers_to_include)
70 | return requests unless headers_to_include
71 | requests.each.with_index do |request_hash, index|
72 | next unless request_hash.key?(key)
73 | headers = request_hash[key]
74 | request_hash[key] = headers.select{ |key, _| headers_to_include.map(&:downcase).include?(key.downcase) }
75 | requests[index] = request_hash
76 | end
77 | end
78 | end
79 | end
80 |
--------------------------------------------------------------------------------
/features/callbacks.feature:
--------------------------------------------------------------------------------
1 | Feature: Document callbacks
2 |
3 | Background:
4 | Given a file named "app_spec.rb" with:
5 | """
6 | require "rspec_api_documentation"
7 | require "rspec_api_documentation/dsl"
8 |
9 | RspecApiDocumentation.configure do |config|
10 | config.app = lambda do
11 | uri = URI.parse("http://example.net/callback")
12 | Net::HTTP.start(uri.host, uri.port) do |http|
13 | request = Net::HTTP::Post.new(uri.path)
14 | request.body = '{"message":"Something interesting happened!"}'
15 | request["Content-Type"] = "application/json"
16 | request["User-Agent"] = "InterestingThingApp"
17 | http.request request
18 | end
19 | [200, {}, []]
20 | end
21 | end
22 |
23 | resource "Interesting Thing" do
24 | callback "/interesting_thing" do
25 | let(:callback_url) { "http://example.net/callback" }
26 |
27 | trigger_callback do
28 | app.call
29 | end
30 |
31 | example "Receiving a callback when interesting things happen" do
32 | do_callback
33 | expect(request_method).to eq("POST")
34 | expect(request_headers["Content-Type"]).to eq("application/json")
35 | expect(request_headers["User-Agent"]).to eq("InterestingThingApp")
36 | expect(request_body).to eq('{"message":"Something interesting happened!"}')
37 | end
38 | end
39 | end
40 | """
41 | When I run `rspec app_spec.rb --format RspecApiDocumentation::ApiFormatter`
42 |
43 | Scenario: Output helpful progress to the console
44 | Then the output should contain:
45 | """
46 | Generating API Docs
47 | Interesting Thing
48 | /interesting_thing
49 | * Receiving a callback when interesting things happen
50 | """
51 | And the output should contain "1 example, 0 failures"
52 | And the exit status should be 0
53 |
54 | Scenario: Create an index of all API examples, including all resources
55 | When I open the index
56 | Then I should see the following resources:
57 | | Interesting Thing |
58 |
59 | Scenario: Example HTML documentation includes the request information
60 | When I open the index
61 | And I navigate to "Receiving a callback when interesting things happen"
62 | Then I should see the route is "POST /callback"
63 | And I should see the following request headers:
64 | | Content-Type | application/json |
65 | | Accept | */* |
66 | | User-Agent | InterestingThingApp |
67 | And I should see the following request body:
68 | """
69 | {"message":"Something interesting happened!"}
70 | """
71 |
--------------------------------------------------------------------------------
/spec/writers/combined_text_example_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 | require "rspec_api_documentation/writers/combined_text_writer"
3 |
4 | describe RspecApiDocumentation::Writers::CombinedTextExample do
5 | let(:metadata) { {} }
6 | let(:rspec_example) { double(:resource_name => "Foo Bar", :description => "ABCDEFG", :metadata => metadata) }
7 | let(:example) { RspecApiDocumentation::Writers::CombinedTextExample.new(rspec_example) }
8 |
9 | it "should format its resource name" do
10 | expect(example.resource_name).to eq("foo_bar")
11 | end
12 |
13 | it "should format its description" do
14 | expect(example.description).to eq("ABCDEFG\n-------\n\n")
15 | end
16 |
17 | context "given parameters" do
18 | let(:metadata) {{
19 | :parameters => [
20 | { :name => "foo", :description => "Foo!" },
21 | { :name => "bar", :description => "Bar!" }
22 | ]
23 | }}
24 |
25 | it "should format its parameters" do
26 | expect(example.parameters).to eq("Parameters:\n * foo - Foo!\n * bar - Bar!\n\n")
27 | end
28 | end
29 |
30 | it "renders nothing if there are no parameters" do
31 | expect(example.parameters).to eq("")
32 | end
33 |
34 | context "when there are requests" do
35 | let(:requests) {[
36 | {
37 | :request_method => "GET",
38 | :request_path => "/greetings",
39 | :request_headers => { "Header" => "value" },
40 | :request_query_parameters => { "foo" => "bar", "baz" => "quux" },
41 | :response_status => 200,
42 | :response_status_text => "OK",
43 | :response_headers => { "Header" => "value", "Foo" => "bar" },
44 | :response_body => "body"
45 | },
46 | {
47 | :request_method => "POST",
48 | :request_path => "/greetings",
49 | :request_body => "body",
50 | :response_status => 404,
51 | :response_status_text => "Not Found",
52 | :response_headers => { "Header" => "value" },
53 | :response_body => "body"
54 | },
55 | {
56 | :request_method => "DELETE",
57 | :request_path => "/greetings/1",
58 | :response_status => 200,
59 | :response_status_text => "OK",
60 | :response_headers => { "Header" => "value" },
61 | },
62 | ]}
63 | let(:metadata) {{ :requests => requests }}
64 |
65 | it "exposes the requests" do
66 | expect(example.requests).to eq([
67 | [" GET /greetings\n Header: value\n\n baz=quux\n foo=bar\n", " Status: 200 OK\n Foo: bar\n Header: value\n\n body\n"],
68 | [" POST /greetings\n\n body\n", " Status: 404 Not Found\n Header: value\n\n body\n"],
69 | [" DELETE /greetings/1\n", " Status: 200 OK\n Header: value\n"],
70 | ])
71 | end
72 | end
73 |
74 | it "returns the empty array if there are no requests" do
75 | expect(example.requests).to eq([])
76 | end
77 | end
78 |
--------------------------------------------------------------------------------
/lib/rspec_api_documentation/views/markup_example.rb:
--------------------------------------------------------------------------------
1 | require 'mustache'
2 |
3 | module RspecApiDocumentation
4 | module Views
5 | class MarkupExample < Mustache
6 | def initialize(example, configuration)
7 | @example = example
8 | @host = configuration.curl_host
9 | @filter_headers = configuration.curl_headers_to_filter
10 | self.template_path = configuration.template_path
11 | end
12 |
13 | def method_missing(method, *args, &block)
14 | @example.send(method, *args, &block)
15 | end
16 |
17 | def respond_to?(method, include_private = false)
18 | super || @example.respond_to?(method, include_private)
19 | end
20 |
21 | def dirname
22 | resource_name.to_s.downcase.gsub(/\s+/, '_').gsub(":", "_")
23 | end
24 |
25 | def filename
26 | special_chars = /[<>:"\/\\|?*]/
27 | basename = description.downcase.gsub(/\s+/, '_').gsub(special_chars, '')
28 | basename = Digest::MD5.new.update(description).to_s if basename.blank?
29 | "#{basename}.#{extension}"
30 | end
31 |
32 | def parameters
33 | super.each do |parameter|
34 | if parameter.has_key?(:scope)
35 | parameter[:scope] = format_scope(parameter[:scope])
36 | end
37 | end
38 | end
39 |
40 | def response_fields
41 | super.each do |response_field|
42 | if response_field.has_key?(:scope)
43 | response_field[:scope] = format_scope(response_field[:scope])
44 | end
45 | end
46 | end
47 |
48 | def requests
49 | super.map do |hash|
50 | hash[:request_headers_text] = format_hash(hash[:request_headers])
51 | hash[:request_query_parameters_text] = format_hash(hash[:request_query_parameters])
52 | hash[:response_headers_text] = format_hash(hash[:response_headers])
53 | if @host
54 | if hash[:curl].is_a? RspecApiDocumentation::Curl
55 | hash[:curl] = hash[:curl].output(@host, @filter_headers)
56 | end
57 | else
58 | hash[:curl] = nil
59 | end
60 | hash
61 | end
62 | end
63 |
64 | def extension
65 | raise 'Parent class. This method should not be called.'
66 | end
67 |
68 | private
69 |
70 | def format_hash(hash = {})
71 | return nil unless hash.present?
72 | hash.collect do |k, v|
73 | "#{k}: #{v}"
74 | end.join("\n")
75 | end
76 |
77 | def format_scope(unformatted_scope)
78 | Array(unformatted_scope).each_with_index.map do |scope, index|
79 | if index == 0
80 | scope
81 | else
82 | "[#{scope}]"
83 | end
84 | end.join
85 | end
86 |
87 | def content_type(headers)
88 | headers && headers.fetch("Content-Type", nil)
89 | end
90 | end
91 | end
92 | end
93 |
--------------------------------------------------------------------------------
/lib/rspec_api_documentation/http_test_client.rb:
--------------------------------------------------------------------------------
1 | begin
2 | require 'faraday'
3 | rescue LoadError
4 | raise "Faraday needs to be installed before using the HttpTestClient"
5 | end
6 |
7 | Faraday::Request.register_middleware :request_saver => lambda { RspecApiDocumentation::RequestSaver }
8 |
9 | module RspecApiDocumentation
10 | class RequestSaver < Faraday::Middleware
11 | attr_reader :client
12 |
13 | def initialize(app, client)
14 | super(app)
15 | @client = client
16 | end
17 |
18 | def call(env)
19 | client.last_request = env
20 |
21 | @app.call(env).on_complete do |env|
22 | client.last_response = env
23 | end
24 | end
25 | end
26 |
27 | class HttpTestClient < ClientBase
28 | attr_reader :last_response, :last_request
29 |
30 | LastRequest = Struct.new(:url, :method, :request_headers, :body)
31 |
32 | def request_headers
33 | env_to_headers(last_request.request_headers)
34 | end
35 |
36 | def response_headers
37 | last_response.response_headers
38 | end
39 |
40 | def query_string
41 | last_request.url.query
42 | end
43 |
44 | def status
45 | last_response.status
46 | end
47 |
48 | def response_body
49 | last_response.body
50 | end
51 |
52 | def request_content_type
53 | last_request.request_headers["CONTENT_TYPE"]
54 | end
55 |
56 | def response_content_type
57 | last_response.request_headers["CONTENT_TYPE"]
58 | end
59 |
60 | def do_request(method, path, params, request_headers)
61 | http_test_session.send(method, path, params, headers(method, path, params, request_headers))
62 | end
63 |
64 | def last_request=(env)
65 | @last_request = LastRequest.new(env.url, env.method, env.request_headers, env.body)
66 | end
67 |
68 | def last_response=(env)
69 | @last_response = env
70 | end
71 |
72 | protected
73 |
74 | def headers(*args)
75 | headers_to_env(super)
76 | end
77 |
78 | def handle_multipart_body(request_headers, request_body)
79 | parsed_parameters = Rack::Request.new({
80 | "CONTENT_TYPE" => request_headers["Content-Type"],
81 | "rack.input" => StringIO.new(request_body)
82 | }).params
83 |
84 | clean_out_uploaded_data(parsed_parameters, request_body)
85 | end
86 |
87 | def read_request_body
88 | if [:post, :put].include?(last_request.method)
89 | last_request.body || ""
90 | else
91 | ""
92 | end
93 | end
94 |
95 | private
96 |
97 | def http_test_session
98 | ::Faraday.new(:url => options[:host]) do |faraday|
99 | faraday.request :url_encoded # form-encode POST params
100 | faraday.request :request_saver, self # save the request and response
101 | faraday.adapter Faraday.default_adapter # make requests with Net::HTTP
102 | end
103 | end
104 | end
105 | end
106 |
--------------------------------------------------------------------------------
/example/spec/acceptance/orders_spec.rb:
--------------------------------------------------------------------------------
1 | require 'acceptance_helper'
2 |
3 | resource "Orders" do
4 | header "Accept", "application/json"
5 | header "Content-Type", "application/json"
6 |
7 | let(:order) { Order.create(:name => "Old Name", :paid => true, :email => "email@example.com") }
8 |
9 | get "/orders" do
10 | parameter :page, "Current page of orders"
11 |
12 | let(:page) { 1 }
13 |
14 | before do
15 | 2.times do |i|
16 | Order.create(:name => "Order #{i}", :email => "email#{i}@example.com", :paid => true)
17 | end
18 | end
19 |
20 | example_request "Getting a list of orders" do
21 | expect(response_body).to eq(Order.all.to_json)
22 | expect(status).to eq(200)
23 | end
24 | end
25 |
26 | head "/orders" do
27 | example_request "Getting the headers" do
28 | expect(response_headers["Cache-Control"]).to eq("max-age=0, private, must-revalidate")
29 | end
30 | end
31 |
32 | post "/orders" do
33 | parameter :name, "Name of order", :required => true, :scope => :order
34 | parameter :paid, "If the order has been paid for", :required => true, :scope => :order
35 | parameter :email, "Email of user that placed the order", :scope => :order
36 |
37 | response_field :name, "Name of order", :scope => :order, "Type" => "String"
38 | response_field :paid, "If the order has been paid for", :scope => :order, "Type" => "Boolean"
39 | response_field :email, "Email of user that placed the order", :scope => :order, "Type" => "String"
40 |
41 | let(:name) { "Order 1" }
42 | let(:paid) { true }
43 | let(:email) { "email@example.com" }
44 |
45 | let(:raw_post) { params.to_json }
46 |
47 | example_request "Creating an order" do
48 | explanation "First, create an order, then make a later request to get it back"
49 |
50 | order = JSON.parse(response_body)
51 | expect(order.except("id", "created_at", "updated_at")).to eq({
52 | "name" => name,
53 | "paid" => paid,
54 | "email" => email,
55 | })
56 | expect(status).to eq(201)
57 |
58 | client.get(URI.parse(response_headers["location"]).path, {}, headers)
59 | expect(status).to eq(200)
60 | end
61 | end
62 |
63 | get "/orders/:id" do
64 | let(:id) { order.id }
65 |
66 | example_request "Getting a specific order" do
67 | expect(response_body).to eq(order.to_json)
68 | expect(status).to eq(200)
69 | end
70 | end
71 |
72 | put "/orders/:id" do
73 | parameter :name, "Name of order", :scope => :order
74 | parameter :paid, "If the order has been paid for", :scope => :order
75 | parameter :email, "Email of user that placed the order", :scope => :order
76 |
77 | let(:id) { order.id }
78 | let(:name) { "Updated Name" }
79 |
80 | let(:raw_post) { params.to_json }
81 |
82 | example_request "Updating an order" do
83 | expect(status).to eq(204)
84 | end
85 | end
86 |
87 | delete "/orders/:id" do
88 | let(:id) { order.id }
89 |
90 | example_request "Deleting an order" do
91 | expect(status).to eq(204)
92 | end
93 | end
94 | end
95 |
--------------------------------------------------------------------------------
/spec/api_formatter_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe RspecApiDocumentation::ApiFormatter do
4 | let(:metadata) { {} }
5 | let(:group) { RSpec::Core::ExampleGroup.describe("Orders", metadata) }
6 | let(:output) { StringIO.new }
7 | let(:formatter) { RspecApiDocumentation::ApiFormatter.new(output) }
8 |
9 | describe "generating documentation" do
10 | include FakeFS::SpecHelpers
11 |
12 | before do
13 | RspecApiDocumentation.documentations.each do |configuration|
14 | allow(configuration).to receive(:clear_docs)
15 | allow(configuration).to receive(:document_example)
16 | allow(configuration).to receive(:write)
17 | end
18 | end
19 |
20 | it "should clear all docs on start" do
21 | RspecApiDocumentation.documentations.each do |configuration|
22 | expect(configuration).to receive(:clear_docs)
23 | end
24 |
25 | formatter.start(RSpec::Core::Notifications::StartNotification.new(0, 0))
26 | end
27 |
28 | it "should document passing examples" do
29 | example = group.example("Ordering a cup of coffee") {}
30 |
31 | RspecApiDocumentation.documentations.each do |configuration|
32 | expect(configuration).to receive(:document_example).with(example)
33 | end
34 |
35 | formatter.example_passed(RSpec::Core::Notifications::ExampleNotification.for(example))
36 | end
37 |
38 | it "should write the docs on stop" do
39 | RspecApiDocumentation.documentations.each do |configuration|
40 | expect(configuration).to receive(:write)
41 | end
42 |
43 | formatter.stop(RSpec::Core::Notifications::NullNotification.new)
44 | end
45 | end
46 |
47 | describe "output" do
48 | let(:reporter) { RSpec::Core::Reporter.new(RSpec::Core::Configuration.new) }
49 |
50 | before do
51 | # don't do any work
52 | allow(RspecApiDocumentation).to receive(:documentations).and_return([])
53 |
54 | reporter.register_listener formatter, :start, :example_group_started, :example_passed, :example_failed, :stop
55 | end
56 |
57 | context "with passing examples" do
58 | before do
59 | group.example("Ordering a cup of coffee") {}
60 | group.example("Updating an order") {}
61 | group.example("Viewing an order") {}
62 | end
63 |
64 | it "should list the generated docs" do
65 | group.run(reporter)
66 | expect(output.string.split($/)).to eq([
67 | "Generating API Docs",
68 | " Orders",
69 | " * Ordering a cup of coffee",
70 | " * Updating an order",
71 | " * Viewing an order"
72 | ])
73 | end
74 | end
75 |
76 | context "with failing examples" do
77 | before do
78 | group.example("Ordering a cup of coffee") {}
79 | group.example("Updating an order") { expect(true).to eq(false) }
80 | group.example("Viewing an order") { expect(true).to eq(false) }
81 | end
82 |
83 | it "should indicate failures" do
84 | group.run(reporter)
85 | expect(output.string.split($/)).to eq([
86 | "Generating API Docs",
87 | " Orders",
88 | " * Ordering a cup of coffee",
89 | " ! Updating an order (FAILED)",
90 | " ! Viewing an order (FAILED)"
91 | ])
92 | end
93 | end
94 | end
95 | end
96 |
--------------------------------------------------------------------------------
/lib/rspec_api_documentation/writers/json_iodocs_writer.rb:
--------------------------------------------------------------------------------
1 | require 'rspec_api_documentation/writers/formatter'
2 |
3 | module RspecApiDocumentation
4 | module Writers
5 | class JsonIodocsWriter < Writer
6 | attr_accessor :api_key
7 | delegate :docs_dir, :to => :configuration
8 |
9 | def initialize(index, configuration)
10 | super
11 | self.api_key = configuration.api_name.parameterize
12 | end
13 |
14 | def write
15 | File.open(docs_dir.join("apiconfig.json"), "w+") do |file|
16 | file.write Formatter.to_json(ApiConfig.new(configuration))
17 | end
18 | File.open(docs_dir.join("#{api_key}.json"), "w+") do |file|
19 | file.write Formatter.to_json(JsonIndex.new(index, configuration))
20 | end
21 | end
22 | end
23 |
24 | class JsonIndex
25 | def initialize(index, configuration)
26 | @index = index
27 | @configuration = configuration
28 | end
29 |
30 | def sections
31 | IndexHelper.sections(examples, @configuration)
32 | end
33 |
34 | def examples
35 | @index.examples.map { |example| JsonExample.new(example, @configuration) }
36 | end
37 |
38 | def as_json(opts = nil)
39 | sections.inject({:endpoints => []}) do |h, section|
40 | h[:endpoints].push(
41 | :name => section[:resource_name],
42 | :methods => section[:examples].map do |example|
43 | example.as_json(opts)
44 | end
45 | )
46 | h
47 | end
48 | end
49 | end
50 |
51 | class JsonExample
52 | def initialize(example, configuration)
53 | @example = example
54 | end
55 |
56 | def method_missing(method, *args, &block)
57 | @example.send(method, *args, &block)
58 | end
59 |
60 | def parameters
61 | params = []
62 | if @example.respond_to?(:parameters)
63 | @example.parameters.map do |param|
64 | params << {
65 | "Name" => param[:name],
66 | "Description" => param[:description],
67 | "Default" => "",
68 | "Required" => param[:required] ? "Y" : "N"
69 | }
70 | end
71 | end
72 | params
73 | end
74 |
75 | def as_json(opts = nil)
76 | {
77 | :MethodName => description,
78 | :Synopsis => explanation,
79 | :HTTPMethod => http_method,
80 | :URI => (requests.first[:request_path] rescue ""),
81 | :RequiresOAuth => "N",
82 | :parameters => parameters
83 | }
84 | end
85 | end
86 |
87 | class ApiConfig
88 | def initialize(configuration)
89 | @configuration = configuration
90 | @api_key = configuration.api_name.parameterize
91 | end
92 |
93 | def as_json(opts = nil)
94 | {
95 | @api_key.to_sym => {
96 | :name => @configuration.api_name,
97 | :description => @configuration.api_explanation,
98 | :protocol => @configuration.io_docs_protocol,
99 | :publicPath => "",
100 | :baseURL => @configuration.curl_host
101 | }
102 | }
103 | end
104 | end
105 | end
106 | end
107 |
--------------------------------------------------------------------------------
/lib/rspec_api_documentation/views/api_blueprint_index.rb:
--------------------------------------------------------------------------------
1 | module RspecApiDocumentation
2 | module Views
3 | class ApiBlueprintIndex < MarkupIndex
4 | def initialize(index, configuration)
5 | super
6 | self.template_name = "rspec_api_documentation/api_blueprint_index"
7 | end
8 |
9 | def sections
10 | super.map do |section|
11 | routes = section[:examples].group_by { |e| "#{e.route_uri}#{e.route_optionals}" }.map do |route, examples|
12 | attrs = fields(:attributes, examples)
13 | params = fields(:parameters, examples)
14 |
15 | methods = examples.group_by(&:http_method).map do |http_method, examples|
16 | {
17 | http_method: http_method,
18 | description: examples.first.respond_to?(:action_name) && examples.first.action_name,
19 | examples: examples
20 | }
21 | end
22 |
23 | {
24 | "has_attributes?".to_sym => attrs.size > 0,
25 | "has_parameters?".to_sym => params.size > 0,
26 | route: route,
27 | route_name: examples[0][:route_name],
28 | attributes: attrs,
29 | parameters: params,
30 | http_methods: methods
31 | }
32 | end
33 |
34 | section.merge({
35 | routes: routes
36 | })
37 | end
38 | end
39 |
40 | def examples
41 | @index.examples.map do |example|
42 | ApiBlueprintExample.new(example, @configuration)
43 | end
44 | end
45 |
46 | private
47 |
48 | # APIB has both `parameters` and `attributes`. This generates a hash
49 | # with all of its properties, like name, description, required.
50 | # {
51 | # required: true,
52 | # example: "1",
53 | # type: "string",
54 | # name: "id",
55 | # description: "The id",
56 | # properties_description: "required, string"
57 | # }
58 | def fields(property_name, examples)
59 | examples
60 | .map { |example| example.metadata[property_name] }
61 | .flatten
62 | .compact
63 | .uniq { |property| property[:name] }
64 | .map do |property|
65 | properties = []
66 | properties << "required" if property[:required]
67 | properties << property[:type] if property[:type]
68 | if properties.count > 0
69 | property[:properties_description] = properties.join(", ")
70 | else
71 | property[:properties_description] = nil
72 | end
73 |
74 | property[:description] = nil if description_blank?(property)
75 | property
76 | end
77 | end
78 |
79 | # When no `description` was specified for a parameter, the DSL class
80 | # is making `description = "#{scope} #{name}"`, which is bad because it
81 | # assumes that all formats want this behavior. To avoid changing there
82 | # and breaking everything, I do my own check here and if description
83 | # equals the name, I assume it is blank.
84 | def description_blank?(property)
85 | !property[:description] ||
86 | property[:description].to_s.strip == property[:name].to_s.strip
87 | end
88 | end
89 | end
90 | end
91 |
--------------------------------------------------------------------------------
/example/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | # This file was generated by the `rails generate rspec:install` command. Conventionally, all
2 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3 | # The generated `.rspec` file contains `--require spec_helper` which will cause this
4 | # file to always be loaded, without a need to explicitly require it in any files.
5 | #
6 | # Given that it is always loaded, you are encouraged to keep this file as
7 | # light-weight as possible. Requiring heavyweight dependencies from this file
8 | # will add to the boot time of your test suite on EVERY test run, even for an
9 | # individual file that may not need all of that loaded. Instead, make a
10 | # separate helper file that requires this one and then use it only in the specs
11 | # that actually need it.
12 | #
13 | # The `.rspec` file also contains a few flags that are not defaults but that
14 | # users commonly want.
15 | #
16 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
17 | RSpec.configure do |config|
18 | # These two settings work together to allow you to limit a spec run
19 | # to individual examples or groups you care about by tagging them with
20 | # `:focus` metadata. When nothing is tagged with `:focus`, all examples
21 | # get run.
22 | config.filter_run :focus
23 | config.run_all_when_everything_filtered = true
24 |
25 | # Many RSpec users commonly either run the entire suite or an individual
26 | # file, and it's useful to allow more verbose output when running an
27 | # individual spec file.
28 | if config.files_to_run.one?
29 | # Use the documentation formatter for detailed output,
30 | # unless a formatter has already been configured
31 | # (e.g. via a command-line flag).
32 | config.default_formatter = 'doc'
33 | end
34 |
35 | # Print the 10 slowest examples and example groups at the
36 | # end of the spec run, to help surface which specs are running
37 | # particularly slow.
38 | config.profile_examples = 10
39 |
40 | # Run specs in random order to surface order dependencies. If you find an
41 | # order dependency and want to debug it, you can fix the order by providing
42 | # the seed, which is printed after each run.
43 | # --seed 1234
44 | config.order = :random
45 |
46 | # Seed global randomization in this process using the `--seed` CLI option.
47 | # Setting this allows you to use `--seed` to deterministically reproduce
48 | # test failures related to randomization by passing the same `--seed` value
49 | # as the one that triggered the failure.
50 | Kernel.srand config.seed
51 |
52 | # rspec-expectations config goes here. You can use an alternate
53 | # assertion/expectation library such as wrong or the stdlib/minitest
54 | # assertions if you prefer.
55 | config.expect_with :rspec do |expectations|
56 | # Enable only the newer, non-monkey-patching expect syntax.
57 | # For more details, see:
58 | # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
59 | expectations.syntax = :expect
60 | end
61 |
62 | # rspec-mocks config goes here. You can use an alternate test double
63 | # library (such as bogus or mocha) by changing the `mock_with` option here.
64 | config.mock_with :rspec do |mocks|
65 | # Enable only the newer, non-monkey-patching expect syntax.
66 | # For more details, see:
67 | # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
68 | mocks.syntax = :expect
69 |
70 | # Prevents you from mocking or stubbing a method that does not exist on
71 | # a real object. This is generally recommended.
72 | mocks.verify_partial_doubles = true
73 | end
74 | end
75 |
--------------------------------------------------------------------------------
/lib/rspec_api_documentation/writers/combined_text_writer.rb:
--------------------------------------------------------------------------------
1 | module RspecApiDocumentation
2 | module Writers
3 | class CombinedTextWriter < Writer
4 | def write
5 | index.examples.each do |rspec_example|
6 | example = CombinedTextExample.new(rspec_example)
7 | FileUtils.mkdir_p(configuration.docs_dir.join(example.resource_name))
8 | File.open(configuration.docs_dir.join(example.resource_name, "index.txt"), "a+") do |f|
9 | f.print example.description
10 | f.print example.parameters
11 |
12 | example.requests.each_with_index do |(request, response), i|
13 | f.puts "Request:"
14 | f.puts request
15 | f.puts
16 | f.puts "Response:"
17 | f.puts response
18 |
19 | if i + 1 < example.requests.count
20 | f.puts
21 | end
22 | end
23 |
24 | unless rspec_example == index.examples.last
25 | f.puts
26 | f.puts
27 | end
28 | end
29 | end
30 | end
31 |
32 | def self.format_hash(hash, separator="=")
33 | hash.sort_by { |k, v| k }.inject("") do |out, (k, v)|
34 | out << " #{k}#{separator}#{v}\n"
35 | end
36 | end
37 | end
38 |
39 | class CombinedTextExample
40 | attr_reader :example
41 |
42 | def initialize(example)
43 | @example = example
44 | end
45 |
46 | def resource_name
47 | example.resource_name.to_s.downcase.gsub(/\s+/, '_')
48 | end
49 |
50 | def description
51 | example.description + "\n" + "-" * example.description.length + "\n\n"
52 | end
53 |
54 | def parameters
55 | return "" unless example.metadata[:parameters]
56 | "Parameters:\n" + example.metadata[:parameters].inject("") do |out, parameter|
57 | out << " * #{parameter[:name]} - #{parameter[:description]}\n"
58 | end + "\n"
59 | end
60 |
61 | def requests
62 | return [] unless example.metadata[:requests]
63 | example.metadata[:requests].map do |request|
64 | [format_request(request), format_response(request)]
65 | end
66 | end
67 |
68 | private
69 | def format_request(request)
70 | [
71 | [
72 | " #{request[:request_method]} #{request[:request_path]}",
73 | format_hash(request[:request_headers], ": ")
74 | ],
75 | [
76 | format_string(request[:request_body]) || format_hash(request[:request_query_parameters])
77 | ]
78 | ].map { |x| x.compact.join("\n") }.reject(&:blank?).join("\n\n") + "\n"
79 | end
80 |
81 | def format_response(request)
82 | [
83 | [
84 | " Status: #{request[:response_status]} #{request[:response_status_text]}",
85 | format_hash(request[:response_headers], ": ")
86 | ],
87 | [
88 | format_string(request[:response_body])
89 | ]
90 | ].map { |x| x.compact.join("\n") }.reject(&:blank?).join("\n\n") + "\n"
91 | end
92 |
93 | def format_string(string)
94 | return unless string
95 | string.split("\n").map { |line| " #{line}" }.join("\n")
96 | end
97 |
98 | def format_hash(hash, separator="=")
99 | return unless hash
100 | hash.sort_by { |k, v| k }.map do |k, v|
101 | " #{k}#{separator}#{v}"
102 | end.join("\n")
103 | end
104 | end
105 | end
106 | end
107 |
--------------------------------------------------------------------------------
/example/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # Code is not reloaded between requests.
5 | config.cache_classes = true
6 |
7 | # Eager load code on boot. This eager loads most of Rails and
8 | # your application in memory, allowing both threaded web servers
9 | # and those relying on copy on write to perform better.
10 | # Rake tasks automatically ignore this option for performance.
11 | config.eager_load = true
12 |
13 | # Full error reports are disabled and caching is turned on.
14 | config.consider_all_requests_local = false
15 | config.action_controller.perform_caching = true
16 |
17 | # Enable Rack::Cache to put a simple HTTP cache in front of your application
18 | # Add `rack-cache` to your Gemfile before enabling this.
19 | # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid.
20 | # config.action_dispatch.rack_cache = true
21 |
22 | # Disable Rails's static asset server (Apache or nginx will already do this).
23 | config.serve_static_assets = false
24 |
25 | # Compress JavaScripts and CSS.
26 | config.assets.js_compressor = :uglifier
27 | # config.assets.css_compressor = :sass
28 |
29 | # Do not fallback to assets pipeline if a precompiled asset is missed.
30 | config.assets.compile = false
31 |
32 | # Generate digests for assets URLs.
33 | config.assets.digest = true
34 |
35 | # Version of your assets, change this if you want to expire all your assets.
36 | config.assets.version = '1.0'
37 |
38 | # Specifies the header that your server uses for sending files.
39 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache
40 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx
41 |
42 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
43 | # config.force_ssl = true
44 |
45 | # Set to :debug to see everything in the log.
46 | config.log_level = :info
47 |
48 | # Prepend all log lines with the following tags.
49 | # config.log_tags = [ :subdomain, :uuid ]
50 |
51 | # Use a different logger for distributed setups.
52 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new)
53 |
54 | # Use a different cache store in production.
55 | # config.cache_store = :mem_cache_store
56 |
57 | # Enable serving of images, stylesheets, and JavaScripts from an asset server.
58 | # config.action_controller.asset_host = "http://assets.example.com"
59 |
60 | # Precompile additional assets.
61 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added.
62 | # config.assets.precompile += %w( search.js )
63 |
64 | # Ignore bad email addresses and do not raise email delivery errors.
65 | # Set this to true and configure the email server for immediate delivery to raise delivery errors.
66 | # config.action_mailer.raise_delivery_errors = false
67 |
68 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
69 | # the I18n.default_locale when a translation cannot be found).
70 | config.i18n.fallbacks = true
71 |
72 | # Send deprecation notices to registered listeners.
73 | config.active_support.deprecation = :notify
74 |
75 | # Disable automatic flushing of the log to improve performance.
76 | # config.autoflush_log = false
77 |
78 | # Use default logging formatter so that PID and timestamp are not suppressed.
79 | config.log_formatter = ::Logger::Formatter.new
80 |
81 | # Do not dump schema after migrations.
82 | config.active_record.dump_schema_after_migration = false
83 | end
84 |
--------------------------------------------------------------------------------
/lib/rspec_api_documentation/writers/json_writer.rb:
--------------------------------------------------------------------------------
1 | require 'rspec_api_documentation/writers/formatter'
2 |
3 | module RspecApiDocumentation
4 | module Writers
5 | class JsonWriter < Writer
6 | delegate :docs_dir, :to => :configuration
7 |
8 | def write
9 | File.open(docs_dir.join("index.json"), "w+") do |f|
10 | f.write Formatter.to_json(JsonIndex.new(index, configuration))
11 | end
12 | write_examples
13 | end
14 |
15 | def write_examples
16 | index.examples.each do |example|
17 | json_example = JsonExample.new(example, configuration)
18 | FileUtils.mkdir_p(docs_dir.join(json_example.dirname))
19 | File.open(docs_dir.join(json_example.dirname, json_example.filename), "w+") do |f|
20 | f.write Formatter.to_json(json_example)
21 | end
22 | end
23 | end
24 | end
25 |
26 | class JsonIndex
27 | def initialize(index, configuration)
28 | @index = index
29 | @configuration = configuration
30 | end
31 |
32 | def sections
33 | IndexHelper.sections(examples, @configuration)
34 | end
35 |
36 | def examples
37 | @index.examples.map { |example| JsonExample.new(example, @configuration) }
38 | end
39 |
40 | def as_json(opts = nil)
41 | sections.inject({:resources => []}) do |h, section|
42 | h[:resources].push(section_hash(section))
43 | h
44 | end
45 | end
46 |
47 | def section_hash(section)
48 | {
49 | :name => section[:resource_name],
50 | :explanation => section[:resource_explanation],
51 | :examples => section[:examples].map { |example|
52 | {
53 | :description => example.description,
54 | :link => "#{example.dirname}/#{example.filename}",
55 | :groups => example.metadata[:document],
56 | :route => example.route,
57 | :method => example.metadata[:method]
58 | }
59 | }
60 | }
61 | end
62 | end
63 |
64 | class JsonExample
65 | def initialize(example, configuration)
66 | @example = example
67 | @host = configuration.curl_host
68 | @filter_headers = configuration.curl_headers_to_filter
69 | end
70 |
71 | def method_missing(method, *args, &block)
72 | @example.send(method, *args, &block)
73 | end
74 |
75 | def respond_to?(method, include_private = false)
76 | super || @example.respond_to?(method, include_private)
77 | end
78 |
79 | def dirname
80 | resource_name.to_s.downcase.gsub(/\s+/, '_').sub(/^\//,'')
81 | end
82 |
83 | def filename
84 | basename = description.downcase.gsub(/\s+/, '_').gsub(Pathname::SEPARATOR_PAT, '')
85 | "#{basename}.json"
86 | end
87 |
88 | def as_json(opts = nil)
89 | {
90 | :resource => resource_name,
91 | :resource_explanation => resource_explanation,
92 | :http_method => http_method,
93 | :route => route,
94 | :description => description,
95 | :explanation => explanation,
96 | :parameters => respond_to?(:parameters) ? parameters : [],
97 | :response_fields => respond_to?(:response_fields) ? response_fields : [],
98 | :requests => requests
99 | }
100 | end
101 |
102 | def requests
103 | super.map do |hash|
104 | if @host
105 | if hash[:curl].is_a? RspecApiDocumentation::Curl
106 | hash[:curl] = hash[:curl].output(@host, @filter_headers)
107 | end
108 | else
109 | hash[:curl] = nil
110 | end
111 | hash
112 | end
113 | end
114 | end
115 | end
116 | end
117 |
--------------------------------------------------------------------------------
/lib/rspec_api_documentation/client_base.rb:
--------------------------------------------------------------------------------
1 | module RspecApiDocumentation
2 | # Base client class that documents all requests that go through it.
3 | #
4 | # client.get("/orders", { :page => 2 }, { "Accept" => "application/json" })
5 | class ClientBase < Struct.new(:context, :options)
6 | include Headers
7 |
8 | delegate :example, :app, :to => :context
9 | delegate :metadata, :to => :example
10 |
11 | def get(*args)
12 | process :get, *args
13 | end
14 |
15 | def post(*args)
16 | process :post, *args
17 | end
18 |
19 | def put(*args)
20 | process :put, *args
21 | end
22 |
23 | def delete(*args)
24 | process :delete, *args
25 | end
26 |
27 | def head(*args)
28 | process :head, *args
29 | end
30 |
31 | def patch(*args)
32 | process :patch, *args
33 | end
34 |
35 | def response_status
36 | status
37 | end
38 |
39 | private
40 |
41 | def process(method, path, params = {}, headers ={})
42 | do_request(method, path, params, headers)
43 | document_example(method.to_s.upcase, path)
44 | end
45 |
46 | def read_request_body
47 | input = last_request.env["rack.input"]
48 | input.rewind
49 | input.read
50 | end
51 |
52 | def document_example(method, path)
53 | return unless metadata[:document]
54 |
55 | request_body = read_request_body
56 |
57 | request_metadata = {}
58 |
59 | if request_content_type =~ /multipart\/form-data/ && respond_to?(:handle_multipart_body, true)
60 | request_body = handle_multipart_body(request_headers, request_body)
61 | end
62 |
63 | request_metadata[:request_method] = method
64 | request_metadata[:request_path] = path
65 | request_metadata[:request_body] = request_body.empty? ? nil : request_body.force_encoding("UTF-8")
66 | request_metadata[:request_headers] = request_headers
67 | request_metadata[:request_query_parameters] = query_hash
68 | request_metadata[:request_content_type] = request_content_type
69 | request_metadata[:response_status] = status
70 | request_metadata[:response_status_text] = Rack::Utils::HTTP_STATUS_CODES[status]
71 | request_metadata[:response_body] = record_response_body(response_content_type, response_body)
72 | request_metadata[:response_headers] = response_headers
73 | request_metadata[:response_content_type] = response_content_type
74 | request_metadata[:curl] = Curl.new(method, path, request_body, request_headers)
75 |
76 | metadata[:requests] ||= []
77 | metadata[:requests] << request_metadata
78 | end
79 |
80 | def query_hash
81 | Rack::Utils.parse_nested_query(query_string)
82 | end
83 |
84 | def headers(method, path, params, request_headers)
85 | request_headers || {}
86 | end
87 |
88 | def record_response_body(response_content_type, response_body)
89 | return nil if response_body.empty?
90 | if response_body.encoding == Encoding::ASCII_8BIT
91 | "[binary data]"
92 | else
93 | formatter = RspecApiDocumentation.configuration.response_body_formatter
94 | return formatter.call(response_content_type, response_body)
95 | end
96 | end
97 |
98 | def clean_out_uploaded_data(params, request_body)
99 | params.each do |value|
100 | if [Hash, Array].member? value.class
101 | request_body = if value.respond_to?(:has_key?) && value.has_key?(:tempfile)
102 | data = value[:tempfile].read
103 | request_body.gsub(data, "[uploaded data]")
104 | else
105 | clean_out_uploaded_data(value, request_body)
106 | end
107 | end
108 | end
109 | request_body
110 | end
111 | end
112 | end
113 |
--------------------------------------------------------------------------------
/lib/rspec_api_documentation/views/api_blueprint_example.rb:
--------------------------------------------------------------------------------
1 | module RspecApiDocumentation
2 | module Views
3 | class ApiBlueprintExample < MarkupExample
4 | TOTAL_SPACES_INDENTATION = 8.freeze
5 |
6 | def initialize(example, configuration)
7 | super
8 | self.template_name = "rspec_api_documentation/api_blueprint_example"
9 | end
10 |
11 | def parameters
12 | super.map do |parameter|
13 | parameter.merge({
14 | :required => !!parameter[:required],
15 | :has_example => !!parameter[:example],
16 | :has_type => !!parameter[:type]
17 | })
18 | end
19 | end
20 |
21 | def requests
22 | super.map do |request|
23 | request[:request_headers_text] = remove_utf8_for_json(request[:request_headers_text])
24 | request[:request_headers_text] = indent(request[:request_headers_text])
25 | request[:request_content_type] = content_type(request[:request_headers])
26 | request[:request_content_type] = remove_utf8_for_json(request[:request_content_type])
27 | request[:request_body] = body_to_json(request, :request)
28 | request[:request_body] = indent(request[:request_body])
29 |
30 | request[:response_headers_text] = remove_utf8_for_json(request[:response_headers_text])
31 | request[:response_headers_text] = indent(request[:response_headers_text])
32 | request[:response_content_type] = content_type(request[:response_headers])
33 | request[:response_content_type] = remove_utf8_for_json(request[:response_content_type])
34 | request[:response_body] = body_to_json(request, :response)
35 | request[:response_body] = indent(request[:response_body])
36 |
37 | request[:has_request?] = has_request?(request)
38 | request[:has_response?] = has_response?(request)
39 | request
40 | end
41 | end
42 |
43 | def extension
44 | Writers::ApiBlueprintWriter::EXTENSION
45 | end
46 |
47 | private
48 |
49 | def has_request?(metadata)
50 | metadata.any? do |key, value|
51 | [:request_body, :request_headers, :request_content_type].include?(key) && value
52 | end
53 | end
54 |
55 | def has_response?(metadata)
56 | metadata.any? do |key, value|
57 | [:response_status, :response_body, :response_headers, :response_content_type].include?(key) && value
58 | end
59 | end
60 |
61 | def indent(string)
62 | string.tap do |str|
63 | str.gsub!(/\n/, "\n" + (" " * TOTAL_SPACES_INDENTATION)) if str
64 | end
65 | end
66 |
67 | # http_call: the hash that contains all information about the HTTP
68 | # request and response.
69 | # message_direction: either `request` or `response`.
70 | def body_to_json(http_call, message_direction)
71 | content_type = http_call["#{message_direction}_content_type".to_sym]
72 | body = http_call["#{message_direction}_body".to_sym] # e.g request_body
73 |
74 | if json?(content_type) && body
75 | body = JSON.pretty_generate(JSON.parse(body))
76 | end
77 |
78 | body
79 | end
80 |
81 | # JSON requests should use UTF-8 by default according to
82 | # http://www.ietf.org/rfc/rfc4627.txt, so we will remove `charset=utf-8`
83 | # when we find it to remove noise.
84 | def remove_utf8_for_json(headers)
85 | return unless headers
86 | headers
87 | .split("\n")
88 | .map { |header|
89 | header.gsub!(/; *charset=utf-8/, "") if json?(header)
90 | header
91 | }
92 | .join("\n")
93 | end
94 |
95 | def content_type(headers)
96 | headers && headers.fetch("Content-Type", nil)
97 | end
98 |
99 | def json?(string)
100 | string =~ /application\/.*json/
101 | end
102 | end
103 | end
104 | end
105 |
--------------------------------------------------------------------------------
/spec/api_documentation_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe RspecApiDocumentation::ApiDocumentation do
4 | let(:configuration) { RspecApiDocumentation::Configuration.new }
5 | let(:documentation) { RspecApiDocumentation::ApiDocumentation.new(configuration) }
6 |
7 | subject { documentation }
8 |
9 | its(:configuration) { should equal(configuration) }
10 | its(:index) { should be_a(RspecApiDocumentation::Index) }
11 |
12 | describe "#clear_docs" do
13 |
14 | it "should rebuild the docs directory" do
15 | test_file = configuration.docs_dir.join("test")
16 | FileUtils.mkdir_p configuration.docs_dir
17 | FileUtils.touch test_file
18 | allow(FileUtils).to receive(:cp_r)
19 | subject.clear_docs
20 |
21 | expect(File.directory?(configuration.docs_dir)).to be_truthy
22 | expect(File.exists?(test_file)).to be_falsey
23 | end
24 | end
25 |
26 | describe "#document_example" do
27 | let(:metadata) {{ :should_document => true }}
28 | let(:group) { RSpec::Core::ExampleGroup.describe("test group") }
29 | let(:example) { group.example("test example", metadata) }
30 | let!(:wrapped_example) { RspecApiDocumentation::Example.new(example, configuration) }
31 |
32 | before do
33 | allow(RspecApiDocumentation::Example).to receive(:new).and_return(wrapped_example)
34 | end
35 |
36 | it "should create a new wrapped example" do
37 | expect(RspecApiDocumentation::Example).to receive(:new).with(example, configuration).and_return(wrapped_example)
38 | documentation.document_example(example)
39 | end
40 |
41 | context "when the given example should be documented" do
42 | before { allow(wrapped_example).to receive(:should_document?).and_return(true) }
43 |
44 | it "should add the wrapped example to the index" do
45 | documentation.document_example(example)
46 | expect(documentation.index.examples).to eq([wrapped_example])
47 | end
48 | end
49 |
50 | context "when the given example should not be documented" do
51 | before { allow(wrapped_example).to receive(:should_document?).and_return(false) }
52 |
53 | it "should not add the wrapped example to the index" do
54 | documentation.document_example(example)
55 | expect(documentation.index.examples).to be_empty
56 | end
57 | end
58 | end
59 |
60 | describe "#writers" do
61 | class RspecApiDocumentation::Writers::HtmlWriter; end
62 | class RspecApiDocumentation::Writers::JsonWriter; end
63 | class RspecApiDocumentation::Writers::TextileWriter; end
64 |
65 | context "multiple" do
66 | before do
67 | configuration.format = [:html, :json, :textile]
68 | end
69 |
70 | it "should return the classes from format" do
71 | expect(subject.writers).to eq([RspecApiDocumentation::Writers::HtmlWriter,
72 | RspecApiDocumentation::Writers::JsonWriter,
73 | RspecApiDocumentation::Writers::TextileWriter])
74 | end
75 | end
76 |
77 | context "single" do
78 | before do
79 | configuration.format = :html
80 | end
81 |
82 | it "should return the classes from format" do
83 | expect(subject.writers).to eq([RspecApiDocumentation::Writers::HtmlWriter])
84 | end
85 | end
86 | end
87 |
88 | describe "#write" do
89 | let(:html_writer) { double(:html_writer) }
90 | let(:json_writer) { double(:json_writer) }
91 | let(:textile_writer) { double(:textile_writer) }
92 |
93 | before do
94 | allow(subject).to receive(:writers).and_return([html_writer, json_writer, textile_writer])
95 | end
96 |
97 | it "should write the docs in each format" do
98 | expect(html_writer).to receive(:write).with(subject.index, configuration)
99 | expect(json_writer).to receive(:write).with(subject.index, configuration)
100 | expect(textile_writer).to receive(:write).with(subject.index, configuration)
101 | subject.write
102 | end
103 | end
104 | end
105 |
--------------------------------------------------------------------------------
/lib/rspec_api_documentation/dsl/resource.rb:
--------------------------------------------------------------------------------
1 | module RspecApiDocumentation::DSL
2 | # DSL methods available at the example group level
3 | module Resource
4 | extend ActiveSupport::Concern
5 |
6 | module ClassMethods
7 | def self.define_action(method)
8 | define_method method do |*args, &block|
9 | options = args.extract_options!
10 | options[:method] = method
11 | if metadata[:route_uri]
12 | options[:route] = metadata[:route_uri]
13 | options[:action_name] = args.first
14 | else
15 | options[:route] = args.first
16 | end
17 | options[:api_doc_dsl] = :endpoint
18 | args.push(options)
19 | args[0] = "#{method.to_s.upcase} #{args[0]}"
20 | context(*args, &block)
21 | end
22 | end
23 |
24 | define_action :get
25 | define_action :post
26 | define_action :put
27 | define_action :delete
28 | define_action :head
29 | define_action :patch
30 |
31 | def callback(*args, &block)
32 | begin
33 | require 'webmock'
34 | rescue LoadError
35 | raise "Callbacks require webmock to be installed"
36 | end
37 | self.send(:include, WebMock::API)
38 |
39 | options = if args.last.is_a?(Hash) then args.pop else {} end
40 | options[:api_doc_dsl] = :callback
41 | args.push(options)
42 |
43 | context(*args, &block)
44 | end
45 |
46 | def route(*args, &block)
47 | raise "You must define the route URI" if args[0].blank?
48 | raise "You must define the route name" if args[1].blank?
49 | options = args.extract_options!
50 | options[:route_uri] = args[0].gsub(/\{.*\}/, "")
51 | options[:route_optionals] = (optionals = args[0].match(/(\{.*\})/) and optionals[-1])
52 | options[:route_name] = args[1]
53 | args.push(options)
54 | context(*args, &block)
55 | end
56 |
57 | def parameter(name, *args)
58 | parameters.push(field_specification(name, *args))
59 | end
60 |
61 | def attribute(name, *args)
62 | attributes.push(field_specification(name, *args))
63 | end
64 |
65 | def response_field(name, *args)
66 | response_fields.push(field_specification(name, *args))
67 | end
68 |
69 | def header(name, value)
70 | headers[name] = value
71 | end
72 |
73 | def explanation(text)
74 | safe_metadata(:resource_explanation, text)
75 | end
76 |
77 | private
78 |
79 | def field_specification(name, *args)
80 | options = args.extract_options!
81 | description = args.pop || "#{Array(options[:scope]).join(" ")} #{name}".humanize
82 |
83 | options.merge(:name => name.to_s, :description => description)
84 | end
85 |
86 | def safe_metadata(field, default)
87 | metadata[field] ||= default
88 | if superclass_metadata && metadata[field].equal?(superclass_metadata[field])
89 | metadata[field] = Marshal.load(Marshal.dump(superclass_metadata[field]))
90 | end
91 | metadata[field]
92 | end
93 |
94 | def parameters
95 | safe_metadata(:parameters, [])
96 | end
97 |
98 | def attributes
99 | safe_metadata(:attributes, [])
100 | end
101 |
102 | def response_fields
103 | safe_metadata(:response_fields, [])
104 | end
105 |
106 | def headers
107 | safe_metadata(:headers, {})
108 | end
109 |
110 | def parameter_keys
111 | parameters.map { |param| param[:name] }
112 | end
113 | end
114 |
115 | def app
116 | RspecApiDocumentation.configuration.app
117 | end
118 |
119 | def client
120 | @client ||= RspecApiDocumentation::RackTestClient.new(self)
121 | end
122 |
123 | def no_doc(&block)
124 | requests = example.metadata[:requests]
125 | example.metadata[:requests] = []
126 |
127 | instance_eval(&block)
128 |
129 | example.metadata[:requests] = requests
130 | end
131 | end
132 | end
133 |
--------------------------------------------------------------------------------
/spec/views/api_blueprint_example_spec.rb:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | require 'spec_helper'
3 |
4 | describe RspecApiDocumentation::Views::ApiBlueprintExample do
5 | let(:metadata) { { :resource_name => "Orders" } }
6 | let(:group) { RSpec::Core::ExampleGroup.describe("Orders", metadata) }
7 | let(:rspec_example) { group.example("Ordering a cup of coffee") {} }
8 | let(:rad_example) do
9 | RspecApiDocumentation::Example.new(rspec_example, configuration)
10 | end
11 | let(:configuration) { RspecApiDocumentation::Configuration.new }
12 | let(:html_example) { described_class.new(rad_example, configuration) }
13 |
14 | let(:content_type) { "application/json; charset=utf-8" }
15 | let(:requests) do
16 | [{
17 | request_body: "{}",
18 | request_headers: {
19 | "Content-Type" => content_type,
20 | "Another" => "header; charset=utf-8"
21 | },
22 | request_content_type: "",
23 | response_body: "{}",
24 | response_headers: {
25 | "Content-Type" => content_type,
26 | "Another" => "header; charset=utf-8"
27 | },
28 | response_content_type: ""
29 | }]
30 | end
31 |
32 | before do
33 | rspec_example.metadata[:requests] = requests
34 | end
35 |
36 | subject(:view) { described_class.new(rad_example, configuration) }
37 |
38 | describe '#requests' do
39 | describe 'request_content_type' do
40 | subject { view.requests[0][:request_content_type] }
41 |
42 | context 'when charset=utf-8 is present' do
43 | it "just strips that because it's the default for json" do
44 | expect(subject).to eq "application/json"
45 | end
46 | end
47 |
48 | context 'when charset=utf-16 is present' do
49 | let(:content_type) { "application/json; charset=utf-16" }
50 |
51 | it "keeps that because it's NOT the default for json" do
52 | expect(subject).to eq "application/json; charset=utf-16"
53 | end
54 | end
55 | end
56 |
57 | describe 'request_headers_text' do
58 | subject { view.requests[0][:request_headers_text] }
59 |
60 | context 'when charset=utf-8 is present' do
61 | it "just strips that because it's the default for json" do
62 | expect(subject).to eq "Content-Type: application/json\n Another: header; charset=utf-8"
63 | end
64 | end
65 |
66 | context 'when charset=utf-16 is present' do
67 | let(:content_type) { "application/json; charset=utf-16" }
68 |
69 | it "keeps that because it's NOT the default for json" do
70 | expect(subject).to eq "Content-Type: application/json; charset=utf-16\n Another: header; charset=utf-8"
71 | end
72 | end
73 | end
74 |
75 | describe 'response_content_type' do
76 | subject { view.requests[0][:response_content_type] }
77 |
78 | context 'when charset=utf-8 is present' do
79 | it "just strips that because it's the default for json" do
80 | expect(subject).to eq "application/json"
81 | end
82 | end
83 |
84 | context 'when charset=utf-16 is present' do
85 | let(:content_type) { "application/json; charset=utf-16" }
86 |
87 | it "keeps that because it's NOT the default for json" do
88 | expect(subject).to eq "application/json; charset=utf-16"
89 | end
90 | end
91 | end
92 |
93 | describe 'response_headers_text' do
94 | subject { view.requests[0][:response_headers_text] }
95 |
96 | context 'when charset=utf-8 is present' do
97 | it "just strips that because it's the default for json" do
98 | expect(subject).to eq "Content-Type: application/json\n Another: header; charset=utf-8"
99 | end
100 | end
101 |
102 | context 'when charset=utf-16 is present' do
103 | let(:content_type) { "application/json; charset=utf-16" }
104 |
105 | it "keeps that because it's NOT the default for json" do
106 | expect(subject).to eq "Content-Type: application/json; charset=utf-16\n Another: header; charset=utf-8"
107 | end
108 | end
109 | end
110 | end
111 | end
112 |
--------------------------------------------------------------------------------
/lib/rspec_api_documentation/dsl/endpoint.rb:
--------------------------------------------------------------------------------
1 | require 'rspec/core/formatters/base_formatter'
2 | require 'rack/utils'
3 | require 'rack/test/utils'
4 | require 'rspec_api_documentation/dsl/endpoint/params'
5 |
6 | module RspecApiDocumentation::DSL
7 | # DSL methods available inside the RSpec example.
8 | module Endpoint
9 | extend ActiveSupport::Concern
10 | include Rack::Test::Utils
11 |
12 | URL_PARAMS_REGEX = /[:\{](\w+)\}?/.freeze
13 |
14 | delegate :response_headers, :response_status, :response_body, :to => :rspec_api_documentation_client
15 |
16 | module ClassMethods
17 | def example_request(description, params = {}, &block)
18 | example description, :caller => block.send(:caller) do
19 | do_request(params)
20 | instance_eval &block if block_given?
21 | end
22 | end
23 |
24 | private
25 |
26 | # from rspec-core
27 | def relative_path(line)
28 | line = line.sub(File.expand_path("."), ".")
29 | line = line.sub(/\A([^:]+:\d+)$/, '\\1')
30 | return nil if line == '-e:1'
31 | line
32 | end
33 | end
34 |
35 | def do_request(extra_params = {})
36 | @extra_params = extra_params
37 |
38 | params_or_body = nil
39 | path_or_query = path
40 |
41 | if http_method == :get && !query_string.blank?
42 | path_or_query += "?#{query_string}"
43 | else
44 | if respond_to?(:raw_post)
45 | params_or_body = raw_post
46 | else
47 | formatter = RspecApiDocumentation.configuration.request_body_formatter
48 | case formatter
49 | when :json
50 | params_or_body = params.empty? ? nil : params.to_json
51 | when :xml
52 | params_or_body = params.to_xml
53 | when Proc
54 | params_or_body = formatter.call(params)
55 | else
56 | params_or_body = params
57 | end
58 | end
59 | end
60 |
61 | rspec_api_documentation_client.send(http_method, path_or_query, params_or_body, headers)
62 | end
63 |
64 | def query_string
65 | build_nested_query(params || {})
66 | end
67 |
68 | def params
69 | Params.new(self, example, extra_params).call
70 | end
71 |
72 | def header(name, value)
73 | example.metadata[:headers] ||= {}
74 | example.metadata[:headers][name] = value
75 | end
76 |
77 | def headers
78 | return unless example.metadata[:headers]
79 | example.metadata[:headers].inject({}) do |hash, (header, value)|
80 | if value.is_a?(Symbol)
81 | hash[header] = send(value) if respond_to?(value)
82 | else
83 | hash[header] = value
84 | end
85 | hash
86 | end
87 | end
88 |
89 | def http_method
90 | example.metadata[:method]
91 | end
92 |
93 | def method
94 | http_method
95 | end
96 |
97 | def status
98 | rspec_api_documentation_client.status
99 | end
100 |
101 | def in_path?(param)
102 | path_params.include?(param)
103 | end
104 |
105 | def path_params
106 | example.metadata[:route].scan(URL_PARAMS_REGEX).flatten
107 | end
108 |
109 | def path
110 | example.metadata[:route].gsub(URL_PARAMS_REGEX) do |match|
111 | if extra_params.keys.include?($1)
112 | delete_extra_param($1)
113 | elsif respond_to?($1)
114 | escape send($1)
115 | else
116 | escape match
117 | end
118 | end
119 | end
120 |
121 | def explanation(text)
122 | example.metadata[:explanation] = text
123 | end
124 |
125 | def example
126 | RSpec.current_example
127 | end
128 |
129 | private
130 |
131 | def rspec_api_documentation_client
132 | send(RspecApiDocumentation.configuration.client_method)
133 | end
134 |
135 | def extra_params
136 | return {} if @extra_params.nil?
137 | @extra_params.inject({}) do |h, (k, v)|
138 | v = v.is_a?(Hash) ? v.stringify_keys : v
139 | h[k.to_s] = v
140 | h
141 | end
142 | end
143 |
144 | def delete_extra_param(key)
145 | @extra_params.delete(key.to_sym) || @extra_params.delete(key.to_s)
146 | end
147 |
148 | end
149 | end
150 |
--------------------------------------------------------------------------------
/features/json_iodocs.feature:
--------------------------------------------------------------------------------
1 | Feature: Json Iodocs
2 | In order to serve the docs from my API
3 | As Zipmark
4 | I want to generate text files for each of my resources containing their combined docs
5 |
6 | Background:
7 | Given a file named "app.rb" with:
8 | """
9 | class App
10 | def self.call(env)
11 | request = Rack::Request.new(env)
12 | response = Rack::Response.new
13 | response["Content-Type"] = "text/plain"
14 | response.write("Hello, #{request.params["target"]}!")
15 | response.finish
16 | end
17 | end
18 | """
19 | And a file named "app_spec.rb" with:
20 | """
21 | require "rspec_api_documentation"
22 | require "rspec_api_documentation/dsl"
23 |
24 | RspecApiDocumentation.configure do |config|
25 | config.app = App
26 | config.api_name = "app"
27 | config.api_explanation = "desc"
28 | config.format = :json_iodocs
29 | config.io_docs_protocol = "https"
30 | end
31 |
32 | resource "Greetings" do
33 | get "/greetings" do
34 | parameter :target, "The thing you want to greet"
35 |
36 | example "Greeting your favorite gem" do
37 | do_request :target => "rspec_api_documentation"
38 |
39 | expect(response_headers["Content-Type"]).to eq("text/plain")
40 | expect(status).to eq(200)
41 | expect(response_body).to eq('Hello, rspec_api_documentation!')
42 | end
43 |
44 | example "Greeting your favorite developers of your favorite gem" do
45 | do_request :target => "Sam & Eric"
46 |
47 | expect(response_headers["Content-Type"]).to eq("text/plain")
48 | expect(status).to eq(200)
49 | expect(response_body).to eq('Hello, Sam & Eric!')
50 | end
51 | end
52 | end
53 | """
54 | When I run `rspec app_spec.rb --require ./app.rb --format RspecApiDocumentation::ApiFormatter`
55 |
56 | Scenario: Output helpful progress to the console
57 | Then the output should contain:
58 | """
59 | Generating API Docs
60 | Greetings
61 | GET /greetings
62 | * Greeting your favorite gem
63 | * Greeting your favorite developers of your favorite gem
64 | """
65 | And the output should contain "2 examples, 0 failures"
66 | And the exit status should be 0
67 |
68 | Scenario: File should look like we expect
69 | Then the file "doc/api/apiconfig.json" should contain JSON exactly like:
70 | """
71 | {
72 | "app" : {
73 | "name" : "app",
74 | "description": "desc",
75 | "protocol" : "https",
76 | "publicPath" : "",
77 | "baseURL" : null
78 | }
79 | }
80 | """
81 | Then the file "doc/api/app.json" should contain JSON exactly like:
82 | """
83 | {
84 | "endpoints": [
85 | {
86 | "name": "Greetings",
87 | "methods": [
88 | {
89 | "MethodName": "Greeting your favorite developers of your favorite gem",
90 | "Synopsis": null,
91 | "HTTPMethod": "GET",
92 | "URI": "/greetings?target=Sam+%26+Eric",
93 | "RequiresOAuth": "N",
94 | "parameters": [
95 | {
96 | "Name": "target",
97 | "Description": "The thing you want to greet",
98 | "Default": "",
99 | "Required": "N"
100 | }
101 | ]
102 | },
103 | {
104 | "MethodName": "Greeting your favorite gem",
105 | "Synopsis": null,
106 | "HTTPMethod": "GET",
107 | "URI": "/greetings?target=rspec_api_documentation",
108 | "RequiresOAuth": "N",
109 | "parameters": [
110 | {
111 | "Name": "target",
112 | "Description": "The thing you want to greet",
113 | "Default": "",
114 | "Required": "N"
115 | }
116 | ]
117 | }
118 | ]
119 | }
120 | ]
121 | }
122 | """
123 |
124 |
--------------------------------------------------------------------------------
/features/combined_text.feature:
--------------------------------------------------------------------------------
1 | Feature: Combined text
2 | In order to serve the docs from my API
3 | As Zipmark
4 | I want to generate text files for each of my resources containing their combined docs
5 |
6 | Background:
7 | Given a file named "app.rb" with:
8 | """
9 | class App
10 | def self.call(env)
11 | request = Rack::Request.new(env)
12 | response = Rack::Response.new
13 | response["Content-Type"] = "text/plain"
14 | response.write("Hello, #{request.params["target"]}!")
15 | response.finish
16 | end
17 | end
18 | """
19 | And a file named "app_spec.rb" with:
20 | """
21 | require "rspec_api_documentation"
22 | require "rspec_api_documentation/dsl"
23 |
24 | RspecApiDocumentation.configure do |config|
25 | config.app = App
26 | config.format = :combined_text
27 | end
28 |
29 | resource "Greetings" do
30 | get "/greetings" do
31 | parameter :target, "The thing you want to greet"
32 |
33 | example "Greeting your favorite gem" do
34 | do_request :target => "rspec_api_documentation"
35 |
36 | expect(response_headers["Content-Type"]).to eq("text/plain")
37 | expect(status).to eq(200)
38 | expect(response_body).to eq('Hello, rspec_api_documentation!')
39 | end
40 |
41 | example "Greeting your favorite developers of your favorite gem" do
42 | do_request :target => "Sam & Eric"
43 |
44 | expect(response_headers["Content-Type"]).to eq("text/plain")
45 | expect(status).to eq(200)
46 | expect(response_body).to eq('Hello, Sam & Eric!')
47 | end
48 |
49 | example "Multiple Requests" do
50 | do_request :target => "Sam"
51 | do_request :target => "Eric"
52 | end
53 | end
54 | end
55 | """
56 | When I run `rspec app_spec.rb --require ./app.rb --format RspecApiDocumentation::ApiFormatter`
57 |
58 | Scenario: Output helpful progress to the console
59 | Then the output should contain:
60 | """
61 | Generating API Docs
62 | Greetings
63 | GET /greetings
64 | * Greeting your favorite gem
65 | * Greeting your favorite developers of your favorite gem
66 | """
67 | And the output should contain "3 examples, 0 failures"
68 | And the exit status should be 0
69 |
70 | Scenario: File should look like we expect
71 | Then the file "doc/api/greetings/index.txt" should contain exactly:
72 | """
73 | Greeting your favorite gem
74 | --------------------------
75 |
76 | Parameters:
77 | * target - The thing you want to greet
78 |
79 | Request:
80 | GET /greetings?target=rspec_api_documentation
81 | Cookie:
82 | Host: example.org
83 |
84 | target=rspec_api_documentation
85 |
86 | Response:
87 | Status: 200 OK
88 | Content-Length: 31
89 | Content-Type: text/plain
90 |
91 | Hello, rspec_api_documentation!
92 |
93 |
94 | Greeting your favorite developers of your favorite gem
95 | ------------------------------------------------------
96 |
97 | Parameters:
98 | * target - The thing you want to greet
99 |
100 | Request:
101 | GET /greetings?target=Sam+%26+Eric
102 | Cookie:
103 | Host: example.org
104 |
105 | target=Sam & Eric
106 |
107 | Response:
108 | Status: 200 OK
109 | Content-Length: 18
110 | Content-Type: text/plain
111 |
112 | Hello, Sam & Eric!
113 |
114 |
115 | Multiple Requests
116 | -----------------
117 |
118 | Parameters:
119 | * target - The thing you want to greet
120 |
121 | Request:
122 | GET /greetings?target=Sam
123 | Cookie:
124 | Host: example.org
125 |
126 | target=Sam
127 |
128 | Response:
129 | Status: 200 OK
130 | Content-Length: 11
131 | Content-Type: text/plain
132 |
133 | Hello, Sam!
134 |
135 | Request:
136 | GET /greetings?target=Eric
137 | Cookie:
138 | Host: example.org
139 |
140 | target=Eric
141 |
142 | Response:
143 | Status: 200 OK
144 | Content-Length: 12
145 | Content-Type: text/plain
146 |
147 | Hello, Eric!
148 | """
149 |
150 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: .
3 | specs:
4 | rspec_api_documentation (5.1.0)
5 | activesupport (>= 3.0.0)
6 | mustache (~> 1.0, >= 0.99.4)
7 | rspec (~> 3.0)
8 |
9 | GEM
10 | remote: http://rubygems.org/
11 | specs:
12 | activesupport (4.2.5.1)
13 | i18n (~> 0.7)
14 | json (~> 1.7, >= 1.7.7)
15 | minitest (~> 5.1)
16 | thread_safe (~> 0.3, >= 0.3.4)
17 | tzinfo (~> 1.1)
18 | addressable (2.4.0)
19 | aruba (0.13.0)
20 | childprocess (~> 0.5.6)
21 | contracts (~> 0.9)
22 | cucumber (>= 1.3.19)
23 | ffi (~> 1.9.10)
24 | rspec-expectations (>= 2.99)
25 | thor (~> 0.19)
26 | attr_required (1.0.1)
27 | builder (3.2.2)
28 | capybara (2.6.2)
29 | addressable
30 | mime-types (>= 1.16)
31 | nokogiri (>= 1.3.3)
32 | rack (>= 1.0.0)
33 | rack-test (>= 0.5.4)
34 | xpath (~> 2.0)
35 | childprocess (0.5.9)
36 | ffi (~> 1.0, >= 1.0.11)
37 | coderay (1.1.0)
38 | contracts (0.13.0)
39 | crack (0.4.3)
40 | safe_yaml (~> 1.0.0)
41 | cucumber (2.3.2)
42 | builder (>= 2.1.2)
43 | cucumber-core (~> 1.4.0)
44 | cucumber-wire (~> 0.0.1)
45 | diff-lcs (>= 1.1.3)
46 | gherkin (~> 3.2.0)
47 | multi_json (>= 1.7.5, < 2.0)
48 | multi_test (>= 0.1.2)
49 | cucumber-core (1.4.0)
50 | gherkin (~> 3.2.0)
51 | cucumber-wire (0.0.1)
52 | daemons (1.2.3)
53 | diff-lcs (1.2.5)
54 | eventmachine (1.0.9.1)
55 | fakefs (0.6.0)
56 | faraday (0.9.2)
57 | multipart-post (>= 1.2, < 3)
58 | ffi (1.9.10)
59 | gherkin (3.2.0)
60 | hashdiff (0.2.3)
61 | httpclient (2.7.1)
62 | i18n (0.7.0)
63 | inch (0.7.0)
64 | pry
65 | sparkr (>= 0.2.0)
66 | term-ansicolor
67 | yard (~> 0.8.7.5)
68 | json (1.8.3)
69 | method_source (0.8.2)
70 | mime-types (3.0)
71 | mime-types-data (~> 3.2015)
72 | mime-types-data (3.2015.1120)
73 | mini_portile2 (2.0.0)
74 | minitest (5.8.4)
75 | multi_json (1.11.2)
76 | multi_test (0.1.2)
77 | multipart-post (2.0.0)
78 | mustache (1.0.3)
79 | nokogiri (1.6.7.2)
80 | mini_portile2 (~> 2.0.0.rc2)
81 | pry (0.10.3)
82 | coderay (~> 1.1.0)
83 | method_source (~> 0.8.1)
84 | slop (~> 3.4)
85 | rack (1.6.4)
86 | rack-oauth2 (1.2.2)
87 | activesupport (>= 2.3)
88 | attr_required (>= 0.0.5)
89 | httpclient (>= 2.4)
90 | multi_json (>= 1.3.6)
91 | rack (>= 1.1)
92 | rack-protection (1.5.3)
93 | rack
94 | rack-test (0.6.3)
95 | rack (>= 1.0)
96 | rake (10.5.0)
97 | rspec (3.4.0)
98 | rspec-core (~> 3.4.0)
99 | rspec-expectations (~> 3.4.0)
100 | rspec-mocks (~> 3.4.0)
101 | rspec-core (3.4.2)
102 | rspec-support (~> 3.4.0)
103 | rspec-expectations (3.4.0)
104 | diff-lcs (>= 1.2.0, < 2.0)
105 | rspec-support (~> 3.4.0)
106 | rspec-its (1.2.0)
107 | rspec-core (>= 3.0.0)
108 | rspec-expectations (>= 3.0.0)
109 | rspec-mocks (3.4.1)
110 | diff-lcs (>= 1.2.0, < 2.0)
111 | rspec-support (~> 3.4.0)
112 | rspec-support (3.4.1)
113 | safe_yaml (1.0.4)
114 | sinatra (1.4.7)
115 | rack (~> 1.5)
116 | rack-protection (~> 1.4)
117 | tilt (>= 1.3, < 3)
118 | slop (3.6.0)
119 | sparkr (0.4.1)
120 | term-ansicolor (1.3.2)
121 | tins (~> 1.0)
122 | thin (1.6.4)
123 | daemons (~> 1.0, >= 1.0.9)
124 | eventmachine (~> 1.0, >= 1.0.4)
125 | rack (~> 1.0)
126 | thor (0.19.1)
127 | thread_safe (0.3.5)
128 | tilt (2.0.2)
129 | tins (1.8.2)
130 | tzinfo (1.2.2)
131 | thread_safe (~> 0.1)
132 | webmock (1.22.6)
133 | addressable (>= 2.3.6)
134 | crack (>= 0.3.2)
135 | hashdiff
136 | xpath (2.0.0)
137 | nokogiri (~> 1.3)
138 | yard (0.8.7.6)
139 |
140 | PLATFORMS
141 | ruby
142 |
143 | DEPENDENCIES
144 | aruba (~> 0.5)
145 | bundler (~> 1.0)
146 | capybara (~> 2.2)
147 | fakefs (~> 0.4)
148 | faraday (~> 0.9, >= 0.9.0)
149 | inch
150 | rack-oauth2 (~> 1.2.2, >= 1.0.7)
151 | rack-test (~> 0.6.2)
152 | rake (~> 10.1)
153 | rspec-its (~> 1.0)
154 | rspec_api_documentation!
155 | sinatra (~> 1.4, >= 1.4.4)
156 | thin (~> 1.6, >= 1.6.3)
157 | webmock (~> 1.7)
158 |
159 | BUNDLED WITH
160 | 1.15.3
161 |
--------------------------------------------------------------------------------
/spec/configuration_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe RspecApiDocumentation::Configuration do
4 | let(:parent) { nil }
5 | let(:configuration) { RspecApiDocumentation::Configuration.new(parent) }
6 |
7 | subject { configuration }
8 |
9 | its(:parent) { should equal(parent) }
10 | its(:settings) { should == {} }
11 | its(:groups) { should == [] }
12 |
13 | describe ".add_setting" do
14 | it 'should allow creating a new setting' do
15 | RspecApiDocumentation::Configuration.add_setting :new_setting
16 | expect(configuration).to respond_to(:new_setting)
17 | expect(configuration).to respond_to(:new_setting=)
18 | end
19 |
20 | it 'should allow setting a default' do
21 | RspecApiDocumentation::Configuration.add_setting :new_setting, :default => "default"
22 | expect(configuration.new_setting).to eq("default")
23 | end
24 |
25 | it "should allow the default setting to be a lambda" do
26 | RspecApiDocumentation::Configuration.add_setting :another_setting, :default => lambda { |config| config.new_setting }
27 | expect(configuration.another_setting).to eq("default")
28 | end
29 | end
30 |
31 | describe "default settings" do
32 | let(:default_template_path) { File.expand_path("../../templates", __FILE__) }
33 |
34 | context "when Rails is defined" do
35 | let(:rails_root) { Pathname.new("tmp") }
36 | let(:rails_app) { double(:rails_app) }
37 |
38 | before { Rails = double(:application => rails_app, :root => rails_root) }
39 | after { Object.send(:remove_const, :Rails) }
40 |
41 | its(:docs_dir) { should == rails_root.join("doc", "api") }
42 | its(:app) { should == rails_app }
43 | end
44 |
45 | its(:docs_dir) { should == Pathname.new("doc/api") }
46 | its(:format) { should == :html }
47 | its(:template_path) { should == default_template_path }
48 | its(:filter) { should == :all }
49 | its(:exclusion_filter) { should be_nil }
50 | its(:app) { should be_nil }
51 | its(:curl_headers_to_filter) { should be_nil }
52 | its(:curl_host) { should be_nil }
53 | its(:keep_source_order) { should be_falsey }
54 | its(:api_name) { should == "API Documentation" }
55 | its(:api_explanation) { should be_nil }
56 | its(:client_method) { should == :client }
57 | its(:io_docs_protocol) { should == "http" }
58 | its(:request_headers_to_include) { should be_nil }
59 | its(:response_headers_to_include) { should be_nil }
60 | its(:html_embedded_css_file) { should be_nil }
61 |
62 | specify "post body formatter" do
63 | expect(configuration.request_body_formatter.call({ :page => 1})).to eq({ :page => 1 })
64 | end
65 | end
66 |
67 | describe "#define_groups" do
68 | it "should take a block" do
69 | called = false
70 | subject.define_group(:foo) { called = true }
71 | expect(called).to eq(true)
72 | end
73 |
74 | it "should yield a sub-configuration" do
75 | subject.define_group(:foo) do |config|
76 | expect(config).to be_a(described_class)
77 | expect(config.parent).to equal(subject)
78 | end
79 | end
80 |
81 | it "should set the sub-configuration filter" do
82 | subject.define_group(:foo) do |config|
83 | expect(config.filter).to eq(:foo)
84 | end
85 | end
86 |
87 | it "should inherit its parents configurations" do
88 | subject.format = :json
89 | subject.define_group(:sub) do |config|
90 | expect(config.format).to eq(:json)
91 | end
92 | end
93 |
94 | it "should scope the documentation directory" do
95 | subject.define_group(:sub) do |config|
96 | expect(config.docs_dir).to eq(subject.docs_dir.join('sub'))
97 | end
98 | end
99 | end
100 |
101 | it { expect(subject).to be_a(Enumerable) }
102 |
103 | it "should enumerate through recursively and include self" do
104 | configs = [subject]
105 | subject.define_group(:sub1) do |config|
106 | configs << config
107 | config.define_group(:sub2) do |config|
108 | configs << config
109 | config.define_group(:sub3) do |config|
110 | configs << config
111 | end
112 | end
113 | end
114 | expect(subject.to_a).to eq(configs)
115 | end
116 |
117 | describe "#groups" do
118 | it "should list all of the defined groups" do
119 | subject.define_group(:sub) do |config|
120 | end
121 |
122 | expect(subject.groups.count).to eq(1)
123 | end
124 | end
125 | end
126 |
--------------------------------------------------------------------------------
/templates/rspec_api_documentation/html_example.mustache:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{{ resource_explanation }}}
16 | {{/ resource_explanation }} 17 | 18 |23 | {{{ explanation }}} 24 |
25 | {{/ explanation }} 26 | 27 | {{# has_parameters? }} 28 || Name | 33 |Description | 34 |
|---|---|
| 40 | {{# scope }} 41 | {{ scope }}[{{ name }}] 42 | {{/ scope }} 43 | {{^ scope }} 44 | {{ name }} 45 | {{/ scope }} 46 | | 47 |48 | {{ description }} 49 | | 50 |
| Name | 62 |Description | 63 |
|---|---|
| 69 | {{# scope }} 70 | {{ scope }}[{{ name }}] 71 | {{/ scope }} 72 | {{^ scope }} 73 | {{ name }} 74 | {{/ scope }} 75 | | 76 |77 | {{ description }} 78 | | 79 |
{{ request_headers_text }}
91 | {{/ request_headers_text }}
92 |
93 | {{ request_method }} {{ request_path }}
95 |
96 | {{# request_query_parameters_text }}
97 | {{ request_query_parameters_text }}
99 | {{/ request_query_parameters_text }}
100 |
101 | {{# request_body }}
102 | {{{ request_body }}}
104 | {{/ request_body }}
105 |
106 | {{# curl }}
107 | {{ curl }}
109 | {{/ curl }}
110 |
111 | {{# response_status }}
112 | {{ response_headers_text }}
116 | {{/ response_headers_text }}
117 | {{ response_status }} {{ response_status_text}}
119 | {{# response_body }}
120 | {{ response_body }}
122 | {{/ response_body }}
123 | {{/ response_status }}
124 | {{/ requests }}
125 | Example API Description
" 28 | config.request_headers_to_include = %w[Cookie] 29 | config.response_headers_to_include = %w[Content-Type] 30 | end 31 | 32 | resource "Greetings" do 33 | get "/greetings" do 34 | parameter :target, "The thing you want to greet" 35 | parameter :scoped, "This is a scoped variable", :scope => :scope 36 | parameter :sub, "This is scoped", :scope => [:scope, :further] 37 | 38 | response_field :hello, "The greeted thing" 39 | response_field :message, "Translated greeting", scope: [:more_greetings, :bonjour] 40 | 41 | example "Greeting your favorite gem" do 42 | do_request :target => "rspec_api_documentation" 43 | 44 | expect(response_headers["Content-Type"]).to eq("application/json") 45 | expect(status).to eq(200) 46 | expect(response_body).to eq('{"hello":"rspec_api_documentation","more_greetings":{"bonjour":{"message":"le monde"}}}') 47 | end 48 | end 49 | end 50 | """ 51 | When I run `rspec app_spec.rb --require ./app.rb --format RspecApiDocumentation::ApiFormatter` 52 | 53 | Scenario: Output helpful progress to the console 54 | Then the output should contain: 55 | """ 56 | Generating API Docs 57 | Greetings 58 | GET /greetings 59 | * Greeting your favorite gem 60 | """ 61 | And the output should contain "1 example, 0 failures" 62 | And the exit status should be 0 63 | 64 | Scenario: Create an index of all API examples, including all resources 65 | When I open the index 66 | Then I should see the following resources: 67 | | Greetings | 68 | And I should see the api name "Example API" 69 | 70 | Scenario: Create an index with proper description 71 | When I open the index 72 | Then I should see the following resources: 73 | | Greetings | 74 | And I should see the api explanation "Example API Description" 75 | 76 | Scenario: Example HTML documentation includes the parameters 77 | When I open the index 78 | And I navigate to "Greeting your favorite gem" 79 | Then I should see the following parameters: 80 | | name | description | 81 | | target | The thing you want to greet | 82 | | scope[scoped] | This is a scoped variable | 83 | | scope[further][sub] | This is scoped | 84 | 85 | Scenario: Example HTML documentation should include the response fields 86 | When I open the index 87 | And I navigate to "Greeting your favorite gem" 88 | Then I should see the following response fields: 89 | | name | description | 90 | | hello | The greeted thing | 91 | | more_greetings[bonjour][message] | Translated greeting | 92 | 93 | Scenario: Example HTML documentation includes the request information 94 | When I open the index 95 | And I navigate to "Greeting your favorite gem" 96 | Then I should see the route is "GET /greetings?target=rspec_api_documentation" 97 | And I should see the following request headers: 98 | | Cookie | | 99 | And I should not see the following request headers: 100 | | Host | example.org | 101 | And I should see the following query parameters: 102 | | target | rspec_api_documentation | 103 | 104 | Scenario: Example HTML documentation includes the response information 105 | When I open the index 106 | And I navigate to "Greeting your favorite gem" 107 | Then I should see the response status is "200 OK" 108 | And I should see the following response headers: 109 | | Content-Type | application/json | 110 | And I should not see the following response headers: 111 | | Content-Length | 35 | 112 | And I should see the following response body: 113 | """ 114 | { "hello": "rspec_api_documentation", "more_greetings": { "bonjour": { "message": "le monde" } } } 115 | """ 116 | -------------------------------------------------------------------------------- /features/oauth2_mac_client.feature: -------------------------------------------------------------------------------- 1 | Feature: Use OAuth2 MAC client as a test client 2 | Background: 3 | Given a file named "app_spec.rb" with: 4 | """ 5 | require "rspec_api_documentation" 6 | require "rspec_api_documentation/dsl" 7 | require "rack/builder" 8 | 9 | RspecApiDocumentation.configure do |config| 10 | config.app = Rack::Builder.new do 11 | map "/oauth2/token" do 12 | app = lambda do |env| 13 | headers = {"Pragma"=>"no-cache", "Content-Type"=>"application/json", "Content-Length"=>"274", "Cache-Control"=>"no-store"} 14 | body = ["{\"mac_algorithm\":\"hmac-sha-256\",\"expires_in\":29,\"access_token\":\"HfIBIMe/hxNKSMogD33OJmLN+i9x3d2iM7WLzrN1RQvINOFz+QT8hiMiY+avbp2mc8IpzrxoupHyy0DeKuB05Q==\",\"token_type\":\"mac\",\"mac_key\":\"jb59zUztvDIC0AeaNZz+BptWvmFd4C41JyZS1DfWqKCkZTErxSMfkdjkePUcpE9/joqFt0ELyV/oIsFAf0V1ew==\"}"] 15 | [200, headers, body] 16 | end 17 | 18 | run app 19 | end 20 | 21 | map "/" do 22 | app = lambda do |env| 23 | if env["HTTP_AUTHORIZATION"].blank? 24 | return [401, {"Content-Type" => "text/plain"}, [""]] 25 | end 26 | 27 | request = Rack::Request.new(env) 28 | response = Rack::Response.new 29 | response["Content-Type"] = "text/plain" 30 | response.write("hello #{request.params["target"]}") 31 | response.finish 32 | end 33 | 34 | run app 35 | end 36 | 37 | map "/multiple" do 38 | app = lambda do |env| 39 | if env["HTTP_AUTHORIZATION"].blank? 40 | return [401, {"Content-Type" => "text/plain"}, [""]] 41 | end 42 | 43 | request = Rack::Request.new(env) 44 | response = Rack::Response.new 45 | response["Content-Type"] = "text/plain" 46 | response.write("hello #{request.params["targets"].join(", ")}") 47 | response.finish 48 | end 49 | 50 | run app 51 | end 52 | 53 | map "/multiple_nested" do 54 | app = lambda do |env| 55 | if env["HTTP_AUTHORIZATION"].blank? 56 | return [401, {"Content-Type" => "text/plain"}, [""]] 57 | end 58 | 59 | request = Rack::Request.new(env) 60 | response = Rack::Response.new 61 | response["Content-Type"] = "text/plain" 62 | response.write("hello #{request.params["targets"].sort.map {|company, products| company.to_s + ' with ' + products.join(' and ')}.join(", ")}") 63 | response.finish 64 | end 65 | 66 | run app 67 | end 68 | end 69 | end 70 | 71 | resource "Greetings" do 72 | let(:client) { RspecApiDocumentation::OAuth2MACClient.new(self, {:identifier => "1", :secret => "secret"}) } 73 | 74 | get "/" do 75 | parameter :target, "The thing you want to greet" 76 | 77 | example "Greeting your favorite gem" do 78 | do_request :target => "rspec_api_documentation" 79 | 80 | expect(response_headers["Content-Type"]).to eq("text/plain") 81 | expect(status).to eq(200) 82 | expect(response_body).to eq('hello rspec_api_documentation') 83 | end 84 | end 85 | 86 | get "/multiple" do 87 | parameter :targets, "The people you want to greet" 88 | 89 | let(:targets) { ["eric", "sam"] } 90 | 91 | example "Greeting your favorite people" do 92 | do_request 93 | 94 | expect(response_headers["Content-Type"]).to eq("text/plain") 95 | expect(status).to eq(200) 96 | expect(response_body).to eq("hello eric, sam") 97 | end 98 | end 99 | 100 | get "/multiple_nested" do 101 | parameter :targets, "The companies you want to greet" 102 | 103 | let(:targets) { { "apple" => ['mac', 'ios'], "google" => ['search', 'mail']} } 104 | 105 | example "Greeting your favorite companies" do 106 | do_request 107 | 108 | expect(response_headers["Content-Type"]).to eq("text/plain") 109 | expect(status).to eq(200) 110 | expect(response_body).to eq("hello apple with mac and ios, google with search and mail") 111 | end 112 | end 113 | 114 | end 115 | """ 116 | When I run `rspec app_spec.rb --format RspecApiDocumentation::ApiFormatter` 117 | 118 | Scenario: Output should contain 119 | Then the output should contain: 120 | """ 121 | Generating API Docs 122 | Greetings 123 | GET / 124 | * Greeting your favorite gem 125 | GET /multiple 126 | * Greeting your favorite people 127 | GET /multiple_nested 128 | * Greeting your favorite companies 129 | """ 130 | And the output should contain "3 examples, 0 failures" 131 | And the exit status should be 0 132 | --------------------------------------------------------------------------------