rspec_api_documentation 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/lib/rspec_api_documentation.rb +36 -0
- data/lib/rspec_api_documentation/api_documentation.rb +38 -0
- data/lib/rspec_api_documentation/api_formatter.rb +45 -0
- data/lib/rspec_api_documentation/configuration.rb +66 -0
- data/lib/rspec_api_documentation/dsl.rb +204 -0
- data/lib/rspec_api_documentation/example.rb +49 -0
- data/lib/rspec_api_documentation/html_writer.rb +72 -0
- data/lib/rspec_api_documentation/index.rb +7 -0
- data/lib/rspec_api_documentation/index_writer.rb +11 -0
- data/lib/rspec_api_documentation/json_writer.rb +93 -0
- data/lib/rspec_api_documentation/railtie.rb +7 -0
- data/lib/rspec_api_documentation/test_client.rb +124 -0
- data/lib/rspec_api_documentation/test_server.rb +44 -0
- data/lib/tasks/docs.rake +9 -0
- metadata +151 -0
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
|
3
|
+
module RspecApiDocumentation
|
4
|
+
extend ActiveSupport::Autoload
|
5
|
+
|
6
|
+
require 'rspec_api_documentation/railtie' if defined?(Rails)
|
7
|
+
include ActiveSupport::JSON
|
8
|
+
|
9
|
+
eager_autoload do
|
10
|
+
autoload :Configuration
|
11
|
+
autoload :ApiDocumentation
|
12
|
+
autoload :ApiFormatter
|
13
|
+
autoload :Example
|
14
|
+
autoload :ExampleGroup
|
15
|
+
autoload :Index
|
16
|
+
autoload :TestClient
|
17
|
+
end
|
18
|
+
|
19
|
+
autoload :DSL
|
20
|
+
autoload :TestServer
|
21
|
+
autoload :HtmlWriter
|
22
|
+
autoload :JsonWriter
|
23
|
+
autoload :IndexWriter
|
24
|
+
|
25
|
+
def self.configuration
|
26
|
+
@configuration ||= Configuration.new
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.documentations
|
30
|
+
@documentations ||= configuration.map { |config| ApiDocumentation.new(config) }
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.configure
|
34
|
+
yield configuration if block_given?
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,38 @@
|
|
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
|
+
if File.exists?(docs_dir)
|
14
|
+
FileUtils.rm_rf(docs_dir, :secure => true)
|
15
|
+
end
|
16
|
+
FileUtils.mkdir_p(docs_dir)
|
17
|
+
end
|
18
|
+
|
19
|
+
def document_example(rspec_example)
|
20
|
+
example = Example.new(rspec_example, configuration)
|
21
|
+
if example.should_document?
|
22
|
+
index.examples << example
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def write
|
27
|
+
writers.each do |writer|
|
28
|
+
writer.write(index, configuration)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def writers
|
33
|
+
[*configuration.format].map do |format|
|
34
|
+
RspecApiDocumentation.const_get("#{format}_writer".classify)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'rspec/core/formatters/base_formatter'
|
2
|
+
|
3
|
+
module RspecApiDocumentation
|
4
|
+
class ApiFormatter < RSpec::Core::Formatters::BaseFormatter
|
5
|
+
def initialize(output)
|
6
|
+
super
|
7
|
+
|
8
|
+
output.puts "Generating API Docs"
|
9
|
+
end
|
10
|
+
|
11
|
+
def start(example_count)
|
12
|
+
super
|
13
|
+
|
14
|
+
RspecApiDocumentation.documentations.each(&:clear_docs)
|
15
|
+
end
|
16
|
+
|
17
|
+
def example_group_started(example_group)
|
18
|
+
super
|
19
|
+
|
20
|
+
output.puts " #{example_group.description}"
|
21
|
+
end
|
22
|
+
|
23
|
+
def example_passed(example)
|
24
|
+
super
|
25
|
+
|
26
|
+
output.puts " * #{example.description}"
|
27
|
+
|
28
|
+
RspecApiDocumentation.documentations.each do |documentation|
|
29
|
+
documentation.document_example(example)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def example_failed(example)
|
34
|
+
super
|
35
|
+
|
36
|
+
output.puts " ! #{example.description} (FAILED)"
|
37
|
+
end
|
38
|
+
|
39
|
+
def stop
|
40
|
+
super
|
41
|
+
|
42
|
+
RspecApiDocumentation.documentations.each(&:write)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module RspecApiDocumentation
|
2
|
+
class Configuration
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
attr_reader :parent
|
6
|
+
|
7
|
+
def initialize(parent = nil)
|
8
|
+
@parent = parent
|
9
|
+
@settings = parent.settings.clone if parent
|
10
|
+
end
|
11
|
+
|
12
|
+
def groups
|
13
|
+
@groups ||= []
|
14
|
+
end
|
15
|
+
|
16
|
+
def define_group(name, &block)
|
17
|
+
subconfig = self.class.new(self)
|
18
|
+
subconfig.filter = name
|
19
|
+
subconfig.docs_dir = self.docs_dir.join(name.to_s)
|
20
|
+
yield subconfig
|
21
|
+
groups << subconfig
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.add_setting(name, opts = {})
|
25
|
+
define_method("#{name}=") { |value| settings[name] = value }
|
26
|
+
define_method("#{name}") do
|
27
|
+
if settings.has_key?(name)
|
28
|
+
settings[name]
|
29
|
+
elsif opts[:default].respond_to?(:call)
|
30
|
+
opts[:default].call(self)
|
31
|
+
else
|
32
|
+
opts[:default]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
add_setting :docs_dir, :default => lambda { |config|
|
38
|
+
if defined?(Rails)
|
39
|
+
Rails.root.join("docs")
|
40
|
+
else
|
41
|
+
Pathname.new("docs")
|
42
|
+
end
|
43
|
+
}
|
44
|
+
|
45
|
+
add_setting :format, :default => :html
|
46
|
+
add_setting :template_path, :default => File.expand_path("../../../templates", __FILE__)
|
47
|
+
add_setting :filter, :default => :all
|
48
|
+
add_setting :exclusion_filter, :default => nil
|
49
|
+
add_setting :app, :default => lambda { |config|
|
50
|
+
if defined?(Rails)
|
51
|
+
Rails.application
|
52
|
+
else
|
53
|
+
nil
|
54
|
+
end
|
55
|
+
}
|
56
|
+
|
57
|
+
def settings
|
58
|
+
@settings ||= {}
|
59
|
+
end
|
60
|
+
|
61
|
+
def each(&block)
|
62
|
+
yield self
|
63
|
+
groups.map { |g| g.each &block }
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,204 @@
|
|
1
|
+
require 'rack/test/methods'
|
2
|
+
require 'rack'
|
3
|
+
require 'webmock'
|
4
|
+
|
5
|
+
module RspecApiDocumentation
|
6
|
+
module DSL
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
def self.define_action(method)
|
11
|
+
define_method method do |*args, &block|
|
12
|
+
options = if args.last.is_a?(Hash) then args.pop else {} end
|
13
|
+
options[:method] = method
|
14
|
+
options[:path] = args.first
|
15
|
+
args.push(options)
|
16
|
+
args[0] = "#{method.to_s.upcase} #{args[0]}"
|
17
|
+
context(*args, &block)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
define_action :get
|
22
|
+
define_action :post
|
23
|
+
define_action :put
|
24
|
+
define_action :delete
|
25
|
+
|
26
|
+
def parameter(name, description, options = {})
|
27
|
+
parameters.push(options.merge(:name => name.to_s, :description => description))
|
28
|
+
end
|
29
|
+
|
30
|
+
def required_parameters(*names)
|
31
|
+
names.each do |name|
|
32
|
+
param = parameters.find { |param| param[:name] == name.to_s }
|
33
|
+
raise "Undefined parameters can not be required." unless param
|
34
|
+
param[:required] = true
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def callback(description, &block)
|
39
|
+
self.send(:include, WebMock::API)
|
40
|
+
context(description, &block)
|
41
|
+
end
|
42
|
+
|
43
|
+
def trigger_callback(&block)
|
44
|
+
define_method(:do_callback) do
|
45
|
+
stub_request(:any, callback_url).to_rack(destination)
|
46
|
+
instance_eval &block
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def scope_parameters(scope, keys)
|
51
|
+
return unless metadata[:parameters]
|
52
|
+
|
53
|
+
if keys == :all
|
54
|
+
keys = parameter_keys.map(&:to_s)
|
55
|
+
else
|
56
|
+
keys = keys.map(&:to_s)
|
57
|
+
end
|
58
|
+
|
59
|
+
keys.each do |key|
|
60
|
+
param = parameters.detect { |param| param[:name] == key }
|
61
|
+
param[:scope] = scope if param
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def example_request(description, params = {}, &block)
|
66
|
+
example(description) do
|
67
|
+
do_request(params)
|
68
|
+
instance_eval &block if block_given?
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
def parameters
|
74
|
+
metadata[:parameters] ||= []
|
75
|
+
if superclass_metadata && metadata[:parameters].equal?(superclass_metadata[:parameters])
|
76
|
+
metadata[:parameters] = Marshal.load(Marshal.dump(superclass_metadata[:parameters]))
|
77
|
+
end
|
78
|
+
metadata[:parameters]
|
79
|
+
end
|
80
|
+
|
81
|
+
def parameter_keys
|
82
|
+
parameters.map { |param| param[:name] }
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
module InstanceMethods
|
87
|
+
def client
|
88
|
+
@client ||= TestClient.new(self)
|
89
|
+
end
|
90
|
+
|
91
|
+
def destination
|
92
|
+
@destination ||= TestServer.new(self)
|
93
|
+
end
|
94
|
+
|
95
|
+
def callback_url
|
96
|
+
raise "You must define callback_url"
|
97
|
+
end
|
98
|
+
|
99
|
+
def do_request(extra_params = {})
|
100
|
+
@extra_params = extra_params
|
101
|
+
params_or_body = nil
|
102
|
+
path_or_query = path
|
103
|
+
|
104
|
+
if method == :get && !query_string.blank?
|
105
|
+
path_or_query = path + "?#{query_string}"
|
106
|
+
else
|
107
|
+
params_or_body = respond_to?(:raw_post) ? raw_post : params
|
108
|
+
end
|
109
|
+
|
110
|
+
client.send(method, path_or_query, params_or_body)
|
111
|
+
end
|
112
|
+
|
113
|
+
def query_string
|
114
|
+
query = params.to_a.map do |param|
|
115
|
+
param.map! { |a| CGI.escape(a.to_s) }
|
116
|
+
param.join("=")
|
117
|
+
end
|
118
|
+
query.join("&")
|
119
|
+
end
|
120
|
+
|
121
|
+
def params
|
122
|
+
return unless example.metadata[:parameters]
|
123
|
+
parameters = example.metadata[:parameters].inject({}) do |hash, param|
|
124
|
+
set_param(hash, param)
|
125
|
+
end
|
126
|
+
parameters.merge!(extra_params)
|
127
|
+
parameters
|
128
|
+
end
|
129
|
+
|
130
|
+
def method
|
131
|
+
example.metadata[:method]
|
132
|
+
end
|
133
|
+
|
134
|
+
def in_path?(param)
|
135
|
+
path_params.include?(param)
|
136
|
+
end
|
137
|
+
|
138
|
+
def path_params
|
139
|
+
example.metadata[:path].scan(/:(\w+)/).flatten
|
140
|
+
end
|
141
|
+
|
142
|
+
def path
|
143
|
+
example.metadata[:path].gsub(/:(\w+)/) do |match|
|
144
|
+
if respond_to?($1)
|
145
|
+
send($1)
|
146
|
+
else
|
147
|
+
match
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def app
|
153
|
+
RspecApiDocumentation.configuration.app
|
154
|
+
end
|
155
|
+
|
156
|
+
def explanation(text)
|
157
|
+
example.metadata[:explanation] = text
|
158
|
+
end
|
159
|
+
|
160
|
+
def status
|
161
|
+
last_response.status
|
162
|
+
end
|
163
|
+
|
164
|
+
def response_body
|
165
|
+
last_response.body
|
166
|
+
end
|
167
|
+
|
168
|
+
private
|
169
|
+
def extra_params
|
170
|
+
return {} if @extra_params.nil?
|
171
|
+
@extra_params.inject({}) do |h, (k, v)|
|
172
|
+
h[k.to_s] = v
|
173
|
+
h
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def set_param(hash, param)
|
178
|
+
key = param[:name]
|
179
|
+
return hash if !respond_to?(key) || in_path?(key)
|
180
|
+
|
181
|
+
if param[:scope]
|
182
|
+
hash[param[:scope].to_s] ||= {}
|
183
|
+
hash[param[:scope].to_s][key] = send(key)
|
184
|
+
else
|
185
|
+
hash[key] = send(key)
|
186
|
+
end
|
187
|
+
|
188
|
+
hash
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def self.resource(*args, &block)
|
195
|
+
options = if args.last.is_a?(Hash) then args.pop else {} end
|
196
|
+
options[:api_docs_dsl] = true
|
197
|
+
options[:resource_name] = args.first
|
198
|
+
options[:document] = true
|
199
|
+
args.push(options)
|
200
|
+
describe(*args, &block)
|
201
|
+
end
|
202
|
+
|
203
|
+
RSpec.configuration.include RspecApiDocumentation::DSL, :api_docs_dsl => true
|
204
|
+
RSpec.configuration.include Rack::Test::Methods, :api_docs_dsl => true
|
@@ -0,0 +1,49 @@
|
|
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 method
|
23
|
+
metadata[:method]
|
24
|
+
end
|
25
|
+
|
26
|
+
def should_document?
|
27
|
+
return false if pending? || !metadata[:resource_name] || !metadata[:document]
|
28
|
+
return true if configuration.filter == :all
|
29
|
+
return false if (Array(metadata[:document]) & Array(configuration.exclusion_filter)).length > 0
|
30
|
+
return true if (Array(metadata[:document]) & Array(configuration.filter)).length > 0
|
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 explanation
|
42
|
+
metadata[:explanation] || nil
|
43
|
+
end
|
44
|
+
|
45
|
+
def requests
|
46
|
+
metadata[:requests] || []
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'mustache'
|
2
|
+
|
3
|
+
module RspecApiDocumentation
|
4
|
+
class HtmlWriter
|
5
|
+
attr_accessor :index, :configuration
|
6
|
+
|
7
|
+
def initialize(index, configuration)
|
8
|
+
self.index = index
|
9
|
+
self.configuration = configuration
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.write(index, configuration)
|
13
|
+
writer = new(index, configuration)
|
14
|
+
writer.write
|
15
|
+
end
|
16
|
+
|
17
|
+
def write
|
18
|
+
File.open(configuration.docs_dir.join("index.html"), "w+") do |f|
|
19
|
+
f.write HtmlIndex.new(index, configuration).render
|
20
|
+
end
|
21
|
+
index.examples.each do |example|
|
22
|
+
html_example = HtmlExample.new(example, configuration)
|
23
|
+
FileUtils.mkdir_p(configuration.docs_dir.join(html_example.dirname))
|
24
|
+
File.open(configuration.docs_dir.join(html_example.dirname, html_example.filename), "w+") do |f|
|
25
|
+
f.write html_example.render
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class HtmlIndex < Mustache
|
32
|
+
def initialize(index, configuration)
|
33
|
+
@index = index
|
34
|
+
@configuration = configuration
|
35
|
+
self.template_path = configuration.template_path
|
36
|
+
end
|
37
|
+
|
38
|
+
def sections
|
39
|
+
IndexWriter.sections(examples)
|
40
|
+
end
|
41
|
+
|
42
|
+
def examples
|
43
|
+
@index.examples.map { |example| HtmlExample.new(example, @configuration) }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class HtmlExample < Mustache
|
48
|
+
delegate :method, :to => :@example
|
49
|
+
|
50
|
+
def initialize(example, configuration)
|
51
|
+
@example = example
|
52
|
+
self.template_path = configuration.template_path
|
53
|
+
end
|
54
|
+
|
55
|
+
def method_missing(method, *args, &block)
|
56
|
+
@example.send(method, *args, &block)
|
57
|
+
end
|
58
|
+
|
59
|
+
def respond_to?(method, include_private = false)
|
60
|
+
super || @example.respond_to?(method, include_private)
|
61
|
+
end
|
62
|
+
|
63
|
+
def dirname
|
64
|
+
resource_name.downcase.gsub(/\s+/, '_')
|
65
|
+
end
|
66
|
+
|
67
|
+
def filename
|
68
|
+
basename = description.downcase.gsub(/\s+/, '_').gsub(/[^a-z_]/, '')
|
69
|
+
"#{basename}.html"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module RspecApiDocumentation
|
2
|
+
module IndexWriter
|
3
|
+
def sections(examples)
|
4
|
+
resources = examples.group_by(&:resource_name).inject([]) do |arr, (resource_name, examples)|
|
5
|
+
arr << { :resource_name => resource_name, :examples => examples.sort_by(&:description) }
|
6
|
+
end
|
7
|
+
resources.sort_by { |resource| resource[:resource_name] }
|
8
|
+
end
|
9
|
+
module_function :sections
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module RspecApiDocumentation
|
2
|
+
class JsonWriter
|
3
|
+
attr_accessor :index, :configuration
|
4
|
+
delegate :docs_dir, :to => :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 write
|
17
|
+
File.open(docs_dir.join("index.json"), "w+") do |f|
|
18
|
+
f.write JsonIndex.new(index).to_json
|
19
|
+
end
|
20
|
+
index.examples.each do |example|
|
21
|
+
json_example = JsonExample.new(example)
|
22
|
+
FileUtils.mkdir_p(docs_dir.join(json_example.dirname))
|
23
|
+
File.open(docs_dir.join(json_example.dirname, json_example.filename), "w+") do |f|
|
24
|
+
f.write json_example.to_json
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class JsonIndex
|
31
|
+
def initialize(index)
|
32
|
+
@index = index
|
33
|
+
end
|
34
|
+
|
35
|
+
def sections
|
36
|
+
IndexWriter.sections(examples)
|
37
|
+
end
|
38
|
+
|
39
|
+
def examples
|
40
|
+
@index.examples.map { |example| JsonExample.new(example) }
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_json
|
44
|
+
sections.inject({:resources => []}) do |h, section|
|
45
|
+
h[:resources].push(
|
46
|
+
:name => section[:resource_name],
|
47
|
+
:examples => section[:examples].map { |example|
|
48
|
+
{
|
49
|
+
:description => example.description,
|
50
|
+
:link => "#{example.dirname}/#{example.filename}"
|
51
|
+
}
|
52
|
+
}
|
53
|
+
)
|
54
|
+
h
|
55
|
+
end.to_json
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class JsonExample
|
60
|
+
delegate :method, :to => :@example
|
61
|
+
|
62
|
+
def initialize(example)
|
63
|
+
@example = example
|
64
|
+
end
|
65
|
+
|
66
|
+
def method_missing(method, *args, &block)
|
67
|
+
@example.send(method, *args, &block)
|
68
|
+
end
|
69
|
+
|
70
|
+
def respond_to?(method, include_private = false)
|
71
|
+
super || @example.respond_to?(method, include_private)
|
72
|
+
end
|
73
|
+
|
74
|
+
def dirname
|
75
|
+
resource_name.downcase.gsub(/\s+/, '_')
|
76
|
+
end
|
77
|
+
|
78
|
+
def filename
|
79
|
+
basename = description.downcase.gsub(/\s+/, '_').gsub(/[^a-z_]/, '')
|
80
|
+
"#{basename}.json"
|
81
|
+
end
|
82
|
+
|
83
|
+
def to_json
|
84
|
+
{
|
85
|
+
:resource => resource_name,
|
86
|
+
:description => description,
|
87
|
+
:explanation => explanation,
|
88
|
+
:parameters => respond_to?(:parameters) ? parameters : [],
|
89
|
+
:requests => requests
|
90
|
+
}.to_json
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
module RspecApiDocumentation
|
2
|
+
class TestClient < Struct.new(:session, :options)
|
3
|
+
attr_accessor :user
|
4
|
+
|
5
|
+
delegate :example, :last_response, :last_request, :to => :session
|
6
|
+
delegate :metadata, :to => :example
|
7
|
+
|
8
|
+
def get(*args)
|
9
|
+
process :get, *args
|
10
|
+
end
|
11
|
+
|
12
|
+
def post(*args)
|
13
|
+
process :post, *args
|
14
|
+
end
|
15
|
+
|
16
|
+
def put(*args)
|
17
|
+
process :put, *args
|
18
|
+
end
|
19
|
+
|
20
|
+
def delete(*args)
|
21
|
+
process :delete, *args
|
22
|
+
end
|
23
|
+
|
24
|
+
def sign_in(user)
|
25
|
+
@user = user
|
26
|
+
end
|
27
|
+
|
28
|
+
def last_headers
|
29
|
+
session.last_request.env.select do |k, v|
|
30
|
+
k =~ /^(HTTP_|CONTENT_TYPE)/
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def last_query_string
|
35
|
+
session.last_request.env["QUERY_STRING"]
|
36
|
+
end
|
37
|
+
|
38
|
+
def last_query_hash
|
39
|
+
strings = last_query_string.split("&")
|
40
|
+
arrays = strings.map do |segment|
|
41
|
+
segment.split("=")
|
42
|
+
end
|
43
|
+
Hash[arrays]
|
44
|
+
end
|
45
|
+
|
46
|
+
def headers(method, action, params)
|
47
|
+
if options && options[:headers]
|
48
|
+
options[:headers]
|
49
|
+
else
|
50
|
+
{}
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
def process(method, action, params = {})
|
56
|
+
session.send(method, action, params, headers(method, action, params))
|
57
|
+
|
58
|
+
document_example(method, action, params)
|
59
|
+
end
|
60
|
+
|
61
|
+
def document_example(method, action, params)
|
62
|
+
return unless metadata[:document]
|
63
|
+
|
64
|
+
input = last_request.env["rack.input"]
|
65
|
+
input.rewind
|
66
|
+
request_body = input.read
|
67
|
+
|
68
|
+
request_metadata = {}
|
69
|
+
|
70
|
+
request_metadata[:method] = method.to_s.upcase
|
71
|
+
request_metadata[:route] = action
|
72
|
+
if is_json?(request_body)
|
73
|
+
request_metadata[:request_body] = prettify_json(request_body)
|
74
|
+
else
|
75
|
+
request_metadata[:request_body] = prettify_request_body(request_body)
|
76
|
+
end
|
77
|
+
request_metadata[:request_headers] = format_headers(last_headers)
|
78
|
+
request_metadata[:request_query_parameters] = format_query_hash(last_query_hash)
|
79
|
+
request_metadata[:response_status] = last_response.status
|
80
|
+
request_metadata[:response_status_text] = Rack::Utils::HTTP_STATUS_CODES[last_response.status]
|
81
|
+
request_metadata[:response_body] = prettify_json(last_response.body)
|
82
|
+
request_metadata[:response_headers] = format_headers(last_response.headers)
|
83
|
+
|
84
|
+
metadata[:requests] ||= []
|
85
|
+
metadata[:requests] << request_metadata
|
86
|
+
end
|
87
|
+
|
88
|
+
def format_headers(headers)
|
89
|
+
headers.map do |key, value|
|
90
|
+
# HTTP_ACCEPT_CHARSET => Accept-Charset
|
91
|
+
formatted_key = key.gsub(/^HTTP_/, '').titleize.split.join("-")
|
92
|
+
"#{formatted_key}: #{value}"
|
93
|
+
end.join("\n")
|
94
|
+
end
|
95
|
+
|
96
|
+
def format_query_hash(query_hash)
|
97
|
+
return if query_hash.blank?
|
98
|
+
query_hash.map do |key, value|
|
99
|
+
"#{key}: #{CGI.unescape(value)}"
|
100
|
+
end.join("\n")
|
101
|
+
end
|
102
|
+
|
103
|
+
def prettify_json(json)
|
104
|
+
begin
|
105
|
+
JSON.pretty_generate(JSON.parse(json))
|
106
|
+
rescue
|
107
|
+
nil
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def prettify_request_body(string)
|
112
|
+
return if string.blank?
|
113
|
+
CGI.unescape(string.split("&").join("\n"))
|
114
|
+
end
|
115
|
+
|
116
|
+
def is_json?(string)
|
117
|
+
begin
|
118
|
+
JSON.parse(string)
|
119
|
+
rescue
|
120
|
+
false
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module RspecApiDocumentation
|
2
|
+
class TestServer < Struct.new(:session)
|
3
|
+
delegate :example, :last_request, :last_response, :to => :session
|
4
|
+
delegate :metadata, :to => :example
|
5
|
+
|
6
|
+
def call(env)
|
7
|
+
env["rack.input"].rewind
|
8
|
+
|
9
|
+
request_metadata = {}
|
10
|
+
|
11
|
+
request_metadata[:method] = env["REQUEST_METHOD"]
|
12
|
+
request_metadata[:route] = env["PATH_INFO"]
|
13
|
+
request_metadata[:request_body] = prettify_json(env["rack.input"].read)
|
14
|
+
request_metadata[:request_headers] = headers(env)
|
15
|
+
|
16
|
+
metadata[:requests] ||= []
|
17
|
+
metadata[:requests] << request_metadata
|
18
|
+
|
19
|
+
return [200, {}, [""]]
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def headers(env)
|
25
|
+
env.
|
26
|
+
select do |k, v|
|
27
|
+
k =~ /^(HTTP_|CONTENT_TYPE)/
|
28
|
+
end.
|
29
|
+
map do |key, value|
|
30
|
+
# HTTP_ACCEPT_CHARSET => Accept-Charset
|
31
|
+
formatted_key = key.gsub(/^HTTP_/, '').titleize.split.join("-")
|
32
|
+
"#{formatted_key}: #{value}"
|
33
|
+
end.join("\n")
|
34
|
+
end
|
35
|
+
|
36
|
+
def prettify_json(json)
|
37
|
+
begin
|
38
|
+
JSON.pretty_generate(JSON.parse(json))
|
39
|
+
rescue
|
40
|
+
nil
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/tasks/docs.rake
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
require 'rspec/core/rake_task'
|
2
|
+
|
3
|
+
if Rails.env.test? || Rails.env.development?
|
4
|
+
desc 'Generate API request documentation from API specs'
|
5
|
+
RSpec::Core::RakeTask.new('docs:generate') do |t|
|
6
|
+
t.pattern = 'spec/acceptance/**/*_spec.rb'
|
7
|
+
t.rspec_opts = ["--format RspecApiDocumentation::ApiFormatter"]
|
8
|
+
end
|
9
|
+
end
|
metadata
ADDED
@@ -0,0 +1,151 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rspec_api_documentation
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.3.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Chris Cahoon
|
9
|
+
- Sam Goldman
|
10
|
+
- Eric Oestrich
|
11
|
+
autorequire:
|
12
|
+
bindir: bin
|
13
|
+
cert_chain: []
|
14
|
+
date: 2012-01-25 00:00:00.000000000Z
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: rspec
|
18
|
+
requirement: &70151618292380 !ruby/object:Gem::Requirement
|
19
|
+
none: false
|
20
|
+
requirements:
|
21
|
+
- - ! '>='
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: '0'
|
24
|
+
type: :runtime
|
25
|
+
prerelease: false
|
26
|
+
version_requirements: *70151618292380
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activesupport
|
29
|
+
requirement: &70151618291040 !ruby/object:Gem::Requirement
|
30
|
+
none: false
|
31
|
+
requirements:
|
32
|
+
- - ! '>='
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '0'
|
35
|
+
type: :runtime
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: *70151618291040
|
38
|
+
- !ruby/object:Gem::Dependency
|
39
|
+
name: i18n
|
40
|
+
requirement: &70151618289580 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
type: :runtime
|
47
|
+
prerelease: false
|
48
|
+
version_requirements: *70151618289580
|
49
|
+
- !ruby/object:Gem::Dependency
|
50
|
+
name: rack-test
|
51
|
+
requirement: &70151618288780 !ruby/object:Gem::Requirement
|
52
|
+
none: false
|
53
|
+
requirements:
|
54
|
+
- - ! '>='
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '0'
|
57
|
+
type: :runtime
|
58
|
+
prerelease: false
|
59
|
+
version_requirements: *70151618288780
|
60
|
+
- !ruby/object:Gem::Dependency
|
61
|
+
name: mustache
|
62
|
+
requirement: &70151618287740 !ruby/object:Gem::Requirement
|
63
|
+
none: false
|
64
|
+
requirements:
|
65
|
+
- - ! '>='
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
type: :runtime
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: *70151618287740
|
71
|
+
- !ruby/object:Gem::Dependency
|
72
|
+
name: webmock
|
73
|
+
requirement: &70151618286920 !ruby/object:Gem::Requirement
|
74
|
+
none: false
|
75
|
+
requirements:
|
76
|
+
- - ! '>='
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '0'
|
79
|
+
type: :runtime
|
80
|
+
prerelease: false
|
81
|
+
version_requirements: *70151618286920
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
name: fakefs
|
84
|
+
requirement: &70151618285800 !ruby/object:Gem::Requirement
|
85
|
+
none: false
|
86
|
+
requirements:
|
87
|
+
- - ! '>='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: *70151618285800
|
93
|
+
- !ruby/object:Gem::Dependency
|
94
|
+
name: sinatra
|
95
|
+
requirement: &70151618283740 !ruby/object:Gem::Requirement
|
96
|
+
none: false
|
97
|
+
requirements:
|
98
|
+
- - ! '>='
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: '0'
|
101
|
+
type: :development
|
102
|
+
prerelease: false
|
103
|
+
version_requirements: *70151618283740
|
104
|
+
description: Generate API docs from your test suite
|
105
|
+
email:
|
106
|
+
- chris@smartlogicsolutions.com
|
107
|
+
- sam@smartlogicsolutions.com
|
108
|
+
- eric@smartlogicsolutions.com
|
109
|
+
executables: []
|
110
|
+
extensions: []
|
111
|
+
extra_rdoc_files: []
|
112
|
+
files:
|
113
|
+
- lib/rspec_api_documentation/api_documentation.rb
|
114
|
+
- lib/rspec_api_documentation/api_formatter.rb
|
115
|
+
- lib/rspec_api_documentation/configuration.rb
|
116
|
+
- lib/rspec_api_documentation/dsl.rb
|
117
|
+
- lib/rspec_api_documentation/example.rb
|
118
|
+
- lib/rspec_api_documentation/html_writer.rb
|
119
|
+
- lib/rspec_api_documentation/index.rb
|
120
|
+
- lib/rspec_api_documentation/index_writer.rb
|
121
|
+
- lib/rspec_api_documentation/json_writer.rb
|
122
|
+
- lib/rspec_api_documentation/railtie.rb
|
123
|
+
- lib/rspec_api_documentation/test_client.rb
|
124
|
+
- lib/rspec_api_documentation/test_server.rb
|
125
|
+
- lib/rspec_api_documentation.rb
|
126
|
+
- lib/tasks/docs.rake
|
127
|
+
homepage: http://smartlogicsolutions.com
|
128
|
+
licenses: []
|
129
|
+
post_install_message:
|
130
|
+
rdoc_options: []
|
131
|
+
require_paths:
|
132
|
+
- lib
|
133
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
134
|
+
none: false
|
135
|
+
requirements:
|
136
|
+
- - ! '>='
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
140
|
+
none: false
|
141
|
+
requirements:
|
142
|
+
- - ! '>='
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
version: 1.3.6
|
145
|
+
requirements: []
|
146
|
+
rubyforge_project:
|
147
|
+
rubygems_version: 1.8.10
|
148
|
+
signing_key:
|
149
|
+
specification_version: 3
|
150
|
+
summary: A double black belt for your docs
|
151
|
+
test_files: []
|