├── 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 | Example 5 | <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %> 6 | <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> 7 | <%= csrf_meta_tags %> 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /lib/rspec_api_documentation/views/textile_example.rb: -------------------------------------------------------------------------------- 1 | module RspecApiDocumentation 2 | module Views 3 | class TextileExample < MarkupExample 4 | EXTENSION = 'textile' 5 | 6 | def initialize(example, configuration) 7 | super 8 | self.template_name = "rspec_api_documentation/textile_example" 9 | end 10 | 11 | def extension 12 | EXTENSION 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /features/step_definitions/curl_steps.rb: -------------------------------------------------------------------------------- 1 | Then /the outputted docs should( not)? filter out headers$/ do |condition| 2 | visit "/foobars/getting_foo.html" 3 | 4 | within("pre.curl") do 5 | if condition 6 | expect(page).to have_content("Host") 7 | expect(page).to have_content("Cookie") 8 | else 9 | expect(page).to_not have_content("Host") 10 | expect(page).to_not have_content("Cookie") 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/rspec_api_documentation/views/textile_index.rb: -------------------------------------------------------------------------------- 1 | module RspecApiDocumentation 2 | module Views 3 | class TextileIndex < MarkupIndex 4 | def initialize(index, configuration) 5 | super 6 | self.template_name = "rspec_api_documentation/textile_index" 7 | end 8 | 9 | def examples 10 | @index.examples.map { |example| TextileExample.new(example, @configuration) } 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/rspec_api_documentation/views/markdown_index.rb: -------------------------------------------------------------------------------- 1 | module RspecApiDocumentation 2 | module Views 3 | class MarkdownIndex < MarkupIndex 4 | def initialize(index, configuration) 5 | super 6 | self.template_name = "rspec_api_documentation/markdown_index" 7 | end 8 | 9 | def examples 10 | @index.examples.map { |example| MarkdownExample.new(example, @configuration) } 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /example/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /lib/rspec_api_documentation/writers/html_writer.rb: -------------------------------------------------------------------------------- 1 | module RspecApiDocumentation 2 | module Writers 3 | class HtmlWriter < GeneralMarkupWriter 4 | EXTENSION = 'html' 5 | 6 | def markup_index_class 7 | RspecApiDocumentation::Views::HtmlIndex 8 | end 9 | 10 | def markup_example_class 11 | RspecApiDocumentation::Views::HtmlExample 12 | end 13 | 14 | def extension 15 | EXTENSION 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/index_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe RspecApiDocumentation::Index do 4 | let(:index) { RspecApiDocumentation::Index.new } 5 | 6 | subject { index } 7 | 8 | describe "#examples" do 9 | let(:examples) { [double(:example), double(:example)] } 10 | 11 | before do 12 | index.examples.push(*examples) 13 | end 14 | 15 | it "should contain all added examples" do 16 | expect(index.examples).to eq(examples) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/rspec_api_documentation/writers/textile_writer.rb: -------------------------------------------------------------------------------- 1 | module RspecApiDocumentation 2 | module Writers 3 | class TextileWriter < GeneralMarkupWriter 4 | EXTENSION = 'textile' 5 | 6 | def markup_index_class 7 | RspecApiDocumentation::Views::TextileIndex 8 | end 9 | 10 | def markup_example_class 11 | RspecApiDocumentation::Views::TextileExample 12 | end 13 | 14 | def extension 15 | EXTENSION 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/rspec_api_documentation/writers/markdown_writer.rb: -------------------------------------------------------------------------------- 1 | module RspecApiDocumentation 2 | module Writers 3 | class MarkdownWriter < GeneralMarkupWriter 4 | EXTENSION = 'markdown' 5 | 6 | def markup_index_class 7 | RspecApiDocumentation::Views::MarkdownIndex 8 | end 9 | 10 | def markup_example_class 11 | RspecApiDocumentation::Views::MarkdownExample 12 | end 13 | 14 | def extension 15 | EXTENSION 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/rspec_api_documentation/views/slate_example.rb: -------------------------------------------------------------------------------- 1 | module RspecApiDocumentation 2 | module Views 3 | class SlateExample < MarkdownExample 4 | def initialize(example, configuration) 5 | super 6 | self.template_name = "rspec_api_documentation/slate_example" 7 | end 8 | 9 | def parameters 10 | super.map do |parameter| 11 | parameter.merge({ 12 | :required => parameter[:required] == 'true' ? true : false, 13 | }) 14 | end 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore the default SQLite database. 11 | /db/*.sqlite3 12 | /db/*.sqlite3-journal 13 | 14 | # Ignore all logfiles and tempfiles. 15 | /log/*.log 16 | /tmp 17 | 18 | doc 19 | -------------------------------------------------------------------------------- /lib/tasks/docs.rake: -------------------------------------------------------------------------------- 1 | require 'rspec/core/rake_task' 2 | 3 | desc 'Generate API request documentation from API specs' 4 | RSpec::Core::RakeTask.new('docs:generate') do |t| 5 | t.pattern = 'spec/acceptance/**/*_spec.rb' 6 | t.rspec_opts = ["--format RspecApiDocumentation::ApiFormatter"] 7 | end 8 | 9 | desc 'Generate API request documentation from API specs (ordered)' 10 | RSpec::Core::RakeTask.new('docs:generate:ordered') do |t| 11 | t.pattern = 'spec/acceptance/**/*_spec.rb' 12 | t.rspec_opts = ["--format RspecApiDocumentation::ApiFormatter", "--order defined"] 13 | end 14 | -------------------------------------------------------------------------------- /spec/views/slate_example_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe RspecApiDocumentation::Views::SlateExample do 4 | let(:metadata) { { :resource_name => "Orders" } } 5 | let(:group) { RSpec::Core::ExampleGroup.describe("Orders", metadata) } 6 | let(:rspec_example) { group.example("Ordering a cup of coffee") {} } 7 | let(:rad_example) do 8 | RspecApiDocumentation::Example.new(rspec_example, configuration) 9 | end 10 | let(:configuration) { RspecApiDocumentation::Configuration.new } 11 | let(:slate_example) { described_class.new(rad_example, configuration) } 12 | 13 | end 14 | -------------------------------------------------------------------------------- /example/bin/spring: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This file loads spring without using Bundler, in order to be fast 4 | # It gets overwritten when you run the `spring binstub` command 5 | 6 | unless defined?(Spring) 7 | require "rubygems" 8 | require "bundler" 9 | 10 | if match = Bundler.default_lockfile.read.match(/^GEM$.*?^ spring \((.*?)\)$.*?^$/m) 11 | ENV["GEM_PATH"] = ([Bundler.bundle_path.to_s] + Gem.path).join(File::PATH_SEPARATOR) 12 | ENV["GEM_HOME"] = "" 13 | Gem.paths = ENV 14 | 15 | gem "spring", match[1] 16 | require "spring/binstub" 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/rspec_api_documentation/views/markup_index.rb: -------------------------------------------------------------------------------- 1 | require 'mustache' 2 | 3 | module RspecApiDocumentation 4 | module Views 5 | class MarkupIndex < Mustache 6 | delegate :api_name, :api_explanation, to: :@configuration, prefix: false 7 | 8 | def initialize(index, configuration) 9 | @index = index 10 | @configuration = configuration 11 | self.template_path = configuration.template_path 12 | end 13 | 14 | def sections 15 | RspecApiDocumentation::Writers::IndexHelper.sections(examples, @configuration) 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /example/README.rdoc: -------------------------------------------------------------------------------- 1 | == README 2 | 3 | This README would normally document whatever steps are necessary to get the 4 | application up and running. 5 | 6 | Things you may want to cover: 7 | 8 | * Ruby version 9 | 10 | * System dependencies 11 | 12 | * Configuration 13 | 14 | * Database creation 15 | 16 | * Database initialization 17 | 18 | * How to run the test suite 19 | 20 | * Services (job queues, cache servers, search engines, etc.) 21 | 22 | * Deployment instructions 23 | 24 | * ... 25 | 26 | 27 | Please feel free to use a different markup language if you do not plan to run 28 | rake doc:app. 29 | -------------------------------------------------------------------------------- /example/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] if respond_to?(:wrap_parameters) 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /lib/rspec_api_documentation/views/markdown_example.rb: -------------------------------------------------------------------------------- 1 | module RspecApiDocumentation 2 | module Views 3 | class MarkdownExample < MarkupExample 4 | EXTENSION = 'markdown' 5 | 6 | def initialize(example, configuration) 7 | super 8 | self.template_name = "rspec_api_documentation/markdown_example" 9 | end 10 | 11 | def parameters 12 | super.map do |parameter| 13 | parameter.merge({ 14 | :required => parameter[:required] ? 'true' : 'false', 15 | }) 16 | end 17 | end 18 | 19 | def extension 20 | EXTENSION 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/rspec_api_documentation/writers/combined_json_writer.rb: -------------------------------------------------------------------------------- 1 | require 'rspec_api_documentation/writers/json_writer' 2 | 3 | module RspecApiDocumentation 4 | module Writers 5 | class CombinedJsonWriter < Writer 6 | def write 7 | File.open(configuration.docs_dir.join("combined.json"), "w+") do |f| 8 | examples = [] 9 | index.examples.each do |rspec_example| 10 | examples << Formatter.to_json(JsonExample.new(rspec_example, configuration)) 11 | end 12 | 13 | f.write "[" 14 | f.write examples.join(",") 15 | f.write "]" 16 | end 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /features/support/capybara.rb: -------------------------------------------------------------------------------- 1 | require 'rack' 2 | require 'capybara/cucumber' 3 | 4 | # Wire up Capybara to test again static files served by Rack 5 | # Courtesy of http://opensoul.org/blog/archives/2010/05/11/capybaras-eating-cucumbers/ 6 | 7 | root_dir = File.join(File.dirname(__FILE__), '..', '..', 'tmp', 'aruba', 'doc', 'api') 8 | 9 | Capybara.app = Rack::Builder.new do 10 | map "/" do 11 | # use Rack::CommonLogger, $stderr 12 | use Rack::Static, :urls => ["/"], :root => root_dir 13 | use Rack::Lint 14 | run lambda {|env| [404, {}, '']} 15 | end 16 | end.to_app 17 | 18 | Capybara.default_selector = :css 19 | Capybara.default_driver = :rack_test 20 | -------------------------------------------------------------------------------- /example/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | # 7 | default: &default 8 | adapter: sqlite3 9 | pool: 5 10 | timeout: 5000 11 | 12 | development: 13 | <<: *default 14 | database: db/development.sqlite3 15 | 16 | # Warning: The database defined as "test" will be erased and 17 | # re-generated from your development database when you run "rake". 18 | # Do not set this db to the same as development or production. 19 | test: 20 | <<: *default 21 | database: db/test.sqlite3 22 | 23 | production: 24 | <<: *default 25 | database: db/production.sqlite3 26 | -------------------------------------------------------------------------------- /lib/rspec_api_documentation/writers/writer.rb: -------------------------------------------------------------------------------- 1 | module RspecApiDocumentation 2 | module Writers 3 | class Writer 4 | attr_accessor :index, :configuration 5 | 6 | def initialize(index, configuration) 7 | self.index = index 8 | self.configuration = configuration 9 | end 10 | 11 | def self.write(index, configuration) 12 | writer = new(index, configuration) 13 | writer.write 14 | end 15 | 16 | def self.clear_docs(docs_dir) 17 | if File.exists?(docs_dir) 18 | FileUtils.rm_rf(docs_dir, :secure => true) 19 | end 20 | FileUtils.mkdir_p(docs_dir) 21 | end 22 | end 23 | end 24 | end 25 | 26 | -------------------------------------------------------------------------------- /spec/headers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | class FakeHeaderable 4 | include RspecApiDocumentation::Headers 5 | 6 | def public_env_to_headers(env) 7 | env_to_headers(env) 8 | end 9 | end 10 | 11 | describe RspecApiDocumentation::Headers do 12 | let(:example) { FakeHeaderable.new } 13 | 14 | describe '#env_to_headers' do 15 | subject { example.public_env_to_headers(env) } 16 | 17 | context 'When the env contains "CONTENT_TYPE"' do 18 | let(:env) { { "CONTENT_TYPE" => 'multipart/form-data' } } 19 | 20 | it 'converts the header to "Content-Type"' do 21 | expect(subject['Content-Type']).to eq 'multipart/form-data' 22 | end 23 | end 24 | end 25 | 26 | end 27 | -------------------------------------------------------------------------------- /spec/support/stub_app.rb: -------------------------------------------------------------------------------- 1 | class StubApp < Sinatra::Base 2 | get "/" do 3 | content_type :json 4 | 5 | { :hello => "world" }.to_json 6 | end 7 | 8 | post "/greet" do 9 | content_type :json 10 | 11 | request.body.rewind 12 | begin 13 | data = JSON.parse request.body.read 14 | rescue JSON::ParserError 15 | request.body.rewind 16 | data = request.body.read 17 | end 18 | { :hello => data["target"] }.to_json 19 | end 20 | 21 | get "/xml" do 22 | content_type :xml 23 | 24 | "World" 25 | end 26 | 27 | get '/binary' do 28 | content_type 'application/octet-stream' 29 | "\x01\x02\x03".force_encoding(Encoding::ASCII_8BIT) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /example/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /example/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more, please read the Rails Internationalization guide 20 | # available at http://guides.rubyonrails.org/i18n.html. 21 | 22 | en: 23 | hello: "Hello world" 24 | -------------------------------------------------------------------------------- /example/app/controllers/orders_controller.rb: -------------------------------------------------------------------------------- 1 | class OrdersController < ApplicationController 2 | def index 3 | render :json => Order.all 4 | end 5 | 6 | def show 7 | render :json => Order.find(params[:id]) 8 | end 9 | 10 | def create 11 | order = Order.create(order_params) 12 | render :json => order, :status => 201, :location => order_url(order) 13 | end 14 | 15 | def update 16 | order = Order.find(params[:id]) 17 | order.update(order_params) 18 | render :nothing => true, :status => 204 19 | end 20 | 21 | def destroy 22 | Order.find(params[:id]).destroy 23 | head 204 24 | end 25 | 26 | private 27 | 28 | def order_params 29 | params.require(:order).permit(:name, :paid, :email) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/rspec_api_documentation/views/html_example.rb: -------------------------------------------------------------------------------- 1 | module RspecApiDocumentation 2 | module Views 3 | class HtmlExample < MarkupExample 4 | EXTENSION = 'html' 5 | 6 | def initialize(example, configuration) 7 | super 8 | self.template_name = "rspec_api_documentation/html_example" 9 | end 10 | 11 | def extension 12 | EXTENSION 13 | end 14 | 15 | def styles 16 | app_styles_url = RspecApiDocumentation.configuration.html_embedded_css_file 17 | gem_styles_url = File.join(File.dirname(__FILE__), "..", "assets", "stylesheets","rspec_api_documentation", "styles.css") 18 | return File.read(app_styles_url) rescue File.read(gem_styles_url) 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /example/app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. 9 | // 10 | // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require jquery 14 | //= require jquery_ujs 15 | //= require turbolinks 16 | //= require_tree . 17 | -------------------------------------------------------------------------------- /example/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any styles 10 | * defined in the other CSS/SCSS files in this directory. It is generally better to create a new 11 | * file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /lib/rspec_api_documentation/views/html_index.rb: -------------------------------------------------------------------------------- 1 | module RspecApiDocumentation 2 | module Views 3 | class HtmlIndex < MarkupIndex 4 | def initialize(index, configuration) 5 | super 6 | self.template_name = "rspec_api_documentation/html_index" 7 | end 8 | 9 | def styles 10 | app_styles_url = RspecApiDocumentation.configuration.html_embedded_css_file 11 | gem_styles_url = File.join(File.dirname(__FILE__), "..", "assets", "stylesheets","rspec_api_documentation", "styles.css") 12 | return File.read(app_styles_url) rescue File.read(gem_styles_url) 13 | end 14 | 15 | def examples 16 | @index.examples.map { |example| HtmlExample.new(example, @configuration) } 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/rspec_api_documentation/writers/api_blueprint_writer.rb: -------------------------------------------------------------------------------- 1 | module RspecApiDocumentation 2 | module Writers 3 | class ApiBlueprintWriter < GeneralMarkupWriter 4 | EXTENSION = 'apib' 5 | 6 | def markup_index_class 7 | RspecApiDocumentation::Views::ApiBlueprintIndex 8 | end 9 | 10 | def markup_example_class 11 | RspecApiDocumentation::Views::ApiBlueprintExample 12 | end 13 | 14 | def extension 15 | EXTENSION 16 | end 17 | 18 | private 19 | 20 | # API Blueprint is a spec, not navigable like HTML, therefore we generate 21 | # only one file with all resources. 22 | def render_options 23 | super.merge({ 24 | examples: false 25 | }) 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/rspec_api_documentation/writers/index_helper.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/enumerable" 2 | 3 | module RspecApiDocumentation 4 | module Writers 5 | module IndexHelper 6 | def sections(examples, configuration) 7 | resources = examples.group_by(&:resource_name).inject([]) do |arr, (resource_name, examples)| 8 | ordered_examples = configuration.keep_source_order ? examples : examples.sort_by(&:description) 9 | arr.push(:resource_name => resource_name, :examples => ordered_examples, resource_explanation: examples.first.resource_explanation) 10 | end 11 | configuration.keep_source_order ? resources : resources.sort_by { |resource| resource[:resource_name] } 12 | end 13 | module_function :sections 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/rspec_api_documentation/dsl/callback.rb: -------------------------------------------------------------------------------- 1 | module RspecApiDocumentation::DSL 2 | # DSL Methods for testing server callbacks 3 | module Callback 4 | extend ActiveSupport::Concern 5 | 6 | delegate :request_method, :request_headers, :request_body, :to => :destination 7 | 8 | module ClassMethods 9 | def trigger_callback(&block) 10 | define_method(:do_callback) do 11 | require 'rack' 12 | stub_request(:any, callback_url).to_rack(destination) 13 | instance_eval &block 14 | end 15 | end 16 | end 17 | 18 | def destination 19 | @destination ||= RspecApiDocumentation::TestServer.new(RSpec.current_example) 20 | end 21 | 22 | def callback_url 23 | raise "You must define callback_url" 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /templates/rspec_api_documentation/html_index.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ api_name }} 5 | 6 | 9 | 10 | 11 |
12 |

{{ api_name }}

13 | {{{ api_explanation }}} 14 | {{# sections }} 15 |
16 |

{{ resource_name }}

17 | {{# resource_explanation }} 18 | 19 |

{{{ resource_explanation }}}

20 | {{/ resource_explanation }} 21 | 22 | 29 |
30 | {{/ sections }} 31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /lib/rspec_api_documentation/dsl/endpoint/params.rb: -------------------------------------------------------------------------------- 1 | require 'rspec_api_documentation/dsl/endpoint/set_param' 2 | 3 | module RspecApiDocumentation 4 | module DSL 5 | module Endpoint 6 | class Params 7 | attr_reader :example_group, :example 8 | 9 | def initialize(example_group, example, extra_params) 10 | @example_group = example_group 11 | @example = example 12 | @extra_params = extra_params 13 | end 14 | 15 | def call 16 | parameters = example.metadata.fetch(:parameters, {}).inject({}) do |hash, param| 17 | SetParam.new(self, hash, param).call 18 | end 19 | parameters.deep_merge!(extra_params) 20 | parameters 21 | end 22 | 23 | private 24 | 25 | attr_reader :extra_params 26 | 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/rspec_api_documentation/headers.rb: -------------------------------------------------------------------------------- 1 | module RspecApiDocumentation 2 | module Headers 3 | private 4 | 5 | def env_to_headers(env) 6 | headers = {} 7 | env.each do |key, value| 8 | # HTTP_ACCEPT_CHARSET => Accept-Charset 9 | if key =~ /^(HTTP_|CONTENT_TYPE)/ 10 | header = key.gsub(/^HTTP_/, '').split('_').map{|s| s.titleize}.join("-") 11 | headers[header] = value 12 | end 13 | end 14 | headers 15 | end 16 | 17 | def headers_to_env(headers) 18 | headers.inject({}) do |hsh, (k, v)| 19 | new_key = k.upcase.gsub("-", "_") 20 | new_key = "HTTP_#{new_key}" unless new_key == "CONTENT_TYPE" 21 | hsh[new_key] = v 22 | hsh 23 | end 24 | end 25 | 26 | def format_headers(headers) 27 | headers.map do |key, value| 28 | "#{key}: #{value}" 29 | end.join("\n") 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/rspec_api_documentation_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe RspecApiDocumentation do 4 | describe "#configuration" do 5 | it "should be a configuration" do 6 | expect(RspecApiDocumentation.configuration).to be_a(RspecApiDocumentation::Configuration) 7 | end 8 | 9 | it "returns the same configuration every time" do 10 | expect(RspecApiDocumentation.configuration).to equal(RspecApiDocumentation.configuration) 11 | end 12 | end 13 | 14 | describe "#configure" do 15 | let(:configuration) { double(:confiugration) } 16 | 17 | before do 18 | allow(RspecApiDocumentation).to receive(:configuration).and_return(configuration) 19 | end 20 | 21 | it "should yield the configuration to the block" do 22 | allow(configuration).to receive(:foo) 23 | RspecApiDocumentation.configure do |config| 24 | config.foo 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/rspec_api_documentation/test_server.rb: -------------------------------------------------------------------------------- 1 | module RspecApiDocumentation 2 | class TestServer < Struct.new(:example) 3 | include Headers 4 | 5 | delegate :metadata, :to => :example 6 | 7 | attr_reader :request_method, :request_headers, :request_body 8 | 9 | def call(env) 10 | input = env["rack.input"] 11 | input.rewind 12 | 13 | @request_method = env["REQUEST_METHOD"] 14 | @request_headers = env_to_headers(env) 15 | @request_body = input.read 16 | 17 | request_metadata = {} 18 | 19 | request_metadata[:request_method] = @request_method 20 | request_metadata[:request_path] = env["PATH_INFO"] 21 | request_metadata[:request_body] = @request_body 22 | request_metadata[:request_headers] = @request_headers 23 | 24 | metadata[:requests] ||= [] 25 | metadata[:requests] << request_metadata 26 | 27 | return [200, {}, [""]] 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/writers/json_writer_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe RspecApiDocumentation::Writers::JsonWriter do 4 | let(:index) { RspecApiDocumentation::Index.new } 5 | let(:configuration) { RspecApiDocumentation::Configuration.new } 6 | 7 | describe ".write" do 8 | let(:writer) { double(:writer) } 9 | 10 | it "should build a new writer and write the docs" do 11 | allow(described_class).to receive(:new).with(index, configuration).and_return(writer) 12 | expect(writer).to receive(:write) 13 | described_class.write(index, configuration) 14 | end 15 | end 16 | 17 | describe "#write" do 18 | let(:writer) { described_class.new(index, configuration) } 19 | 20 | before do 21 | FileUtils.mkdir_p(configuration.docs_dir) 22 | end 23 | 24 | it "should write the index" do 25 | writer.write 26 | index_file = File.join(configuration.docs_dir, "index.json") 27 | expect(File.exists?(index_file)).to be_truthy 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/rspec_api_documentation/api_documentation.rb: -------------------------------------------------------------------------------- 1 | module RspecApiDocumentation 2 | class ApiDocumentation 3 | attr_reader :configuration, :index 4 | 5 | delegate :docs_dir, :format, :to => :configuration 6 | 7 | def initialize(configuration) 8 | @configuration = configuration 9 | @index = Index.new 10 | end 11 | 12 | def clear_docs 13 | writers.each do |writer| 14 | writer.clear_docs(docs_dir) 15 | end 16 | end 17 | 18 | def document_example(rspec_example) 19 | example = Example.new(rspec_example, configuration) 20 | if example.should_document? 21 | index.examples << example 22 | end 23 | end 24 | 25 | def write 26 | writers.each do |writer| 27 | writer.write(index, configuration) 28 | end 29 | end 30 | 31 | def writers 32 | [*configuration.format].map do |format| 33 | RspecApiDocumentation::Writers.const_get("#{format}_writer".classify) 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /example/config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | secret_key_base: 34a93733588de8e7c65d87d011e970a0f31299a948e9400b9258f24e607600c7211b2610c64b751dcf618dff4d486cb6899f315024b4d71567a985784ed2c883 15 | 16 | test: 17 | secret_key_base: 3de2ec6787a1547f7d0bdc0e7497c8f602be7e5edbc65d64673f39e845a9615422f29c52271e01277114e324a9869e166fd390e40d77dc4897d9da8f98dee27c 18 | 19 | # Do not keep production secrets in the repository, 20 | # instead read values from the environment. 21 | production: 22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 23 | -------------------------------------------------------------------------------- /spec/writers/json_example_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | require 'spec_helper' 3 | 4 | describe RspecApiDocumentation::Writers::JsonExample do 5 | let(:configuration) { RspecApiDocumentation::Configuration.new } 6 | 7 | describe "#dirname" do 8 | it "strips out leading slashes" do 9 | example = double(resource_name: "/test_string") 10 | 11 | json_example = 12 | RspecApiDocumentation::Writers::JsonExample.new(example, configuration) 13 | 14 | expect(json_example.dirname).to eq "test_string" 15 | end 16 | 17 | it "does not strip out non-leading slashes" do 18 | example = double(resource_name: "test_string/test") 19 | 20 | json_example = 21 | RspecApiDocumentation::Writers::JsonExample.new(example, configuration) 22 | 23 | expect(json_example.dirname).to eq "test_string/test" 24 | end 25 | end 26 | 27 | describe '#filename' do 28 | specify 'Hello!/ 世界' do |example| 29 | expect(described_class.new(example, configuration).filename).to eq("hello!_世界.json") 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /example/db/schema.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # This file is auto-generated from the current state of the database. Instead 3 | # of editing this file, please use the migrations feature of Active Record to 4 | # incrementally modify your database, and then regenerate this schema definition. 5 | # 6 | # Note that this schema.rb definition is the authoritative source for your 7 | # database schema. If you need to create the application database on another 8 | # system, you should be using db:schema:load, not running all the migrations 9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 10 | # you'll amass, the slower it'll run and the greater likelihood for issues). 11 | # 12 | # It's strongly recommended that you check this file into your version control system. 13 | 14 | ActiveRecord::Schema.define(version: 20140616151047) do 15 | 16 | create_table "orders", force: true do |t| 17 | t.string "name" 18 | t.boolean "paid" 19 | t.string "email" 20 | t.datetime "created_at" 21 | t.datetime "updated_at" 22 | end 23 | 24 | end 25 | -------------------------------------------------------------------------------- /features/folder_structure.feature: -------------------------------------------------------------------------------- 1 | Feature: Folder Structure 2 | Background: 3 | Given a file named "app.rb" with: 4 | """ 5 | class App 6 | def self.call(env) 7 | [200, {}, ["hello"]] 8 | end 9 | end 10 | """ 11 | And a file named "app_spec.rb" with: 12 | """ 13 | require "rspec_api_documentation" 14 | require "rspec_api_documentation/dsl" 15 | 16 | RspecApiDocumentation.configure do |config| 17 | config.app = App 18 | end 19 | 20 | resource "Namespace::Greetings" do 21 | get "/greetings" do 22 | example_request "Greeting your favorite gem" do 23 | expect(status).to eq(200) 24 | end 25 | end 26 | end 27 | """ 28 | When I run `rspec app_spec.rb --require ./app.rb --format RspecApiDocumentation::ApiFormatter` 29 | 30 | Scenario: Folder structure does not contain :: 31 | When I open the index 32 | And I navigate to "Greeting your favorite gem" 33 | 34 | Then I should see the route is "GET /greetings" 35 | -------------------------------------------------------------------------------- /spec/writers/json_iodocs_writer_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe RspecApiDocumentation::Writers::JsonIodocsWriter do 4 | let(:index) { RspecApiDocumentation::Index.new } 5 | let(:configuration) { RspecApiDocumentation::Configuration.new } 6 | 7 | describe ".write" do 8 | let(:writer) { double(:writer) } 9 | 10 | it "should build a new writer and write the docs" do 11 | allow(described_class).to receive(:new).with(index, configuration).and_return(writer) 12 | expect(writer).to receive(:write) 13 | described_class.write(index, configuration) 14 | end 15 | end 16 | 17 | describe "#write" do 18 | let(:writer) { described_class.new(index, configuration) } 19 | 20 | before do 21 | allow(configuration.api_name).to receive(:parameterize).and_return("Name") 22 | FileUtils.mkdir_p(configuration.docs_dir) 23 | end 24 | 25 | it "should write the index" do 26 | writer.write 27 | index_file = File.join(configuration.docs_dir, "apiconfig.json") 28 | expect(File.exists?(index_file)).to be_truthy 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (C) 2016 Zipmark, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /features/redefining_client.feature: -------------------------------------------------------------------------------- 1 | Feature: Redefining the client method 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 | And a file named "app_spec.rb" with: 12 | """ 13 | require "rspec_api_documentation" 14 | require "rspec_api_documentation/dsl" 15 | 16 | RspecApiDocumentation.configure do |config| 17 | config.app = App 18 | config.client_method = :different_client 19 | end 20 | 21 | resource "Example Request" do 22 | let(:client) { double(:client) } 23 | 24 | get "/" do 25 | example_request "Trying out get" do 26 | expect(status).to eq(200) 27 | end 28 | end 29 | end 30 | """ 31 | When I run `rspec app_spec.rb --require ./app.rb --format RspecApiDocumentation::ApiFormatter` 32 | 33 | Scenario: Output should have the correct error line 34 | Then the output should contain "1 example, 0 failures" 35 | And the exit status should be 0 36 | -------------------------------------------------------------------------------- /features/patch.feature: -------------------------------------------------------------------------------- 1 | Feature: Example Request with PATCH 2 | Background: 3 | Given a file named "app.rb" with: 4 | """ 5 | class App 6 | def self.call(env) 7 | if env["REQUEST_METHOD"] == "PATCH" 8 | [200, {}, ["Hello, world"]] 9 | else 10 | [404, {}, ["ERROR"]] 11 | end 12 | end 13 | end 14 | """ 15 | And a file named "app_spec.rb" with: 16 | """ 17 | require "rspec_api_documentation" 18 | require "rspec_api_documentation/dsl" 19 | 20 | RspecApiDocumentation.configure do |config| 21 | config.app = App 22 | end 23 | 24 | resource "Example Request" do 25 | patch "/" do 26 | example_request "Trying out patch" do 27 | expect(status).to eq(200) 28 | end 29 | end 30 | end 31 | """ 32 | When I run `rspec app_spec.rb --require ./app.rb --format RspecApiDocumentation::ApiFormatter` 33 | 34 | Scenario: Output should have the correct error line 35 | Then the output should contain "1 example, 0 failures" 36 | And the exit status should be 0 37 | -------------------------------------------------------------------------------- /features/headers.feature: -------------------------------------------------------------------------------- 1 | Feature: Specifying request headers 2 | 3 | Background: 4 | Given a file named "app.rb" with: 5 | """ 6 | class App 7 | def self.call(env) 8 | if env["HTTP_ACCEPT"] == "foo" 9 | [200, {}, ["foo"]] 10 | else 11 | [406, {}, ["unknown content type"]] 12 | end 13 | end 14 | end 15 | """ 16 | And a file named "app_spec.rb" with: 17 | """ 18 | require "rspec_api_documentation" 19 | require "rspec_api_documentation/dsl" 20 | 21 | RspecApiDocumentation.configure do |config| 22 | config.app = App 23 | end 24 | 25 | resource "FooBars" do 26 | get "/foobar" do 27 | header "Accept", "foo" 28 | 29 | example "Getting Foo" do 30 | do_request 31 | expect(response_body).to eq("foo") 32 | end 33 | end 34 | end 35 | """ 36 | When I run `rspec app_spec.rb --require ./app.rb --format RspecApiDocumentation::ApiFormatter` 37 | 38 | Scenario: Sending headers along with the request 39 | Then the output should not contain "unknown content type" 40 | -------------------------------------------------------------------------------- /lib/rspec_api_documentation/api_formatter.rb: -------------------------------------------------------------------------------- 1 | require 'rspec/core/formatters/base_text_formatter' 2 | 3 | module RspecApiDocumentation 4 | class ApiFormatter < RSpec::Core::Formatters::BaseTextFormatter 5 | RSpec::Core::Formatters.register self, :example_passed, :example_failed, :stop 6 | 7 | def initialize(output) 8 | super 9 | 10 | output.puts "Generating API Docs" 11 | end 12 | 13 | def start(notification) 14 | super 15 | 16 | RspecApiDocumentation.documentations.each(&:clear_docs) 17 | end 18 | 19 | def example_group_started(notification) 20 | super 21 | 22 | output.puts " #{@example_group.description}" 23 | end 24 | 25 | def example_passed(example_notification) 26 | output.puts " * #{example_notification.example.description}" 27 | 28 | RspecApiDocumentation.documentations.each do |documentation| 29 | documentation.document_example(example_notification.example) 30 | end 31 | end 32 | 33 | def example_failed(example_notification) 34 | output.puts " ! #{example_notification.example.description} (FAILED)" 35 | end 36 | 37 | def stop(notification) 38 | RspecApiDocumentation.documentations.each(&:write) 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/writers/html_writer_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | require 'spec_helper' 3 | 4 | describe RspecApiDocumentation::Writers::HtmlWriter do 5 | let(:index) { RspecApiDocumentation::Index.new } 6 | let(:configuration) { RspecApiDocumentation::Configuration.new } 7 | 8 | describe ".write" do 9 | let(:writer) { double(:writer) } 10 | 11 | it "should build a new writer and write the docs" do 12 | allow(described_class).to receive(:new).with(index, configuration).and_return(writer) 13 | expect(writer).to receive(:write) 14 | described_class.write(index, configuration) 15 | end 16 | end 17 | 18 | describe "#write" do 19 | let(:writer) { described_class.new(index, configuration) } 20 | 21 | it "should write the index" do 22 | FakeFS do 23 | template_dir = File.join(configuration.template_path, "rspec_api_documentation") 24 | FileUtils.mkdir_p(template_dir) 25 | File.open(File.join(template_dir, "html_index.mustache"), "w+") { |f| f << "{{ mustache }}" } 26 | FileUtils.mkdir_p(configuration.docs_dir) 27 | 28 | writer.write 29 | index_file = File.join(configuration.docs_dir, "index.html") 30 | expect(File.exists?(index_file)).to be_truthy 31 | end 32 | end 33 | end 34 | end 35 | 36 | -------------------------------------------------------------------------------- /spec/writers/textile_writer_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | require 'spec_helper' 3 | 4 | describe RspecApiDocumentation::Writers::TextileWriter do 5 | let(:index) { RspecApiDocumentation::Index.new } 6 | let(:configuration) { RspecApiDocumentation::Configuration.new } 7 | 8 | describe ".write" do 9 | let(:writer) { double(:writer) } 10 | 11 | it "should build a new writer and write the docs" do 12 | allow(described_class).to receive(:new).with(index, configuration).and_return(writer) 13 | expect(writer).to receive(:write) 14 | described_class.write(index, configuration) 15 | end 16 | end 17 | 18 | describe "#write" do 19 | let(:writer) { described_class.new(index, configuration) } 20 | 21 | it "should write the index" do 22 | FakeFS do 23 | template_dir = File.join(configuration.template_path, "rspec_api_documentation") 24 | FileUtils.mkdir_p(template_dir) 25 | File.open(File.join(template_dir, "textile_index.mustache"), "w+") { |f| f << "{{ mustache }}" } 26 | FileUtils.mkdir_p(configuration.docs_dir) 27 | 28 | writer.write 29 | index_file = File.join(configuration.docs_dir, "index.textile") 30 | expect(File.exists?(index_file)).to be_truthy 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/writers/markdown_writer_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | require 'spec_helper' 3 | 4 | describe RspecApiDocumentation::Writers::MarkdownWriter do 5 | let(:index) { RspecApiDocumentation::Index.new } 6 | let(:configuration) { RspecApiDocumentation::Configuration.new } 7 | 8 | describe ".write" do 9 | let(:writer) { double(:writer) } 10 | 11 | it "should build a new writer and write the docs" do 12 | allow(described_class).to receive(:new).with(index, configuration).and_return(writer) 13 | expect(writer).to receive(:write) 14 | described_class.write(index, configuration) 15 | end 16 | end 17 | 18 | describe "#write" do 19 | let(:writer) { described_class.new(index, configuration) } 20 | 21 | it "should write the index" do 22 | FakeFS do 23 | template_dir = File.join(configuration.template_path, "rspec_api_documentation") 24 | FileUtils.mkdir_p(template_dir) 25 | File.open(File.join(template_dir, "markdown_index.mustache"), "w+") { |f| f << "{{ mustache }}" } 26 | FileUtils.mkdir_p(configuration.docs_dir) 27 | 28 | writer.write 29 | index_file = File.join(configuration.docs_dir, "index.markdown") 30 | expect(File.exists?(index_file)).to be_truthy 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /example/config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | # Pick the frameworks you want: 4 | require "active_model/railtie" 5 | require "active_record/railtie" 6 | require "action_controller/railtie" 7 | require "action_mailer/railtie" 8 | require "action_view/railtie" 9 | require "sprockets/railtie" 10 | # require "rails/test_unit/railtie" 11 | 12 | # Require the gems listed in Gemfile, including any gems 13 | # you've limited to :test, :development, or :production. 14 | Bundler.require(*Rails.groups) 15 | 16 | module Example 17 | class Application < Rails::Application 18 | # Settings in config/environments/* take precedence over those specified here. 19 | # Application configuration should go into files in config/initializers 20 | # -- all .rb files in that directory are automatically loaded. 21 | 22 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 23 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 24 | # config.time_zone = 'Central Time (US & Canada)' 25 | 26 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 27 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 28 | # config.i18n.default_locale = :de 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/rspec_api_documentation/writers/general_markup_writer.rb: -------------------------------------------------------------------------------- 1 | module RspecApiDocumentation 2 | module Writers 3 | # Base class for writers that write HTML 4 | class GeneralMarkupWriter < Writer 5 | INDEX_FILE_NAME = 'index' 6 | 7 | # Write out the generated documentation 8 | def write 9 | if render_options.fetch(:index, true) 10 | File.open(configuration.docs_dir.join(index_file_name + '.' + extension), "w+") do |f| 11 | f.write markup_index_class.new(index, configuration).render 12 | end 13 | end 14 | 15 | if render_options.fetch(:examples, true) 16 | index.examples.each do |example| 17 | markup_example = markup_example_class.new(example, configuration) 18 | FileUtils.mkdir_p(configuration.docs_dir.join(markup_example.dirname)) 19 | 20 | File.open(configuration.docs_dir.join(markup_example.dirname, markup_example.filename), "w+") do |f| 21 | f.write markup_example.render 22 | end 23 | end 24 | end 25 | end 26 | 27 | def index_file_name 28 | INDEX_FILE_NAME 29 | end 30 | 31 | def extension 32 | raise 'Parent class. This method should not be called.' 33 | end 34 | 35 | private 36 | 37 | def render_options 38 | { 39 | index: true, 40 | examples: true 41 | } 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /features/readme.md: -------------------------------------------------------------------------------- 1 | [![Travis status](https://secure.travis-ci.org/zipmark/rspec_api_documentation.png)](https://secure.travis-ci.org/zipmark/rspec_api_documentation) 2 | [![Gemnasium status](https://gemnasium.com/zipmark/rspec_api_documentation.png)](https://gemnasium.com/zipmark/rspec_api_documentation) 3 | 4 | http://github.com/zipmark/rspec_api_documentation 5 | 6 | # RSpec API Doc Generator 7 | 8 | Generate pretty API docs for your Rails APIs. 9 | 10 | ## Installation 11 | 12 | Add rspec_api_documentation to your Gemfile 13 | 14 | gem 'rspec_api_documentation' 15 | 16 | Bundle it! 17 | 18 | $> bundle install 19 | 20 | Require it in your API tests 21 | 22 | require "rspec_api_documentation" 23 | require "rspec_api_documentation/dsl" 24 | 25 | See the wiki for additional setup. [Setting up RSpec API Documentation](https://github.com/zipmark/rspec_api_documentation/wiki/Setting-up-RspecApiDocumentation) 26 | 27 | ## Usage 28 | 29 | resource "Account" do 30 | get "/accounts" do 31 | example "Get a list of all accounts" do 32 | do_request 33 | expect(last_response.status).to be_ok 34 | end 35 | end 36 | 37 | get "/accounts/:id" do 38 | parameter :id, "Account ID" 39 | 40 | let(:account) { Factory(:account) } 41 | let(:id) { account.id } 42 | 43 | example "Get an account", :document => :public do 44 | do_request 45 | expect(last_response.status).to be_ok 46 | end 47 | end 48 | end 49 | 50 | -------------------------------------------------------------------------------- /example/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports and disable caching. 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send. 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger. 20 | config.active_support.deprecation = :log 21 | 22 | # Raise an error on page load if there are pending migrations. 23 | config.active_record.migration_error = :page_load 24 | 25 | # Debug mode disables concatenation and preprocessing of assets. 26 | # This option may cause significant delays in view rendering with a large 27 | # number of complex assets. 28 | config.assets.debug = true 29 | 30 | # Adds additional error checking when serving assets at runtime. 31 | # Checks for improperly declared sprockets dependencies. 32 | # Raises helpful error messages. 33 | config.assets.raise_runtime_errors = true 34 | 35 | # Raises error for missing translations 36 | # config.action_view.raise_on_missing_translations = true 37 | end 38 | -------------------------------------------------------------------------------- /spec/writers/slate_writer_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | require 'spec_helper' 3 | 4 | describe RspecApiDocumentation::Writers::SlateWriter do 5 | let(:index) { RspecApiDocumentation::Index.new } 6 | let(:configuration) { RspecApiDocumentation::Configuration.new } 7 | 8 | describe ".write" do 9 | let(:writer) { double(:writer) } 10 | 11 | it "should build a new writer and write the docs" do 12 | allow(described_class).to receive(:new).with(index, configuration).and_return(writer) 13 | expect(writer).to receive(:write) 14 | described_class.write(index, configuration) 15 | end 16 | end 17 | 18 | describe "#write" do 19 | let(:writer) { described_class.new(index, configuration) } 20 | 21 | it "should write the index" do 22 | FakeFS do 23 | template_dir = File.join(configuration.template_path, "rspec_api_documentation") 24 | FileUtils.mkdir_p(template_dir) 25 | File.open(File.join(template_dir, "markdown_index.mustache"), "w+") { |f| f << "{{ mustache }}" } 26 | FileUtils.mkdir_p(configuration.docs_dir) 27 | 28 | writer.write 29 | index_file = File.join(configuration.docs_dir, "index.html.md") 30 | expect(File.exists?(index_file)).to be_truthy 31 | end 32 | end 33 | end 34 | 35 | context 'instance methods' do 36 | let(:writer) { described_class.new(index, configuration) } 37 | 38 | describe '#markup_example_class' do 39 | subject { writer.markup_example_class } 40 | it { is_expected.to be == RspecApiDocumentation::Views::SlateExample } 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/rspec_api_documentation/writers/slate_writer.rb: -------------------------------------------------------------------------------- 1 | module RspecApiDocumentation 2 | module Writers 3 | 4 | class SlateWriter < MarkdownWriter 5 | EXTENSION = 'html.md' 6 | FILENAME = 'index' 7 | 8 | def self.clear_docs(docs_dir) 9 | FileUtils.mkdir_p(docs_dir) 10 | FileUtils.rm Dir[File.join docs_dir, "#{FILENAME}.*"] 11 | end 12 | 13 | def markup_index_class 14 | RspecApiDocumentation::Views::SlateIndex 15 | end 16 | 17 | def markup_example_class 18 | RspecApiDocumentation::Views::SlateExample 19 | end 20 | 21 | def write 22 | File.open(configuration.docs_dir.join("#{FILENAME}.#{extension}"), 'w+') do |file| 23 | 24 | file.write %Q{---\n} 25 | file.write %Q{title: "#{configuration.api_name}"\n} 26 | file.write %Q{language_tabs:\n} 27 | file.write %Q{ - json: JSON\n} 28 | file.write %Q{ - shell: cURL\n} 29 | file.write %Q{---\n\n} 30 | 31 | IndexHelper.sections(index.examples, @configuration).each do |section| 32 | 33 | file.write "# #{section[:resource_name]}\n\n" 34 | section[:examples].sort_by!(&:description) unless configuration.keep_source_order 35 | 36 | section[:examples].each do |example| 37 | markup_example = markup_example_class.new(example, configuration) 38 | file.write markup_example.render 39 | end 40 | 41 | end 42 | 43 | end 44 | end 45 | 46 | def extension 47 | EXTENSION 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/views/html_example_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | require 'spec_helper' 3 | 4 | describe RspecApiDocumentation::Views::HtmlExample do 5 | let(:metadata) { { :resource_name => "Orders" } } 6 | let(:group) { RSpec::Core::ExampleGroup.describe("Orders", metadata) } 7 | let(:description) { "Ordering a cup of coffee" } 8 | let(:rspec_example) { group.example(description) {} } 9 | let(:rad_example) do 10 | RspecApiDocumentation::Example.new(rspec_example, configuration) 11 | end 12 | let(:configuration) { RspecApiDocumentation::Configuration.new } 13 | let(:html_example) { described_class.new(rad_example, configuration) } 14 | 15 | specify "the directory is 'orders'" do 16 | expect(html_example.dirname).to eq("orders") 17 | end 18 | 19 | it "should have downcased filename" do 20 | expect(html_example.filename).to eq("ordering_a_cup_of_coffee.html") 21 | end 22 | 23 | context "when description contains special characters for Windows OS" do 24 | let(:description) { 'foo<>:"/\|?*bar' } 25 | 26 | it "removes them" do 27 | expect(html_example.filename).to eq("foobar.html") 28 | end 29 | end 30 | 31 | describe "multi-character example name" do 32 | let(:metadata) { { :resource_name => "オーダ" } } 33 | let(:label) { "Coffee / Teaが順番で並んでいること" } 34 | let(:rspec_example) { group.example(label) {} } 35 | 36 | specify "the directory is 'オーダ'" do 37 | expect(html_example.dirname).to eq("オーダ") 38 | end 39 | 40 | it "should have downcased filename" do 41 | expect(html_example.filename).to eq("coffee__teaが順番で並んでいること.html") 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/rspec_api_documentation/dsl.rb: -------------------------------------------------------------------------------- 1 | require "rspec_api_documentation" 2 | require "rspec_api_documentation/dsl/resource" 3 | require "rspec_api_documentation/dsl/endpoint" 4 | require "rspec_api_documentation/dsl/callback" 5 | 6 | 7 | module RspecApiDocumentation 8 | module DSL 9 | 10 | # Custom describe block that sets metadata to enable the rest of RAD 11 | # 12 | # resource "Orders", :meta => :data do 13 | # # ... 14 | # end 15 | # 16 | # Params: 17 | # +args+:: Glob of RSpec's `describe` arguments 18 | # +block+:: Block to pass into describe 19 | # 20 | def resource(*args, &block) 21 | options = args.last.is_a?(Hash) ? args.pop : {} 22 | options[:api_doc_dsl] = :resource 23 | options[:resource_name] = args.first.to_s 24 | options[:document] = :all unless options.key?(:document) 25 | args.push(options) 26 | describe(*args, &block) 27 | end 28 | end 29 | end 30 | 31 | RSpec::Core::ExampleGroup.extend(RspecApiDocumentation::DSL) 32 | RSpec::Core::DSL.expose_example_group_alias(:resource) 33 | 34 | RSpec.configuration.include RspecApiDocumentation::DSL::Resource, :api_doc_dsl => :resource 35 | RSpec.configuration.include RspecApiDocumentation::DSL::Endpoint, :api_doc_dsl => :endpoint 36 | RSpec.configuration.include RspecApiDocumentation::DSL::Callback, :api_doc_dsl => :callback 37 | RSpec.configuration.backtrace_exclusion_patterns << %r{lib/rspec_api_documentation/dsl/} 38 | 39 | if defined? RSpec::Rails::DIRECTORY_MAPPINGS 40 | RSpec::Rails::DIRECTORY_MAPPINGS[:acceptance] = %w[spec acceptance] 41 | RSpec.configuration.infer_spec_type_from_file_location! 42 | end 43 | -------------------------------------------------------------------------------- /features/curl.feature: -------------------------------------------------------------------------------- 1 | Feature: cURL output 2 | Background: 3 | Given a file named "app_spec.rb" with: 4 | """ 5 | require "rspec_api_documentation" 6 | require "rspec_api_documentation/dsl" 7 | 8 | class App 9 | def self.call(env) 10 | if env["HTTP_ACCEPT"] == "foo" 11 | [200, {}, ["foo"]] 12 | else 13 | [406, {}, ["unknown content type"]] 14 | end 15 | end 16 | end 17 | 18 | RspecApiDocumentation.configure do |config| 19 | config.app = App 20 | config.curl_host = "example.org" 21 | end 22 | 23 | resource "FooBars" do 24 | get "/foobar" do 25 | header "Accept", "foo" 26 | 27 | example "Getting Foo" do 28 | do_request 29 | expect(response_body).to eq("foo") 30 | end 31 | end 32 | end 33 | """ 34 | 35 | Scenario: Not filtering headers in cURL 36 | Given a file named "config.rb" with: 37 | """ 38 | """ 39 | When I run `rspec app_spec.rb --require ./config.rb --format RspecApiDocumentation::ApiFormatter` 40 | 41 | Then the outputted docs should not filter out headers 42 | 43 | Scenario: Filtering out headers in cURL 44 | Given a file named "config.rb" with: 45 | """ 46 | require "rspec_api_documentation" 47 | 48 | RspecApiDocumentation.configure do |config| 49 | config.curl_headers_to_filter = ["Host", "Cookie"] 50 | end 51 | """ 52 | When I run `rspec app_spec.rb --require ./config.rb --format RspecApiDocumentation::ApiFormatter` 53 | 54 | Then the outputted docs should filter out headers 55 | -------------------------------------------------------------------------------- /rspec_api_documentation.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path("../lib/", __FILE__) 2 | $:.unshift lib unless $:.include?(lib) 3 | 4 | Gem::Specification.new do |s| 5 | s.name = "rspec_api_documentation" 6 | s.version = "5.1.0" 7 | s.platform = Gem::Platform::RUBY 8 | s.authors = ["Chris Cahoon", "Sam Goldman", "Eric Oestrich"] 9 | s.email = ["chris@smartlogicsolutions.com", "sam@smartlogicsolutions.com", "eric@smartlogicsolutions.com"] 10 | s.summary = "A double black belt for your docs" 11 | s.description = "Generate API docs from your test suite" 12 | s.homepage = "http://smartlogicsolutions.com" 13 | s.license = "MIT" 14 | 15 | s.required_rubygems_version = ">= 1.3.6" 16 | 17 | s.add_runtime_dependency "rspec", "~> 3.0" 18 | s.add_runtime_dependency "activesupport", ">= 3.0.0" 19 | s.add_runtime_dependency "mustache", "~> 1.0", ">= 0.99.4" 20 | 21 | s.add_development_dependency "bundler", "~> 1.0" 22 | s.add_development_dependency "fakefs", "~> 0.4" 23 | s.add_development_dependency "sinatra", "~> 1.4", ">= 1.4.4" 24 | s.add_development_dependency "aruba", "~> 0.5" 25 | s.add_development_dependency "capybara", "~> 2.2" 26 | s.add_development_dependency "rake", "~> 10.1" 27 | s.add_development_dependency "rack-test", "~> 0.6.2" 28 | s.add_development_dependency "rack-oauth2", "~> 1.2.2", ">= 1.0.7" 29 | s.add_development_dependency "webmock", "~> 1.7" 30 | s.add_development_dependency "rspec-its", "~> 1.0" 31 | s.add_development_dependency "faraday", "~> 0.9", ">= 0.9.0" 32 | s.add_development_dependency "thin", "~> 1.6", ">= 1.6.3" 33 | 34 | s.files = Dir.glob("lib/**/*") + Dir.glob("templates/**/*") 35 | s.require_path = "lib" 36 | end 37 | -------------------------------------------------------------------------------- /templates/rspec_api_documentation/textile_example.mustache: -------------------------------------------------------------------------------- 1 | h1. {{ resource_name }} API 2 | {{# resource_explanation }} 3 | 4 | {{{ resource_explanation }}} 5 | {{/ resource_explanation }} 6 | 7 | h2. {{ description }} 8 | 9 | h3. {{ http_method }} {{ route }} 10 | 11 | {{# explanation }} 12 | {{ explanation }} 13 | 14 | {{/ explanation }} 15 | {{# has_parameters? }} 16 | h3. Parameters 17 | {{# parameters }} 18 | 19 | Name : {{ name }} {{# required }} *- required -*{{/ required }} 20 | Description : {{ description }} 21 | {{/ parameters }} 22 | 23 | {{/ has_parameters? }} 24 | {{# has_response_fields? }} 25 | h3. Response Fields 26 | {{# response_fields }} 27 | 28 | Name : {{ name }} 29 | Description : {{ description }} 30 | {{/ response_fields }} 31 | 32 | {{/ has_response_fields? }} 33 | {{# requests }} 34 | h3. Request 35 | 36 | h4. Headers 37 | 38 |
{{ 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 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

If you are the application owner check the logs for more information.

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /features/disable_dsl.feature: -------------------------------------------------------------------------------- 1 | Feature: Disable DSL features 2 | Background: 3 | Given a file named "app.rb" with: 4 | """ 5 | class App 6 | def self.call(env) 7 | request = Rack::Request.new(env) 8 | response = Rack::Response.new 9 | response["Content-Type"] = "text/plain" 10 | response.write(request.params["status"]) 11 | response.write(request.params["method"]) 12 | response.finish 13 | end 14 | end 15 | """ 16 | And a file named "app_spec.rb" with: 17 | """ 18 | require "rspec_api_documentation" 19 | require "rspec_api_documentation/dsl" 20 | 21 | RspecApiDocumentation.configure do |config| 22 | config.app = App 23 | config.disable_dsl_status! 24 | config.disable_dsl_method! 25 | end 26 | 27 | resource "Orders" do 28 | get "/orders" do 29 | parameter :status, "Order status to search for" 30 | parameter :method, "Method of delivery to search for" 31 | 32 | example "Viewing all orders" do 33 | do_request :status => "pending" 34 | expect(response_status).to eq(200) 35 | expect(response_body).to eq("pending") 36 | end 37 | 38 | example "Checking the method" do 39 | do_request :method => "ground" 40 | expect(http_method).to eq(:get) 41 | expect(response_body).to eq("ground") 42 | end 43 | end 44 | end 45 | """ 46 | When I run `rspec app_spec.rb --require ./app.rb --format RspecApiDocumentation::ApiFormatter` 47 | 48 | Scenario: Output should have the correct error line 49 | Then the output should contain "2 examples, 0 failures" 50 | And the exit status should be 0 51 | -------------------------------------------------------------------------------- /templates/rspec_api_documentation/slate_example.mustache: -------------------------------------------------------------------------------- 1 | ## {{ description }} 2 | 3 | {{# explanation }} 4 | {{{ explanation }}} 5 | {{/ explanation }} 6 | 7 | ### Request 8 | 9 | #### Endpoint 10 | 11 | {{# requests}} 12 | ```plaintext 13 | {{ request_method }} {{ request_path }} 14 | {{ request_headers_text }} 15 | ``` 16 | {{/ requests}} 17 | 18 | `{{ http_method }} {{ route }}` 19 | 20 | #### Parameters 21 | 22 | {{# requests}} 23 | {{# request_query_parameters_text }} 24 | 25 | ```json 26 | {{ request_query_parameters_text }} 27 | ``` 28 | {{/ request_query_parameters_text }} 29 | {{# request_body }} 30 | 31 | ```json 32 | {{{ request_body }}} 33 | ``` 34 | {{/ request_body }} 35 | 36 | {{# has_parameters? }} 37 | 38 | | Name | Description | 39 | |:-----|:------------| 40 | {{# parameters }} 41 | | {{#scope}}{{scope}}[{{/scope}}{{ name }}{{#scope}}]{{/scope}} {{# required }}*required*{{/ required }} | {{{ description }}} | 42 | {{/ parameters }} 43 | 44 | {{/ has_parameters? }} 45 | {{^ has_parameters? }} 46 | None known. 47 | {{/ has_parameters? }} 48 | 49 | {{# response_status}} 50 | 51 | ### Response 52 | 53 | ```plaintext 54 | {{ response_headers_text }} 55 | {{ response_status }} {{ response_status_text}} 56 | ``` 57 | 58 | {{# response_body}} 59 | 60 | ```json 61 | {{{ response_body }}} 62 | ``` 63 | {{/response_body}} 64 | 65 | {{/ response_status}} 66 | 67 | {{# has_response_fields? }} 68 | 69 | #### Fields 70 | 71 | | Name | Description | 72 | |:-----------|:--------------------| 73 | {{# response_fields }} 74 | | {{#scope}}{{scope}}[{{/scope}}{{ name }}{{#scope}}]{{/scope}} | {{{ description }}} | 75 | {{/ response_fields }} 76 | 77 | {{/ has_response_fields? }} 78 | 79 | {{# curl }} 80 | ```shell 81 | {{{ curl }}} 82 | ``` 83 | {{/ curl }} 84 | {{/ requests}} 85 | -------------------------------------------------------------------------------- /example/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure static asset server for tests with Cache-Control for performance. 16 | config.serve_static_assets = true 17 | config.static_cache_control = 'public, max-age=3600' 18 | 19 | # Show full error reports and disable caching. 20 | config.consider_all_requests_local = true 21 | config.action_controller.perform_caching = false 22 | 23 | # Raise exceptions instead of rendering exception templates. 24 | config.action_dispatch.show_exceptions = false 25 | 26 | # Disable request forgery protection in test environment. 27 | config.action_controller.allow_forgery_protection = false 28 | 29 | # Tell Action Mailer not to deliver emails to the real world. 30 | # The :test delivery method accumulates sent emails in the 31 | # ActionMailer::Base.deliveries array. 32 | config.action_mailer.delivery_method = :test 33 | 34 | # Print deprecation notices to the stderr. 35 | config.active_support.deprecation = :stderr 36 | 37 | # Raises error for missing translations 38 | # config.action_view.raise_on_missing_translations = true 39 | end 40 | -------------------------------------------------------------------------------- /example/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

Maybe you tried to change something you didn't have access to.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /lib/rspec_api_documentation/writers/append_json_writer.rb: -------------------------------------------------------------------------------- 1 | require 'rspec_api_documentation/writers/formatter' 2 | 3 | module RspecApiDocumentation 4 | module Writers 5 | class AppendJsonWriter < JsonWriter 6 | def write 7 | index_file = docs_dir.join("index.json") 8 | if File.exists?(index_file) && (output = File.read(index_file)).length >= 2 9 | existing_index_hash = JSON.parse(output) 10 | end 11 | File.open(index_file, "w+") do |f| 12 | f.write Formatter.to_json(AppendJsonIndex.new(index, configuration, existing_index_hash)) 13 | end 14 | write_examples 15 | end 16 | 17 | def self.clear_docs(docs_dir) 18 | nil #noop 19 | end 20 | end 21 | 22 | class AppendJsonIndex < JsonIndex 23 | def initialize(index, configuration, existing_index_hash = nil) 24 | @index = index 25 | @configuration = configuration 26 | @existing_index_hash = clean_index_hash(existing_index_hash) 27 | end 28 | 29 | def as_json(opts = nil) 30 | sections.inject(@existing_index_hash) do |h, section| 31 | h[:resources].push(section_hash(section)) 32 | h 33 | end 34 | end 35 | 36 | def clean_index_hash(existing_index_hash) 37 | unless existing_index_hash.is_a?(Hash) && existing_index_hash["resources"].is_a?(Array) #check format 38 | existing_index_hash = {:resources => []} 39 | end 40 | existing_index_hash = existing_index_hash.deep_symbolize_keys 41 | existing_index_hash[:resources].map!(&:deep_symbolize_keys).reject! do |resource| 42 | resource_names = sections.map{|s| s[:resource_name]} 43 | resource_names.include? resource[:name] 44 | end 45 | existing_index_hash 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/rspec_api_documentation/rack_test_client.rb: -------------------------------------------------------------------------------- 1 | module RspecApiDocumentation 2 | class RackTestClient < ClientBase 3 | 4 | delegate :last_request, :last_response, :to => :rack_test_session 5 | private :last_request, :last_response 6 | 7 | def request_headers 8 | env_to_headers(last_request.env) 9 | end 10 | 11 | def response_headers 12 | last_response.headers 13 | end 14 | 15 | def query_string 16 | last_request.env["QUERY_STRING"] 17 | end 18 | 19 | def status 20 | last_response.status 21 | end 22 | 23 | def response_body 24 | last_response.body 25 | end 26 | 27 | def request_content_type 28 | last_request.content_type 29 | end 30 | 31 | def response_content_type 32 | last_response.content_type 33 | end 34 | 35 | protected 36 | 37 | def do_request(method, path, params, request_headers) 38 | rack_test_session.send(method, path, params, headers(method, path, params, request_headers)) 39 | end 40 | 41 | def headers(*args) 42 | headers_to_env(super) 43 | end 44 | 45 | def handle_multipart_body(request_headers, request_body) 46 | parsed_parameters = Rack::Request.new({ 47 | "CONTENT_TYPE" => request_headers["Content-Type"], 48 | "rack.input" => StringIO.new(request_body) 49 | }).params 50 | 51 | clean_out_uploaded_data(parsed_parameters,request_body) 52 | end 53 | 54 | private 55 | 56 | def rack_test_session 57 | @rack_test_session ||= Struct.new(:app) do 58 | begin 59 | require "rack/test" 60 | include Rack::Test::Methods 61 | rescue LoadError 62 | raise "#{self.class.name} requires Rack::Test >= 0.5.5. Please add it to your test dependencies." 63 | end 64 | end.new(app) 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/rspec_api_documentation/dsl/endpoint/set_param.rb: -------------------------------------------------------------------------------- 1 | module RspecApiDocumentation 2 | module DSL 3 | module Endpoint 4 | class SetParam 5 | def initialize(parent, hash, param) 6 | @parent = parent 7 | @hash = hash 8 | @param = param 9 | end 10 | 11 | def call 12 | return hash if path_params.include?(path_name) 13 | return hash unless method_name 14 | 15 | hash.deep_merge build_param_hash(key_scope || [key]) 16 | end 17 | 18 | private 19 | 20 | attr_reader :parent, :hash, :param 21 | delegate :example_group, :example, to: :parent 22 | 23 | def key 24 | @key ||= param[:name] 25 | end 26 | 27 | def key_scope 28 | @key_scope ||= param[:scope] && Array(param[:scope]).dup.push(key) 29 | end 30 | 31 | def scoped_key 32 | @scoped_key ||= key_scope && key_scope.join('_') 33 | end 34 | 35 | def custom_method_name 36 | param[:method] 37 | end 38 | 39 | def path_name 40 | scoped_key || key 41 | end 42 | 43 | def path_params 44 | example.metadata[:route].scan(/:(\w+)/).flatten 45 | end 46 | 47 | def method_name 48 | if custom_method_name 49 | custom_method_name if example_group.respond_to?(custom_method_name) 50 | elsif scoped_key && example_group.respond_to?(scoped_key) 51 | scoped_key 52 | elsif key && example_group.respond_to?(key) 53 | key 54 | end 55 | end 56 | 57 | def build_param_hash(keys) 58 | value = keys[1] ? build_param_hash(keys[1..-1]) : example_group.send(method_name) 59 | { keys[0].to_s => value } 60 | end 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /example/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

You may have mistyped the address or the page may have moved.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /spec/writers/index_helper_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe RspecApiDocumentation::Writers::IndexHelper do 4 | describe "#sections" do 5 | let(:example_1) { double(:resource_name => "Order", :description => "Updating an order", resource_explanation: 'Resource explanation') } 6 | let(:example_2) { double(:resource_name => "Order", :description => "Creating an order", resource_explanation: 'Resource explanation') } 7 | let(:example_3) { double(:resource_name => "Cart", :description => "Creating an cart", resource_explanation: 'Resource explanation') } 8 | let(:examples) { [example_1, example_2, example_3] } 9 | 10 | context "with default value for keep_source_order" do 11 | let(:configuration) { RspecApiDocumentation::Configuration.new } 12 | subject { RspecApiDocumentation::Writers::IndexHelper.sections(examples, configuration) } 13 | 14 | it "should order resources by resource name" do 15 | expect(subject.map { |resource| resource[:resource_name] }).to eq(["Cart", "Order"]) 16 | end 17 | 18 | it "should order examples by description" do 19 | expect(subject.detect { |resource| resource[:resource_name] == "Order"}[:examples]).to eq([example_2, example_1]) 20 | end 21 | end 22 | 23 | context "with keep_source_order set to true" do 24 | subject { RspecApiDocumentation::Writers::IndexHelper.sections(examples, double(:keep_source_order => true)) } 25 | 26 | it "should order resources by source code declaration" do 27 | expect(subject.map { |resource| resource[:resource_name] }).to eq(["Order", "Cart"]) 28 | end 29 | 30 | it "should order examples by source code declaration" do 31 | expect(subject.detect { |resource| resource[:resource_name] == "Order"}[:examples]).to eq([example_1, example_2]) 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /templates/rspec_api_documentation/markdown_example.mustache: -------------------------------------------------------------------------------- 1 | # {{ resource_name }} API 2 | {{# resource_explanation }} 3 | 4 | {{{ resource_explanation }}} 5 | {{/ resource_explanation }} 6 | 7 | ## {{ description }} 8 | 9 | ### {{ http_method }} {{ route }} 10 | {{# explanation }} 11 | 12 | {{ explanation }} 13 | {{/ explanation }} 14 | {{# has_parameters? }} 15 | 16 | ### Parameters 17 | 18 | | Name | Description | Required | Scope | 19 | |------|-------------|----------|-------| 20 | {{# parameters }} 21 | | {{ name }} | {{ description }} | {{ required }} | {{ scope }} | 22 | {{/ parameters }} 23 | 24 | {{/ has_parameters? }} 25 | {{# has_response_fields? }} 26 | 27 | ### Response Fields 28 | 29 | | Name | Description | Scope | 30 | |------|-------------|-------| 31 | {{# response_fields }} 32 | | {{ name }} | {{ description }} | {{ scope }} | 33 | {{/ response_fields }} 34 | 35 | {{/ has_response_fields? }} 36 | {{# requests }} 37 | ### Request 38 | 39 | #### Headers 40 | 41 |
{{ 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_name}} API 5 | 6 | 9 | 10 | 11 |
12 |

{{resource_name}} API

13 | {{# resource_explanation }} 14 | 15 |

{{{ resource_explanation }}}

16 | {{/ resource_explanation }} 17 | 18 |
19 |

{{ description }}

20 |

{{ http_method }} {{ route }}

21 | {{# explanation }} 22 |

23 | {{{ explanation }}} 24 |

25 | {{/ explanation }} 26 | 27 | {{# has_parameters? }} 28 |

Parameters

29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | {{# parameters }} 38 | 39 | 40 | {{# scope }} 41 | {{ scope }}[{{ name }}] 42 | {{/ scope }} 43 | {{^ scope }} 44 | {{ name }} 45 | {{/ scope }} 46 | 47 | 50 | 51 | {{/ parameters }} 52 | 53 |
NameDescription
48 | {{ description }} 49 |
54 | {{/ has_parameters? }} 55 | 56 | {{# has_response_fields? }} 57 |

Response Fields

58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | {{# response_fields }} 67 | 68 | 76 | 79 | 80 | {{/ response_fields }} 81 | 82 |
NameDescription
69 | {{# scope }} 70 | {{ scope }}[{{ name }}] 71 | {{/ scope }} 72 | {{^ scope }} 73 | {{ name }} 74 | {{/ scope }} 75 | 77 | {{ description }} 78 |
83 | {{/ has_response_fields? }} 84 | 85 | {{# requests }} 86 |

Request

87 | 88 | {{# request_headers_text }} 89 |

Headers

90 |
{{ request_headers_text }}
91 | {{/ request_headers_text }} 92 | 93 |

Route

94 |
{{ request_method }} {{ request_path }}
95 | 96 | {{# request_query_parameters_text }} 97 |

Query Parameters

98 |
{{ request_query_parameters_text }}
99 | {{/ request_query_parameters_text }} 100 | 101 | {{# request_body }} 102 |

Body

103 |
{{{ request_body }}}
104 | {{/ request_body }} 105 | 106 | {{# curl }} 107 |

cURL

108 |
{{ curl }}
109 | {{/ curl }} 110 | 111 | {{# response_status }} 112 |

Response

113 | {{# response_headers_text }} 114 |

Headers

115 |
{{ response_headers_text }}
116 | {{/ response_headers_text }} 117 |

Status

118 |
{{ response_status }} {{ response_status_text}}
119 | {{# response_body }} 120 |

Body

121 |
{{ response_body }}
122 | {{/ response_body }} 123 | {{/ response_status }} 124 | {{/ requests }} 125 |
126 |
127 | 128 | 129 | -------------------------------------------------------------------------------- /example/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: git://github.com/smartlogic/raddocs.git 3 | revision: 9cf49c1ef3b3d7dc3bf8e19ef75021040df04652 4 | specs: 5 | raddocs (0.4.0) 6 | haml (~> 4.0, >= 4.0.4) 7 | json (~> 1.8, >= 1.8.1) 8 | sinatra (~> 1.3, >= 1.3.0) 9 | 10 | PATH 11 | remote: ../ 12 | specs: 13 | rspec_api_documentation (4.7.0) 14 | activesupport (>= 3.0.0) 15 | json (~> 1.4, >= 1.4.6) 16 | mustache (~> 0.99, >= 0.99.4) 17 | rspec (>= 3.0.0) 18 | 19 | GEM 20 | remote: https://rubygems.org/ 21 | specs: 22 | actionmailer (4.2.5.1) 23 | actionpack (= 4.2.5.1) 24 | actionview (= 4.2.5.1) 25 | activejob (= 4.2.5.1) 26 | mail (~> 2.5, >= 2.5.4) 27 | rails-dom-testing (~> 1.0, >= 1.0.5) 28 | actionpack (4.2.5.1) 29 | actionview (= 4.2.5.1) 30 | activesupport (= 4.2.5.1) 31 | rack (~> 1.6) 32 | rack-test (~> 0.6.2) 33 | rails-dom-testing (~> 1.0, >= 1.0.5) 34 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 35 | actionview (4.2.5.1) 36 | activesupport (= 4.2.5.1) 37 | builder (~> 3.1) 38 | erubis (~> 2.7.0) 39 | rails-dom-testing (~> 1.0, >= 1.0.5) 40 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 41 | activejob (4.2.5.1) 42 | activesupport (= 4.2.5.1) 43 | globalid (>= 0.3.0) 44 | activemodel (4.2.5.1) 45 | activesupport (= 4.2.5.1) 46 | builder (~> 3.1) 47 | activerecord (4.2.5.1) 48 | activemodel (= 4.2.5.1) 49 | activesupport (= 4.2.5.1) 50 | arel (~> 6.0) 51 | activesupport (4.2.5.1) 52 | i18n (~> 0.7) 53 | json (~> 1.7, >= 1.7.7) 54 | minitest (~> 5.1) 55 | thread_safe (~> 0.3, >= 0.3.4) 56 | tzinfo (~> 1.1) 57 | arel (6.0.3) 58 | builder (3.2.2) 59 | concurrent-ruby (1.0.0) 60 | diff-lcs (1.2.5) 61 | erubis (2.7.0) 62 | globalid (0.3.6) 63 | activesupport (>= 4.1.0) 64 | haml (4.0.5) 65 | tilt 66 | i18n (0.7.0) 67 | json (1.8.3) 68 | loofah (2.0.3) 69 | nokogiri (>= 1.5.9) 70 | mail (2.6.3) 71 | mime-types (>= 1.16, < 3) 72 | mime-types (2.99) 73 | mini_portile2 (2.0.0) 74 | minitest (5.8.4) 75 | mustache (0.99.8) 76 | nokogiri (1.6.7.2) 77 | mini_portile2 (~> 2.0.0.rc2) 78 | rack (1.6.4) 79 | rack-protection (1.5.3) 80 | rack 81 | rack-test (0.6.3) 82 | rack (>= 1.0) 83 | rails (4.2.5.1) 84 | actionmailer (= 4.2.5.1) 85 | actionpack (= 4.2.5.1) 86 | actionview (= 4.2.5.1) 87 | activejob (= 4.2.5.1) 88 | activemodel (= 4.2.5.1) 89 | activerecord (= 4.2.5.1) 90 | activesupport (= 4.2.5.1) 91 | bundler (>= 1.3.0, < 2.0) 92 | railties (= 4.2.5.1) 93 | sprockets-rails 94 | rails-deprecated_sanitizer (1.0.3) 95 | activesupport (>= 4.2.0.alpha) 96 | rails-dom-testing (1.0.7) 97 | activesupport (>= 4.2.0.beta, < 5.0) 98 | nokogiri (~> 1.6.0) 99 | rails-deprecated_sanitizer (>= 1.0.1) 100 | rails-html-sanitizer (1.0.3) 101 | loofah (~> 2.0) 102 | railties (4.2.5.1) 103 | actionpack (= 4.2.5.1) 104 | activesupport (= 4.2.5.1) 105 | rake (>= 0.8.7) 106 | thor (>= 0.18.1, < 2.0) 107 | rake (10.5.0) 108 | rspec (3.0.0) 109 | rspec-core (~> 3.0.0) 110 | rspec-expectations (~> 3.0.0) 111 | rspec-mocks (~> 3.0.0) 112 | rspec-core (3.0.4) 113 | rspec-support (~> 3.0.0) 114 | rspec-expectations (3.0.4) 115 | diff-lcs (>= 1.2.0, < 2.0) 116 | rspec-support (~> 3.0.0) 117 | rspec-mocks (3.0.4) 118 | rspec-support (~> 3.0.0) 119 | rspec-rails (3.0.2) 120 | actionpack (>= 3.0) 121 | activesupport (>= 3.0) 122 | railties (>= 3.0) 123 | rspec-core (~> 3.0.0) 124 | rspec-expectations (~> 3.0.0) 125 | rspec-mocks (~> 3.0.0) 126 | rspec-support (~> 3.0.0) 127 | rspec-support (3.0.4) 128 | sinatra (1.4.5) 129 | rack (~> 1.4) 130 | rack-protection (~> 1.4) 131 | tilt (~> 1.3, >= 1.3.4) 132 | spring (1.1.3) 133 | sprockets (3.5.2) 134 | concurrent-ruby (~> 1.0) 135 | rack (> 1, < 3) 136 | sprockets-rails (3.0.1) 137 | actionpack (>= 4.0) 138 | activesupport (>= 4.0) 139 | sprockets (>= 3.0.0) 140 | sqlite3 (1.3.9) 141 | thor (0.19.1) 142 | thread_safe (0.3.5) 143 | tilt (1.4.1) 144 | tzinfo (1.2.2) 145 | thread_safe (~> 0.1) 146 | 147 | PLATFORMS 148 | ruby 149 | 150 | DEPENDENCIES 151 | raddocs! 152 | rails (= 4.2.5.1) 153 | rspec-rails 154 | rspec_api_documentation! 155 | spring 156 | sqlite3 157 | 158 | BUNDLED WITH 159 | 1.11.2 160 | -------------------------------------------------------------------------------- /features/html_documentation.feature: -------------------------------------------------------------------------------- 1 | Feature: Generate HTML documentation from test examples 2 | 3 | Background: 4 | Given a file named "app.rb" with: 5 | """ 6 | class App 7 | def self.call(env) 8 | request = Rack::Request.new(env) 9 | response = Rack::Response.new 10 | response["Content-Type"] = "application/json" 11 | response.write({ 12 | "hello" => request.params["target"], 13 | "more_greetings" => { "bonjour" => { "message" => "le monde" } } 14 | }.to_json) 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 = "Example API" 27 | config.api_explanation = "

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 | --------------------------------------------------------------------------------