diff --git a/features/slate_documentation.feature b/features/slate_documentation.feature
new file mode 100644
index 00000000..c4be5499
--- /dev/null
+++ b/features/slate_documentation.feature
@@ -0,0 +1,290 @@
+Feature: Generate Slate documentation from test examples
+
+ Background:
+ Given a file named "app.rb" with:
+ """
+ require 'sinatra'
+
+ class App < Sinatra::Base
+ get '/orders' do
+ content_type :json
+
+ [200, {
+ :page => 1,
+ :orders => [
+ { name: 'Order 1', amount: 9.99, description: nil },
+ { name: 'Order 2', amount: 100.0, description: 'A great order' }
+ ]
+ }.to_json]
+ end
+
+ get '/orders/:id' do
+ content_type :json
+
+ [200, { order: { name: 'Order 1', amount: 100.0, description: 'A great order' } }.to_json]
+ end
+
+ post '/orders' do
+ 201
+ end
+
+ put '/orders/:id' do
+ 200
+ end
+
+ delete '/orders/:id' do
+ 200
+ end
+
+ get '/help' do
+ [200, 'Welcome Henry !']
+ end
+ end
+ """
+ And a file named "app_spec.rb" with:
+ """
+ require "rspec_api_documentation"
+ require "rspec_api_documentation/dsl"
+
+ RspecApiDocumentation.configure do |config|
+ config.app = App
+ config.api_name = "Example API"
+ config.format = :slate
+ config.curl_host = 'http://localhost:3000'
+ config.request_headers_to_include = %w[Content-Type Host]
+ config.response_headers_to_include = %w[Content-Type Content-Length]
+ end
+
+ resource 'Orders' do
+ get '/orders' do
+ response_field :page, "Current page"
+
+ example_request 'Getting a list of orders' do
+ status.should eq(200)
+ response_body.should eq('{"page":1,"orders":[{"name":"Order 1","amount":9.99,"description":null},{"name":"Order 2","amount":100.0,"description":"A great order"}]}')
+ end
+ end
+
+ get '/orders/:id' do
+ let(:id) { 1 }
+
+ example_request 'Getting a specific order' do
+ status.should eq(200)
+ response_body.should == '{"order":{"name":"Order 1","amount":100.0,"description":"A great order"}}'
+ end
+ end
+
+ post '/orders' do
+ parameter :name, 'Name of order', :required => true
+ parameter :amount, 'Amount paid', :required => true
+ parameter :description, 'Some comments on the order'
+
+ let(:name) { "Order 3" }
+ let(:amount) { 33.0 }
+
+ example_request 'Creating an order' do
+ status.should == 201
+ end
+ end
+
+ put '/orders/:id' do
+ parameter :name, 'Name of order', :required => true
+ parameter :amount, 'Amount paid', :required => true
+ parameter :description, 'Some comments on the order'
+
+ let(:id) { 2 }
+ let(:name) { "Updated name" }
+
+ example_request 'Updating an order' do
+ status.should == 200
+ end
+ end
+
+ delete "/orders/:id" do
+ let(:id) { 1 }
+
+ example_request "Deleting an order" do
+ status.should == 200
+ end
+ end
+ end
+
+ resource 'Help' do
+ get '/help' do
+ example_request 'Getting welcome message' do
+ status.should eq(200)
+ response_body.should == 'Welcome Henry !'
+ end
+ end
+
+ end
+ """
+ When I run `rspec app_spec.rb --require ./app.rb --format RspecApiDocumentation::ApiFormatter`
+
+ Scenario: Output helpful progress to the console
+ Then the output should contain:
+ """
+ Generating API Docs
+ Orders
+ GET /orders
+ * Getting a list of orders
+ GET /orders/:id
+ * Getting a specific order
+ POST /orders
+ * Creating an order
+ PUT /orders/:id
+ * Updating an order
+ DELETE /orders/:id
+ * Deleting an order
+ Help
+ GET /help
+ * Getting welcome message
+ """
+ And the output should contain "6 examples, 0 failures"
+ And the exit status should be 0
+
+ Scenario: Example 'Getting a list of orders' docs should look like we expect
+ Then the file "doc/api/_generated_examples.markdown" should contain:
+ """
+ ## Getting a list of orders
+
+ ### Request
+
+ #### Endpoint
+
+ ```
+ GET /orders
+ Host: example.org
+ ```
+
+ `GET /orders`
+
+
+ #### Parameters
+
+
+ None known.
+
+
+ ### Response
+
+ ```
+ Content-Type: application/json
+ Content-Length: 137
+ 200 OK
+ ```
+
+
+ ```json
+ {
+ "page": 1,
+ "orders": [
+ {
+ "name": "Order 1",
+ "amount": 9.99,
+ "description": null
+ },
+ {
+ "name": "Order 2",
+ "amount": 100.0,
+ "description": "A great order"
+ }
+ ]
+ }
+ ```
+
+
+
+ #### Fields
+
+ | Name | Description |
+ |:-----------|:--------------------|
+ | page | Current page |
+
+
+
+ ### cURL
+
+ curl "http://localhost:3000/orders" -X GET \
-H "Host: example.org" \
-H "Cookie: "
+ """
+
+ Scenario: Example 'Creating an order' docs should look like we expect
+ Then the file "doc/api/_generated_examples.markdown" should contain:
+ """
+ ## Creating an order
+
+ ### Request
+
+ #### Endpoint
+
+ ```
+ POST /orders
+ Host: example.org
+ Content-Type: application/x-www-form-urlencoded
+ ```
+
+ `POST /orders`
+
+
+ #### Parameters
+
+
+ ```json
+ name=Order+3&amount=33.0
+ ```
+
+
+ | Name | Description |
+ |:-----|:------------|
+ | name *required* | Name of order |
+ | amount *required* | Amount paid |
+ | description | Some comments on the order |
+
+
+
+ ### Response
+
+ ```
+ Content-Type: text/html;charset=utf-8
+ Content-Length: 0
+ 201 Created
+ ```
+
+
+
+
+
+ ### cURL
+
+ curl "http://localhost:3000/orders" -d 'name=Order+3&amount=33.0' -X POST \
-H "Host: example.org" \
-H "Content-Type: application/x-www-form-urlencoded" \
-H "Cookie: "
+ """
+
+ Scenario: Example 'Deleting an order' docs should be created
+ Then the file "doc/api/_generated_examples.markdown" should contain:
+ """
+ ## Deleting an order
+ """
+
+ Scenario: Example 'Getting a list of orders' docs should be created
+ Then the file "doc/api/_generated_examples.markdown" should contain:
+ """
+ ## Getting a list of orders
+ """
+
+ Scenario: Example 'Getting a specific order' docs should be created
+ Then the file "doc/api/_generated_examples.markdown" should contain:
+ """
+ ## Getting a specific order
+ """
+
+ Scenario: Example 'Updating an order' docs should be created
+ Then the file "doc/api/_generated_examples.markdown" should contain:
+ """
+ ## Updating an order
+ """
+
+ Scenario: Example 'Getting welcome message' docs should be created
+ Then the file "doc/api/_generated_examples.markdown" should contain:
+ """
+ ## Getting welcome message
+ """
diff --git a/lib/rspec_api_documentation.rb b/lib/rspec_api_documentation.rb
index 3e07da93..569ff90d 100644
--- a/lib/rspec_api_documentation.rb
+++ b/lib/rspec_api_documentation.rb
@@ -43,6 +43,7 @@ module Writers
autoload :IndexHelper
autoload :CombinedTextWriter
autoload :CombinedJsonWriter
+ autoload :SlateWriter
end
module Views
@@ -56,6 +57,7 @@ module Views
autoload :TextileExample
autoload :MarkdownIndex
autoload :MarkdownExample
+ autoload :SlateExample
end
def self.configuration
diff --git a/lib/rspec_api_documentation/views/slate_example.rb b/lib/rspec_api_documentation/views/slate_example.rb
new file mode 100644
index 00000000..1d23ef69
--- /dev/null
+++ b/lib/rspec_api_documentation/views/slate_example.rb
@@ -0,0 +1,32 @@
+module RspecApiDocumentation
+ module Views
+ class SlateExample < MarkdownExample
+ def initialize(example, configuration)
+ super
+ self.template_name = "rspec_api_documentation/slate_example"
+ end
+
+ def curl_with_linebreaks
+ requests.map {|request| request[:curl].lines }.flatten.map do |line|
+ line.rstrip.gsub("\t", ' ').gsub(' ', ' ').gsub('\\', '\')
+ end.join "
"
+ end
+
+ def explanation_with_linebreaks
+ explanation.gsub "\n", "
\n"
+ end
+
+ def write
+ File.open(configuration.docs_dir.join("#{FILENAME}.#{extension}"), 'w+') do |file|
+ file.write "# #{configuration.api_name}\n\n"
+ index.examples.sort_by!(&:description) unless configuration.keep_source_order
+
+ index.examples.each do |example|
+ markup_example = markup_example_class.new(example, configuration)
+ file.write markup_example.render
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/rspec_api_documentation/writers/slate_writer.rb b/lib/rspec_api_documentation/writers/slate_writer.rb
new file mode 100644
index 00000000..758a6b37
--- /dev/null
+++ b/lib/rspec_api_documentation/writers/slate_writer.rb
@@ -0,0 +1,29 @@
+module RspecApiDocumentation
+ module Writers
+
+ class SlateWriter < MarkdownWriter
+ FILENAME = '_generated_examples'
+
+ def self.clear_docs(docs_dir)
+ FileUtils.mkdir_p(docs_dir)
+ FileUtils.rm Dir[File.join docs_dir, "#{FILENAME}.*"]
+ end
+
+ def markup_example_class
+ RspecApiDocumentation::Views::SlateExample
+ end
+
+ def write
+ File.open(configuration.docs_dir.join("#{FILENAME}.#{extension}"), 'w+') do |file|
+ file.write "# #{configuration.api_name}\n\n"
+ index.examples.sort_by!(&:description) unless configuration.keep_source_order
+
+ index.examples.each do |example|
+ markup_example = markup_example_class.new(example, configuration)
+ file.write markup_example.render
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/views/slate_example_spec.rb b/spec/views/slate_example_spec.rb
new file mode 100644
index 00000000..0a1b47c5
--- /dev/null
+++ b/spec/views/slate_example_spec.rb
@@ -0,0 +1,64 @@
+require 'spec_helper'
+
+describe RspecApiDocumentation::Views::SlateExample do
+ let(:metadata) { { :resource_name => "Orders" } }
+ let(:group) { RSpec::Core::ExampleGroup.describe("Orders", metadata) }
+ let(:rspec_example) { group.example("Ordering a cup of coffee") {} }
+ let(:rad_example) do
+ RspecApiDocumentation::Example.new(rspec_example, configuration)
+ end
+ let(:configuration) { RspecApiDocumentation::Configuration.new }
+ let(:slate_example) { described_class.new(rad_example, configuration) }
+
+ describe '#curl_with_linebreaks' do
+ subject { slate_example.curl_with_linebreaks }
+
+ before(:each) { allow(slate_example).to receive(:requests).and_return requests }
+
+ context 'marshaling' do
+ let(:requests) { [{curl: 'One'}, {curl: "Two \nThree" }, {curl: 'Four '}] }
+
+ it 'joins all the Curl requests with linebreaks, stripping trailing whitespace' do
+ expect(subject).to be == [
+ 'One', 'Two', 'Three', 'Four'
+ ].join('
')
+ end
+ end
+
+ context 'escaping' do
+ let(:requests) { [{curl: string}] }
+
+ context 'spaces' do
+ let(:string) { 'a b' }
+
+ it 'replaces them with nonbreaking spaces' do
+ expect(subject).to be == 'a b'
+ end
+ end
+
+ context 'tabs' do
+ let(:string) { "a\tb" }
+
+ it 'replaces them with two nonbreaking spaces' do
+ expect(subject).to be == 'a b'
+ end
+ end
+
+ context 'backslashes' do
+ let(:string) { 'a\\b'}
+
+ it 'replaces them with an HTML entity' do
+ expect(subject).to be == 'a\b'
+ end
+ end
+ end
+ end
+
+ describe '#explanation_with_linebreaks' do
+ it 'returns the explanation with HTML linebreaks' do
+ explanation = "Line 1\nLine 2\nLine 3\Line 4"
+ allow(slate_example).to receive(:explanation).and_return explanation
+ expect(slate_example.explanation_with_linebreaks).to be == explanation.gsub("\n", "
\n")
+ end
+ end
+end
diff --git a/spec/writers/slate_writer_spec.rb b/spec/writers/slate_writer_spec.rb
new file mode 100644
index 00000000..9e6e33b0
--- /dev/null
+++ b/spec/writers/slate_writer_spec.rb
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+require 'spec_helper'
+
+describe RspecApiDocumentation::Writers::SlateWriter do
+ let(:index) { RspecApiDocumentation::Index.new }
+ let(:configuration) { RspecApiDocumentation::Configuration.new }
+
+ describe ".write" do
+ let(:writer) { double(:writer) }
+
+ it "should build a new writer and write the docs" do
+ allow(described_class).to receive(:new).with(index, configuration).and_return(writer)
+ expect(writer).to receive(:write)
+ described_class.write(index, configuration)
+ end
+ end
+
+ context 'instance methods' do
+ let(:writer) { described_class.new(index, configuration) }
+
+ describe '#markup_example_class' do
+ subject { writer.markup_example_class }
+ it { is_expected.to be == RspecApiDocumentation::Views::SlateExample }
+ end
+ end
+end
diff --git a/templates/rspec_api_documentation/slate_example.mustache b/templates/rspec_api_documentation/slate_example.mustache
new file mode 100644
index 00000000..66262d62
--- /dev/null
+++ b/templates/rspec_api_documentation/slate_example.mustache
@@ -0,0 +1,86 @@
+## {{ description }}
+
+### Request
+
+#### Endpoint
+
+{{# requests}}
+```
+{{ request_method }} {{ request_path }}
+{{ request_headers_text }}
+```
+{{/ requests}}
+
+`{{ http_method }} {{ route }}`
+
+{{# explanation }}
+
+{{{ explanation_with_linebreaks }}}
+{{/ explanation }}
+
+#### Parameters
+
+{{# requests}}
+{{# request_query_parameters_text }}
+
+```json
+{{ request_query_parameters_text }}
+```
+{{/ request_query_parameters_text }}
+{{# request_body }}
+
+```json
+{{{ request_body }}}
+```
+{{/ request_body }}
+
+{{# has_parameters? }}
+
+| Name | Description |
+|:-----|:------------|
+{{# parameters }}
+| {{#scope}}{{scope}}[{{/scope}}{{ name }}{{#scope}}]{{/scope}} {{# required }}*required*{{/ required }} | {{{ description }}} |
+{{/ parameters }}
+
+{{/ has_parameters? }}
+{{^ has_parameters? }}
+None known.
+{{/ has_parameters? }}
+
+{{# response_status}}
+
+### Response
+
+```
+{{ response_headers_text }}
+{{ response_status }} {{ response_status_text}}
+```
+
+{{# response_body}}
+
+```json
+{{{ response_body }}}
+```
+{{/response_body}}
+
+{{/ response_status}}
+
+{{# has_response_fields? }}
+
+#### Fields
+
+| Name | Description |
+|:-----------|:--------------------|
+{{# response_fields }}
+| {{#scope}}{{scope}}[{{/scope}}{{ name }}{{#scope}}]{{/scope}} | {{{ description }}} |
+{{/ response_fields }}
+
+{{/ has_response_fields? }}
+
+{{# curl }}
+
+### cURL
+
+{{{ curl_with_linebreaks }}}
+{{/ curl }}
+{{/ requests}}