api_blueprint 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +100 -0
- data/lib/api_blueprint/collect/controller_hook.rb +92 -0
- data/lib/api_blueprint/collect/merge.rb +334 -0
- data/lib/api_blueprint/collect/preprocessor.rb +175 -0
- data/lib/api_blueprint/collect/renderer.rb +70 -0
- data/lib/api_blueprint/collect/spec_hook.rb +28 -0
- data/lib/api_blueprint/collect/storage.rb +14 -0
- data/lib/api_blueprint/compile/compile.rb +220 -0
- data/lib/api_blueprint/compile/storage.rb +17 -0
- data/lib/api_blueprint/railtie.rb +15 -0
- data/lib/api_blueprint/version.rb +3 -0
- data/lib/api_blueprint.rb +16 -0
- data/lib/tasks/blueprint.rake +140 -0
- metadata +133 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 3c06b5d206b8bf0f3ca2d9540ce47da6200ff8c5
|
4
|
+
data.tar.gz: 1979c9532714e356234d04858b37b437629d674a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ff4835d306e95aa438ef3cfeaed9e777c3600e0beaaa6f0eb3b5569a544060b19c0c4ed4879eac9bc17bf1e47385c87f9618237c6a837cfb2833e5a5b8e8dd32
|
7
|
+
data.tar.gz: 7811e24b5dfdd4912635ca738657e0c888b12520ae4b5a7cd6f616dd2ca075cfed70dd730c36c275d252075cdd157e454c3ae557be7d2ec250a1872fe1c7342b
|
data/README.md
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
# api-blueprint
|
2
|
+
|
3
|
+
Semi-automatic solution for creating Rails app's API documentation. Here's how it works:
|
4
|
+
|
5
|
+
1. You start with method list generated from RSpec request specs. For each method, you get a list of parameters and examples.
|
6
|
+
2. Then, you can extend it in whatever way you need using Markdown syntax. You can organize documentation files into partials.
|
7
|
+
3. Upon any API change, like serializer change that changes responses, you can update automatically generated parts of docs.
|
8
|
+
4. Once done, you can compile your documentation into single, nicely styled HTML file. You can also auto-deploy it via SSH.
|
9
|
+
|
10
|
+
## Installation
|
11
|
+
|
12
|
+
Add to `Gemfile`:
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
gem 'api_blueprint', group: [:development, :test]
|
16
|
+
```
|
17
|
+
|
18
|
+
Then run:
|
19
|
+
|
20
|
+
bundle install
|
21
|
+
|
22
|
+
Add the following inside `RSpec.configure` block in `spec/spec_helper.rb`:
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
config.include ApiBlueprint::Collect::SpecHook
|
26
|
+
```
|
27
|
+
|
28
|
+
## Usage
|
29
|
+
|
30
|
+
**api-blueprint** consists of two modules:
|
31
|
+
|
32
|
+
1. **Collect** module: allows to run RSpec request suite in order to auto-generate API method information.
|
33
|
+
2. **Compile** module: allows to turn whole Markdown documentation into single, ready-to-publish HTML file.
|
34
|
+
|
35
|
+
### Collect
|
36
|
+
|
37
|
+
In order to auto-generate API method information you should invoke:
|
38
|
+
|
39
|
+
rake blueprint:collect
|
40
|
+
|
41
|
+
> By default, all specs inside `spec/requests/api` are run. You can configure that by creating a [Blueprintfile](#configuration) configuration.
|
42
|
+
|
43
|
+
This will generate the `doc/api.md` file with a Markdown documentation ready to compile. If this file already exists, **api-blueprint** will not override it. It will write to `tmp/merge.md` instead so you can merge both existing and generated documentation manually in whatever way you want.
|
44
|
+
|
45
|
+
Of course, it's just a starting point and you should at least fill in some resource, action and parameter descriptions. But that's a story for the **Compile** module.
|
46
|
+
|
47
|
+
#### Regenerate examples
|
48
|
+
|
49
|
+
You get the RSpec-based example listing for every auto-generated API method documentation. There's a chance that
|
50
|
+
|
51
|
+
### Compile
|
52
|
+
|
53
|
+
In order to turn your documentation into ready-to-publish HTML file you should invoke:
|
54
|
+
|
55
|
+
rake blueprint:compile
|
56
|
+
|
57
|
+
This will create the final `doc/api.html`. You can deploy this file to configured SSH target with:
|
58
|
+
|
59
|
+
rake blueprint:deploy
|
60
|
+
|
61
|
+
If you want to preview this file constantly when editing Markdown docs, you can do so with:
|
62
|
+
|
63
|
+
rake blueprint:watch
|
64
|
+
|
65
|
+
> You should add `doc/**/*.html` to your `.gitignore` as there's no need to clutter your project history with compiled HTML that you can easily recreate on demand.
|
66
|
+
|
67
|
+
#### Require another file
|
68
|
+
|
69
|
+
You can split your documentation into separate files and directories in order to organize it better and reuse same fragments in multiple places. You can do that with the following Markdown:
|
70
|
+
|
71
|
+
```md
|
72
|
+
<require:fragments/deprecation_warning.md>
|
73
|
+
```
|
74
|
+
|
75
|
+
## Configuration
|
76
|
+
|
77
|
+
Configuration for **api-blueprint** lives in `Blueprintfile` inside application directory. It's basically a listing of documentations governed by **api-blueprint**, each with a set of options. It looks like this:
|
78
|
+
|
79
|
+
```yaml
|
80
|
+
api:
|
81
|
+
spec: "spec/requests/api/v2"
|
82
|
+
blueprint: "doc/api.md"
|
83
|
+
html: "doc/api.html"
|
84
|
+
deploy: "user@server.com:/home/app/public/api.html"
|
85
|
+
naming:
|
86
|
+
sessions:
|
87
|
+
create: "Sign In"
|
88
|
+
```
|
89
|
+
|
90
|
+
Here's what specific per-documentation options stand for:
|
91
|
+
|
92
|
+
Option | Description
|
93
|
+
-------|------------
|
94
|
+
`spec` | RSpec spec suite directory
|
95
|
+
`blueprint` | Main documentation file (Markdown)
|
96
|
+
`html` | Target HTML file created after compilation
|
97
|
+
`deploy` | SSH address used for documentation deployment
|
98
|
+
`naming` | Dictionary of custom API method names
|
99
|
+
|
100
|
+
First group is always a default one. You can switch any rake task to work on other group by specifying its name with `rake blueprint:collect group=other`.
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module ApiBlueprint::Collect::ControllerHook
|
2
|
+
def self.included(base)
|
3
|
+
return unless ENV['API_BLUEPRINT_DUMP'] == '1'
|
4
|
+
|
5
|
+
base.around_filter :dump_blueprint_around
|
6
|
+
end
|
7
|
+
|
8
|
+
class Parser
|
9
|
+
attr_reader :input
|
10
|
+
|
11
|
+
def initialize(input)
|
12
|
+
@input = input
|
13
|
+
end
|
14
|
+
|
15
|
+
def method
|
16
|
+
input.method.to_s.upcase
|
17
|
+
end
|
18
|
+
|
19
|
+
def params
|
20
|
+
JSON.parse(input.params.reject do |k,_|
|
21
|
+
['action', 'controller'].include?(k)
|
22
|
+
end.to_json)
|
23
|
+
end
|
24
|
+
|
25
|
+
def headers
|
26
|
+
Hash[input.headers.env.select do |k, v|
|
27
|
+
(k.start_with?("HTTP_X_") || k == 'ACCEPT') && v
|
28
|
+
end.map do |k, v|
|
29
|
+
[human_header_key(k), v]
|
30
|
+
end]
|
31
|
+
end
|
32
|
+
|
33
|
+
def body
|
34
|
+
if input.content_type == 'application/json'
|
35
|
+
if input.body != 'null'
|
36
|
+
JSON.parse(input.body)
|
37
|
+
else
|
38
|
+
""
|
39
|
+
end
|
40
|
+
else
|
41
|
+
input.body
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def human_header_key(key)
|
48
|
+
key.sub("HTTP_", '').split("_").map do |x|
|
49
|
+
x.downcase
|
50
|
+
end.join("_")
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def dump_blueprint_around
|
55
|
+
yield
|
56
|
+
ensure
|
57
|
+
dump_blueprint
|
58
|
+
end
|
59
|
+
|
60
|
+
def dump_blueprint
|
61
|
+
file = ApiBlueprint::Collect::Storage.request_dump
|
62
|
+
in_parser = Parser.new(request)
|
63
|
+
out_parser = Parser.new(response)
|
64
|
+
|
65
|
+
data = {
|
66
|
+
'request' => {
|
67
|
+
'path' => request.path,
|
68
|
+
'method' => in_parser.method,
|
69
|
+
'params' => in_parser.params,
|
70
|
+
'headers' => in_parser.headers,
|
71
|
+
'content_type' => request.content_type,
|
72
|
+
'accept' => request.accept
|
73
|
+
},
|
74
|
+
'response' => {
|
75
|
+
'status' => response.status,
|
76
|
+
'content_type' => response.content_type,
|
77
|
+
'body' => out_parser.body
|
78
|
+
},
|
79
|
+
'route' => {
|
80
|
+
'controller' => controller_name,
|
81
|
+
'action' => action_name
|
82
|
+
}
|
83
|
+
}
|
84
|
+
|
85
|
+
spec = ApiBlueprint::Collect::Storage.spec_dump
|
86
|
+
if File.exists?(spec)
|
87
|
+
data['spec'] = YAML::load_file(spec)
|
88
|
+
end
|
89
|
+
|
90
|
+
File.write(file, data.to_yaml)
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,334 @@
|
|
1
|
+
class ApiBlueprint::Collect::Merge
|
2
|
+
attr_reader :target, :renderer, :preprocessor
|
3
|
+
|
4
|
+
def initialize(options)
|
5
|
+
@target = options[:target]
|
6
|
+
@logger = options[:logger]
|
7
|
+
|
8
|
+
@renderer = ApiBlueprint::Collect::Renderer.new
|
9
|
+
@preprocessor = ApiBlueprint::Collect::Preprocessor.new(
|
10
|
+
:naming => options[:naming])
|
11
|
+
end
|
12
|
+
|
13
|
+
def merge
|
14
|
+
log "Merging into '#{@target}'..."
|
15
|
+
|
16
|
+
File.write(target, body_content)
|
17
|
+
end
|
18
|
+
|
19
|
+
def update_examples
|
20
|
+
log "Updating examples in '#{@target}'..."
|
21
|
+
log ''
|
22
|
+
|
23
|
+
library.each do |resource, actions|
|
24
|
+
actions.each do |action, info|
|
25
|
+
log "#{resource}: #{action}\n\n"
|
26
|
+
|
27
|
+
info[:requests].each do |request|
|
28
|
+
insert = find_insertion_point(resource, action, request[:title])
|
29
|
+
|
30
|
+
if insert && insert[1] == nil
|
31
|
+
insert_example_block(insert, request)
|
32
|
+
elsif insert
|
33
|
+
replace_example_block(insert, request)
|
34
|
+
else
|
35
|
+
log "no insertion point: #{request_title request}", :error
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
log ''
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def clear_examples
|
45
|
+
log "Clearing examples in '#{@target}'...\n\n"
|
46
|
+
|
47
|
+
library.each do |resource, actions|
|
48
|
+
actions.each do |action, info|
|
49
|
+
clear = find_clear_point(resource, action)
|
50
|
+
|
51
|
+
log "#{resource}: #{action}\n\n"
|
52
|
+
|
53
|
+
if clear
|
54
|
+
clear_example_block(clear)
|
55
|
+
else
|
56
|
+
log "no clear point", :error
|
57
|
+
end
|
58
|
+
|
59
|
+
log ''
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
# library loading
|
67
|
+
|
68
|
+
def library
|
69
|
+
@library ||= begin
|
70
|
+
resources = {}
|
71
|
+
|
72
|
+
requests.each do |request|
|
73
|
+
resource = (resources[preprocessor.resource_name(request)] ||= {})
|
74
|
+
action = (resource[preprocessor.action_name(request)] ||= {
|
75
|
+
:requests => []
|
76
|
+
})
|
77
|
+
|
78
|
+
action[:requests] << request
|
79
|
+
end
|
80
|
+
|
81
|
+
resources.each do |resource, actions|
|
82
|
+
actions.each do |action, info|
|
83
|
+
preprocessor.preprocess(info)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
resources = resources.sort
|
88
|
+
|
89
|
+
resources
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def requests
|
94
|
+
ApiBlueprint::Collect::Storage.request_dumps.collect do |file|
|
95
|
+
YAML::load_file(file)
|
96
|
+
end.uniq
|
97
|
+
end
|
98
|
+
|
99
|
+
# content assembly
|
100
|
+
|
101
|
+
def body_content
|
102
|
+
library.collect do |resource, actions|
|
103
|
+
text = renderer.resource_header(resource)
|
104
|
+
|
105
|
+
text += actions.collect do |action, info|
|
106
|
+
text = renderer.action_header(action)
|
107
|
+
|
108
|
+
text += renderer.description_header
|
109
|
+
text += renderer.signature(info[:path], info[:method])
|
110
|
+
text += renderer.parameter_table(info[:params])
|
111
|
+
text += examples(info)
|
112
|
+
end.join
|
113
|
+
end.join
|
114
|
+
end
|
115
|
+
|
116
|
+
def examples(info)
|
117
|
+
text = renderer.examples_header
|
118
|
+
|
119
|
+
text += info[:requests].collect do |request|
|
120
|
+
example(request)
|
121
|
+
end.join
|
122
|
+
end
|
123
|
+
|
124
|
+
def example(request)
|
125
|
+
text = renderer.example_header(request[:title])
|
126
|
+
|
127
|
+
if request[:request_headers].present?
|
128
|
+
text += renderer.example_subheader(:request_headers) +
|
129
|
+
renderer.code_block(request[:request_headers])
|
130
|
+
end
|
131
|
+
|
132
|
+
if request[:params]
|
133
|
+
text += renderer.example_subheader(:request_params) +
|
134
|
+
renderer.code_block(request[:params])
|
135
|
+
end
|
136
|
+
|
137
|
+
text += renderer.example_subheader(:response_headers) +
|
138
|
+
renderer.code_block(request[:response_headers]) +
|
139
|
+
renderer.example_subheader(:response_body) +
|
140
|
+
renderer.code_block(request[:body])
|
141
|
+
end
|
142
|
+
|
143
|
+
# example updating
|
144
|
+
|
145
|
+
def find_insertion_point(resource, action, example)
|
146
|
+
@partial_map = build_partial_map(@target)
|
147
|
+
|
148
|
+
resource = find_chapter(1, "Resource: #{resource}")
|
149
|
+
return nil unless resource
|
150
|
+
|
151
|
+
action = find_chapter(2, "Action: #{action}", resource)
|
152
|
+
return nil unless action
|
153
|
+
|
154
|
+
examples = find_chapter(3, "Examples:", action)
|
155
|
+
return nil unless examples
|
156
|
+
|
157
|
+
example = find_chapter(4, "Example: #{example}", examples)
|
158
|
+
|
159
|
+
if example
|
160
|
+
example
|
161
|
+
else
|
162
|
+
[examples[1], nil]
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def find_clear_point(resource, action)
|
167
|
+
@partial_map = build_partial_map(@target)
|
168
|
+
|
169
|
+
resource = find_chapter(1, "Resource: #{resource}")
|
170
|
+
return nil unless resource
|
171
|
+
|
172
|
+
action = find_chapter(2, "Action: #{action}", resource)
|
173
|
+
return nil unless action
|
174
|
+
|
175
|
+
examples = find_chapter(3, "Examples:", action)
|
176
|
+
return nil unless examples
|
177
|
+
|
178
|
+
[examples[0] + 2, examples[1]]
|
179
|
+
end
|
180
|
+
|
181
|
+
def find_chapter(level, title, constraints = nil)
|
182
|
+
from = constraints ? constraints[0] : 0
|
183
|
+
to = constraints ? constraints[1] : @partial_map.length - 1
|
184
|
+
|
185
|
+
point = []
|
186
|
+
|
187
|
+
@partial_map[from..to].each_with_index do |line, index|
|
188
|
+
map_index = from + index
|
189
|
+
|
190
|
+
if line[0].strip == (('#' * level) + ' ' + title)
|
191
|
+
point[0] = map_index
|
192
|
+
elsif point[0].present? && line[0].match(/^\#{1,#{level}}\s/)
|
193
|
+
point[1] = map_index - 1
|
194
|
+
|
195
|
+
break
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
if point[0]
|
200
|
+
point[1] ||= to
|
201
|
+
|
202
|
+
point
|
203
|
+
else
|
204
|
+
nil
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
def insert_example_block(insert, request)
|
209
|
+
insert_map = @partial_map[insert[0]]
|
210
|
+
|
211
|
+
file = insert_map[1]
|
212
|
+
at = insert_map[2]
|
213
|
+
|
214
|
+
log "#{file} @ #{at}: #{request_title request}", :add
|
215
|
+
|
216
|
+
e = example(request).split("\n").map { |l| l + "\n" }
|
217
|
+
|
218
|
+
lines = File.readlines(file)
|
219
|
+
if lines[at].present?
|
220
|
+
lines.insert(at + 1, "\n")
|
221
|
+
at += 1
|
222
|
+
end
|
223
|
+
if lines[at + 1].present?
|
224
|
+
lines.insert(at + 1, "\n")
|
225
|
+
end
|
226
|
+
lines = lines[0..at] + e + lines[at + 1..-1]
|
227
|
+
|
228
|
+
File.write(file, lines.join(''))
|
229
|
+
end
|
230
|
+
|
231
|
+
def replace_example_block(insert, request)
|
232
|
+
from_map = @partial_map[insert[0]]
|
233
|
+
to_map = @partial_map[insert[1]]
|
234
|
+
|
235
|
+
unless from_map[1] == to_map[1]
|
236
|
+
return log("[!] multi-file range: #{request_title request}")
|
237
|
+
end
|
238
|
+
|
239
|
+
file = from_map[1]
|
240
|
+
from = from_map[2] - 1
|
241
|
+
to = to_map[2] + 1
|
242
|
+
|
243
|
+
log "#{file} @ #{from}-#{to}: #{request_title request}", :modify
|
244
|
+
|
245
|
+
e = example(request).split("\n").map { |l| l + "\n" }
|
246
|
+
|
247
|
+
lines = File.readlines(file)
|
248
|
+
if lines[to].present?
|
249
|
+
lines.insert(to, "\n")
|
250
|
+
end
|
251
|
+
lines = lines[0..from] + e + lines[to..-1]
|
252
|
+
|
253
|
+
File.write(file, lines.join(''))
|
254
|
+
end
|
255
|
+
|
256
|
+
def clear_example_block(clear)
|
257
|
+
from_map = @partial_map[clear[0]]
|
258
|
+
to_map = @partial_map[clear[1]]
|
259
|
+
|
260
|
+
unless from_map and to_map
|
261
|
+
return log('wrong mapping', :error)
|
262
|
+
end
|
263
|
+
|
264
|
+
unless from_map[1] == to_map[1]
|
265
|
+
return log("multi-file range", :error)
|
266
|
+
end
|
267
|
+
|
268
|
+
file = from_map[1]
|
269
|
+
from = from_map[2] - 1
|
270
|
+
to = to_map[2] + 1
|
271
|
+
|
272
|
+
lines = File.readlines(file)
|
273
|
+
if lines[from].blank? && lines[to].blank?
|
274
|
+
to += 1
|
275
|
+
end
|
276
|
+
|
277
|
+
log "#{file} @ #{from}-#{to}", :remove
|
278
|
+
|
279
|
+
final_lines = lines[0..from]
|
280
|
+
final_lines += lines[to..-1] if lines[to..-1]
|
281
|
+
|
282
|
+
File.write(file, final_lines.join(''))
|
283
|
+
end
|
284
|
+
|
285
|
+
def build_partial_map(file)
|
286
|
+
lines = File.readlines(file)
|
287
|
+
map = []
|
288
|
+
|
289
|
+
lines.each_with_index do |line, index|
|
290
|
+
if line.start_with?("<require:")
|
291
|
+
filename = line.split('<require:')[1].split('>')[0] + '.md'
|
292
|
+
path = file.split('/')[0..-2].join('/')
|
293
|
+
|
294
|
+
map += build_partial_map(path + '/' + filename)
|
295
|
+
else
|
296
|
+
map << [line, file, index]
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
map
|
301
|
+
end
|
302
|
+
|
303
|
+
def request_title(request)
|
304
|
+
limit = 60
|
305
|
+
title = request[:title].strip
|
306
|
+
|
307
|
+
if title.length < limit + 2
|
308
|
+
title
|
309
|
+
else
|
310
|
+
title[0..limit] + '(...)'
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
# other
|
315
|
+
|
316
|
+
def log(message, kind = nil)
|
317
|
+
if @logger.to_s == 'stdout'
|
318
|
+
message = case kind.to_s
|
319
|
+
when 'error'
|
320
|
+
(' ! ' + message).colorize(:red)
|
321
|
+
when 'add'
|
322
|
+
(' + ' + message).colorize(:green)
|
323
|
+
when 'modify'
|
324
|
+
(' * ' + message).colorize(:magenta)
|
325
|
+
when 'remove'
|
326
|
+
(' - ' + message).colorize(:cyan)
|
327
|
+
else
|
328
|
+
message
|
329
|
+
end
|
330
|
+
|
331
|
+
puts message
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
class ApiBlueprint::Collect::Preprocessor
|
2
|
+
attr_reader :naming
|
3
|
+
|
4
|
+
def initialize(options = {})
|
5
|
+
@naming = options[:naming]
|
6
|
+
end
|
7
|
+
|
8
|
+
def resource_name(request)
|
9
|
+
request['route']['controller'].singularize.split('_').map do |word|
|
10
|
+
word.camelize
|
11
|
+
end.join(' ').pluralize
|
12
|
+
end
|
13
|
+
|
14
|
+
def action_name(request)
|
15
|
+
model = resource_name(request)
|
16
|
+
prefix = %w{a e i o u}.include?(model[0].downcase) ? 'an' : 'a'
|
17
|
+
|
18
|
+
if (n = naming) && (n = n[model.underscore]) && (n = n[request['route']['action']])
|
19
|
+
return n
|
20
|
+
end
|
21
|
+
|
22
|
+
case request['route']['action']
|
23
|
+
when 'index'
|
24
|
+
"List all #{model.pluralize}"
|
25
|
+
when 'show'
|
26
|
+
"Retrieve single #{model.singularize}"
|
27
|
+
when 'update'
|
28
|
+
"Update an existing #{model.singularize}"
|
29
|
+
when 'destroy'
|
30
|
+
"Remove an existing #{model.singularize}"
|
31
|
+
else
|
32
|
+
request['route']['action'].humanize + " #{prefix} #{model.singularize}"
|
33
|
+
end
|
34
|
+
|
35
|
+
# request['route']['action'].humanize
|
36
|
+
end
|
37
|
+
|
38
|
+
def preprocess(info)
|
39
|
+
any_request = info[:requests].first
|
40
|
+
|
41
|
+
info[:path] = any_request['request']['path'].sub(/\d+$/, '{id}')
|
42
|
+
info[:method] = any_request['request']['method']
|
43
|
+
info[:params] = collect_request_params(info[:requests])
|
44
|
+
|
45
|
+
info[:requests].each do |request|
|
46
|
+
preprocess_request(request)
|
47
|
+
end
|
48
|
+
|
49
|
+
unique_requests = []
|
50
|
+
info[:requests].each do |request|
|
51
|
+
unique_requests.reject! do |existing_request|
|
52
|
+
existing_request[:title] == request[:title]
|
53
|
+
end
|
54
|
+
|
55
|
+
unique_requests.push(request)
|
56
|
+
end
|
57
|
+
info[:requests] = unique_requests
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def collect_request_params(requests)
|
63
|
+
merged_params = {}
|
64
|
+
requests.sort_by do |request|
|
65
|
+
-request['response']['status']
|
66
|
+
end.each do |request|
|
67
|
+
merged_params.deep_merge!(request['request']['params'])
|
68
|
+
end
|
69
|
+
|
70
|
+
merged_params = Hash[merged_params.select { |k,v| ! v.is_a?(Hash) || v.any? }]
|
71
|
+
|
72
|
+
collect_merged_params(merged_params)
|
73
|
+
end
|
74
|
+
|
75
|
+
def collect_merged_params(merged_params)
|
76
|
+
params = {}
|
77
|
+
merged_params.each do |param, value|
|
78
|
+
if value.is_a?(Hash)
|
79
|
+
if value['original_filename'].present?
|
80
|
+
params[param] = {
|
81
|
+
:type => 'file',
|
82
|
+
:example => value['original_filename']
|
83
|
+
}
|
84
|
+
else
|
85
|
+
params[param] = {
|
86
|
+
:type => 'nested',
|
87
|
+
:params => collect_merged_params(value)
|
88
|
+
}
|
89
|
+
end
|
90
|
+
elsif value.is_a?(Array)
|
91
|
+
items = value.collect { |i| collect_merged_params(i) }
|
92
|
+
|
93
|
+
|
94
|
+
params[param] = {
|
95
|
+
:type => 'array',
|
96
|
+
:params => items.inject(&:merge)
|
97
|
+
}
|
98
|
+
else
|
99
|
+
if value == true || value == false
|
100
|
+
type = 'boolean'
|
101
|
+
value = value ? 'true' : 'false'
|
102
|
+
elsif value.to_i.to_s == value
|
103
|
+
type = 'integer'
|
104
|
+
elsif value.to_f.to_s == value
|
105
|
+
type = 'decimal'
|
106
|
+
value = value.to_f.round(6).to_s
|
107
|
+
else
|
108
|
+
type = 'string'
|
109
|
+
end
|
110
|
+
|
111
|
+
params[param] = {
|
112
|
+
:type => type,
|
113
|
+
:example => value
|
114
|
+
}
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
params
|
119
|
+
end
|
120
|
+
|
121
|
+
def clear_files(params)
|
122
|
+
p = {}
|
123
|
+
|
124
|
+
params.each do |key, value|
|
125
|
+
if value.is_a?(Hash)
|
126
|
+
if value['original_filename'].present?
|
127
|
+
p[key] = "file <#{value['original_filename']}>"
|
128
|
+
else
|
129
|
+
p[key] = clear_files(value)
|
130
|
+
end
|
131
|
+
else
|
132
|
+
p[key] = value
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
p
|
137
|
+
end
|
138
|
+
|
139
|
+
def preprocess_request(request)
|
140
|
+
if request['request']['params'].present?
|
141
|
+
params = Hash[request['request']['params'].select { |k, v| ! v.is_a?(Hash) || v.any?}]
|
142
|
+
params = clear_files(params)
|
143
|
+
request[:params] = JSON.pretty_generate(params) if params.any?
|
144
|
+
end
|
145
|
+
|
146
|
+
if request['response']['body'].is_a?(Hash)
|
147
|
+
request[:body] = JSON.pretty_generate(request['response']['body'])
|
148
|
+
else
|
149
|
+
request[:body] = request['response']['body']
|
150
|
+
end
|
151
|
+
|
152
|
+
request[:request_headers] = preprocess_headers({
|
153
|
+
# 'Accept' => request['request']['accept'],
|
154
|
+
'Content-Type' => request['request']['content_type']
|
155
|
+
}.merge(request['request']['headers']).select { |_, v| v.present? })
|
156
|
+
|
157
|
+
request[:response_headers] = preprocess_headers({
|
158
|
+
'Status' => request['response']['status'],
|
159
|
+
'Content-Type' => request['response']['content_type']
|
160
|
+
})
|
161
|
+
|
162
|
+
request[:title] = request['spec']['title_parts'][1..-1].join(' ')
|
163
|
+
request[:title] = request[:title][0].upcase + request[:title][1..-1]
|
164
|
+
end
|
165
|
+
|
166
|
+
def preprocess_headers(headers)
|
167
|
+
header_key_length = headers.collect do |key, _|
|
168
|
+
key.length
|
169
|
+
end.max
|
170
|
+
|
171
|
+
headers.collect do |key, value|
|
172
|
+
"#{key.split("_").map(&:camelize).join("-")}:#{' ' * (header_key_length - key.length)} #{value}"
|
173
|
+
end.join("\n")
|
174
|
+
end
|
175
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
class ApiBlueprint::Collect::Renderer
|
2
|
+
def parameter_table(params, level = 0)
|
3
|
+
text = ''
|
4
|
+
|
5
|
+
if level == 0
|
6
|
+
text += "#### Parameters:\n\n"
|
7
|
+
text += "Name | Type | Description\n"
|
8
|
+
text += "-----|------|---------|------------\n"
|
9
|
+
end
|
10
|
+
|
11
|
+
params.each do |name, info|
|
12
|
+
comment = ''
|
13
|
+
comment = "Params for each #{name.singularize}:" if info[:type] == 'array'
|
14
|
+
|
15
|
+
text += "#{'[]' * level} #{name} | *#{info[:type]}*#{info[:example].present? ? " `Example: #{info[:example]}`" : ''} | #{comment}\n"
|
16
|
+
|
17
|
+
if info[:type] == 'nested' || info[:type] == 'array'
|
18
|
+
text += parameter_table(info[:params], level + 1)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
text += "\n" if level == 0
|
22
|
+
|
23
|
+
# text += "#### Parameters:\n\n" if level == 0
|
24
|
+
# text += params.collect do |name, info|
|
25
|
+
# if info[:type] == 'nested'
|
26
|
+
# "#{' ' * (level * 2)}- **#{name}**\n" +
|
27
|
+
# parameter_table(info[:params], level + 1)
|
28
|
+
# else
|
29
|
+
# "#{' ' * (level * 2)}- **#{name}** (#{info[:type]}, `#{info[:example]}`)"
|
30
|
+
# end
|
31
|
+
# end.join("\n")
|
32
|
+
# text += "\n\n" if level == 0
|
33
|
+
|
34
|
+
text
|
35
|
+
end
|
36
|
+
|
37
|
+
def resource_header(content)
|
38
|
+
"# Resource: #{content}\n\n"
|
39
|
+
end
|
40
|
+
|
41
|
+
def action_header(content)
|
42
|
+
"## Action: #{content}\n\n"
|
43
|
+
end
|
44
|
+
|
45
|
+
def description_header
|
46
|
+
"### Description:\n\n"
|
47
|
+
end
|
48
|
+
|
49
|
+
def signature(url, method)
|
50
|
+
"#### Signature:\n\n**#{method}** `#{url}`\n\n"
|
51
|
+
end
|
52
|
+
|
53
|
+
def examples_header
|
54
|
+
"### Examples:\n\n"
|
55
|
+
end
|
56
|
+
|
57
|
+
def example_header(content)
|
58
|
+
"#### Example: #{content}\n\n"
|
59
|
+
end
|
60
|
+
|
61
|
+
def example_subheader(content)
|
62
|
+
content = content.to_s.humanize + ':' if content.is_a?(Symbol)
|
63
|
+
|
64
|
+
"##### #{content}\n\n"
|
65
|
+
end
|
66
|
+
|
67
|
+
def code_block(content)
|
68
|
+
content.split("\n").collect { |line| " " * 4 + line }.join("\n") + "\n\n"
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module ApiBlueprint::Collect::SpecHook
|
2
|
+
def self.included(base)
|
3
|
+
return unless ENV['API_BLUEPRINT_DUMP'] == '1'
|
4
|
+
|
5
|
+
base.before(:each) do |example|
|
6
|
+
data = {
|
7
|
+
'title_parts' => example_description_parts(example)
|
8
|
+
}
|
9
|
+
|
10
|
+
File.write(ApiBlueprint::Collect::Storage.spec_dump, data.to_yaml)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def example_description_parts(example)
|
17
|
+
parts = []
|
18
|
+
parts << example.metadata[:description_args].join(' ')
|
19
|
+
at = example.metadata[:example_group]
|
20
|
+
|
21
|
+
while at && at[:description_args]
|
22
|
+
parts << at[:description_args].join(' ')
|
23
|
+
at = at[:parent_example_group]
|
24
|
+
end
|
25
|
+
|
26
|
+
parts.reverse!
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module ApiBlueprint::Collect::Storage
|
2
|
+
def self.request_dumps
|
3
|
+
Dir[Rails.root.join('tmp', 'api_blueprint_request_*.yml').to_s]
|
4
|
+
end
|
5
|
+
|
6
|
+
def self.spec_dump
|
7
|
+
Rails.root.join('tmp', 'api_blueprint_spec.yml')
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.request_dump
|
11
|
+
Rails.root.join('tmp',
|
12
|
+
"api_blueprint_request_#{(Time.now.to_f * 1000).to_i}_#{sprintf("%09d", rand(1e9))}.yml")
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,220 @@
|
|
1
|
+
class ApiBlueprint::Compile::Compile
|
2
|
+
attr_reader :source, :target, :partials
|
3
|
+
|
4
|
+
def initialize(options)
|
5
|
+
@source = options[:source]
|
6
|
+
@target = options[:target]
|
7
|
+
@logger = options[:logger]
|
8
|
+
|
9
|
+
@partials = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def compile
|
13
|
+
layout = load_layout
|
14
|
+
insert_content(layout)
|
15
|
+
insert_stylesheets(layout)
|
16
|
+
|
17
|
+
layout_html = layout.to_html
|
18
|
+
insert_javascripts(layout_html)
|
19
|
+
|
20
|
+
layout_doc = load_document(layout_html)
|
21
|
+
insert_title(layout_doc)
|
22
|
+
insert_host(layout_doc)
|
23
|
+
insert_copyright(layout_doc)
|
24
|
+
|
25
|
+
write_html(layout_doc.to_html)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def load_layout
|
31
|
+
log "Rendering '#{@source}' within the blueprint layout..."
|
32
|
+
|
33
|
+
Nokogiri::HTML(File.read(ApiBlueprint::Compile::Storage.index_html))
|
34
|
+
end
|
35
|
+
|
36
|
+
def load_document(html)
|
37
|
+
Nokogiri::HTML(html)
|
38
|
+
end
|
39
|
+
|
40
|
+
def write_html(layout_html)
|
41
|
+
return unless @target
|
42
|
+
|
43
|
+
File.write(@target, layout_html)
|
44
|
+
|
45
|
+
log "Wrote #{@title_text || "document"} into '#{@target}'"
|
46
|
+
end
|
47
|
+
|
48
|
+
def insert_content(layout)
|
49
|
+
container = layout.at_css("#blueprint-document")
|
50
|
+
|
51
|
+
if container
|
52
|
+
content = render_markdown(@source)
|
53
|
+
content = prepend_toc(content)
|
54
|
+
|
55
|
+
container.add_child(content)
|
56
|
+
end
|
57
|
+
|
58
|
+
log " - compiled #{(container/'h1').length} chapter(s)"
|
59
|
+
end
|
60
|
+
|
61
|
+
def insert_stylesheets(layout)
|
62
|
+
style = layout.at_css("#blueprint-style")
|
63
|
+
|
64
|
+
if style
|
65
|
+
text = ApiBlueprint::Compile::Storage.stylesheets.collect do |file|
|
66
|
+
Sass::Engine.new(File.read(file), :syntax => :scss, :style => :compressed).render
|
67
|
+
end.join("\n\n")
|
68
|
+
|
69
|
+
style.add_child(text)
|
70
|
+
|
71
|
+
log " - compiled #{ApiBlueprint::Compile::Storage.stylesheets.count} stylesheet(s)"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def insert_javascripts(layout_html)
|
76
|
+
return unless layout_html.include?('<script id="blueprint-script"></script>');
|
77
|
+
|
78
|
+
text = ApiBlueprint::Compile::Storage.javascripts.collect do |file|
|
79
|
+
Uglifier.compile(File.read(file))
|
80
|
+
end.join("\n\n")
|
81
|
+
|
82
|
+
layout_html.sub!('<script id="blueprint-script"></script>',
|
83
|
+
'<script id="blueprint-script">' + text + '</script>')
|
84
|
+
|
85
|
+
log " - compiled #{ApiBlueprint::Compile::Storage.stylesheets.count} javascript(s)"
|
86
|
+
end
|
87
|
+
|
88
|
+
def insert_title(doc)
|
89
|
+
title_node = doc.at('p:contains("Title: ")')
|
90
|
+
|
91
|
+
if title_node
|
92
|
+
@title_text = title_node.text.strip.sub("Title: ", '')
|
93
|
+
title_tag = doc.at('title')
|
94
|
+
|
95
|
+
title_node['id'] = 'title'
|
96
|
+
title_node.content = @title_text
|
97
|
+
|
98
|
+
if title_tag
|
99
|
+
title_tag.content = @title_text
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def insert_host(doc)
|
105
|
+
host_node = doc.at('p:contains("Host: ")')
|
106
|
+
|
107
|
+
if host_node
|
108
|
+
@host_text = host_node.text.strip.sub("Host: ", '')
|
109
|
+
|
110
|
+
host_node['id'] = 'host'
|
111
|
+
host_node.content = @host_text
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def insert_copyright(doc)
|
116
|
+
copyright_node = doc.at('p:contains("Copyright: ")')
|
117
|
+
|
118
|
+
if copyright_node
|
119
|
+
copyright_text = copyright_node.text.strip.sub("Copyright: ", '')
|
120
|
+
copyright_text = "© #{Date.today.year} #{copyright_text}"
|
121
|
+
copyright_tag = doc.at('.copyright')
|
122
|
+
|
123
|
+
copyright_node['id'] = 'copyright'
|
124
|
+
copyright_node.content = copyright_text
|
125
|
+
|
126
|
+
if copyright_tag
|
127
|
+
copyright_tag.content = copyright_text
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def render_markdown(input_file)
|
133
|
+
markdown = Redcarpet::Markdown.new(Redcarpet::Render::HTML, :tables => true, :no_intra_emphasis => true)
|
134
|
+
content = markdown.render(File.read(input_file))
|
135
|
+
doc = Nokogiri::HTML(content)
|
136
|
+
requires = (doc/'a:contains("require:")')
|
137
|
+
|
138
|
+
requires.each do |require_link|
|
139
|
+
file = require_link['href'].split(':').last
|
140
|
+
file = input_file.split('/')[0..-2].join('/') + '/' + file + '.md'
|
141
|
+
file = render_markdown(file)
|
142
|
+
|
143
|
+
require_link.after(file)
|
144
|
+
require_link.remove
|
145
|
+
end
|
146
|
+
|
147
|
+
partials << input_file unless partials.include?(input_file)
|
148
|
+
|
149
|
+
doc.to_html
|
150
|
+
end
|
151
|
+
|
152
|
+
def prepend_toc(doc)
|
153
|
+
doc = Nokogiri::HTML(doc)
|
154
|
+
headers = []
|
155
|
+
|
156
|
+
(doc/"h1").each_with_index do |header, header_index|
|
157
|
+
subheaders = []
|
158
|
+
header['id'] = header.text.sub('Resource: ', '').parameterize
|
159
|
+
headers << { :title => header.text.sub('Resource: ', ''), :id => header['id'], :subheaders => subheaders }
|
160
|
+
|
161
|
+
next_all(header, :where => "h2", :until => 'h1').each_with_index do |action_header, action_index|
|
162
|
+
action_header['id'] = header['id'] + "-" + action_header.text.sub('Action: ', '').parameterize
|
163
|
+
subheaders << { :title => action_header.text.sub('Action: ', ''), :id => action_header['id'] }
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
toc = "<h1>Table Of Contents</h1>"
|
168
|
+
toc += "<ul>"
|
169
|
+
|
170
|
+
headers.collect do |header|
|
171
|
+
toc += "<li>"
|
172
|
+
toc += "<a href='##{header[:id]}'>#{header[:title]}</a>"
|
173
|
+
|
174
|
+
if header[:subheaders].any?
|
175
|
+
toc += "<ul>"
|
176
|
+
header[:subheaders].each do |subheader|
|
177
|
+
toc += "<li><a href='##{subheader[:id]}'>#{subheader[:title]}</a></li>"
|
178
|
+
end
|
179
|
+
toc += "</ul>"
|
180
|
+
end
|
181
|
+
|
182
|
+
toc += '</li>'
|
183
|
+
end
|
184
|
+
|
185
|
+
toc += "</ul>"
|
186
|
+
|
187
|
+
doc.at('h1').before(toc)
|
188
|
+
|
189
|
+
doc.to_html
|
190
|
+
end
|
191
|
+
|
192
|
+
def next_all(element, options = {})
|
193
|
+
doc = element.document
|
194
|
+
|
195
|
+
selector = options[:where]
|
196
|
+
rejector = options[:until]
|
197
|
+
results = Nokogiri::XML::NodeSet.new(doc)
|
198
|
+
element = element.next_element
|
199
|
+
|
200
|
+
while element
|
201
|
+
set = Nokogiri::XML::NodeSet.new(doc, [element])
|
202
|
+
|
203
|
+
if rejector && (set/rejector).any?
|
204
|
+
break
|
205
|
+
elsif ! selector || (set/selector).any?
|
206
|
+
results << element
|
207
|
+
end
|
208
|
+
|
209
|
+
element = element.next_element
|
210
|
+
end
|
211
|
+
|
212
|
+
return results
|
213
|
+
end
|
214
|
+
|
215
|
+
def log(message)
|
216
|
+
if @logger.to_s == 'stdout'
|
217
|
+
puts message
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module ApiBlueprint::Compile::Storage
|
2
|
+
def self.root_join(*parts)
|
3
|
+
File.join(File.dirname(__FILE__), *parts)
|
4
|
+
end
|
5
|
+
|
6
|
+
def self.index_html
|
7
|
+
root_join('assets', 'index.html')
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.javascripts
|
11
|
+
Dir[root_join('assets', '*.js').to_s]
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.stylesheets
|
15
|
+
Dir[root_join('assets', '*.scss').to_s]
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module ApiBlueprint
|
2
|
+
class Railtie < Rails::Railtie
|
3
|
+
railtie_name :api_blueprint
|
4
|
+
|
5
|
+
initializer "api_blueprint.action_controller" do
|
6
|
+
ActiveSupport.on_load(:action_controller) do
|
7
|
+
include ApiBlueprint::Collect::ControllerHook
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
rake_tasks do
|
12
|
+
load 'tasks/blueprint.rake'
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module ApiBlueprint
|
2
|
+
Collect = Module.new
|
3
|
+
Compile = Module.new
|
4
|
+
end
|
5
|
+
|
6
|
+
require 'redcarpet'
|
7
|
+
require 'api_blueprint/collect/controller_hook'
|
8
|
+
require 'api_blueprint/collect/merge'
|
9
|
+
require 'api_blueprint/collect/preprocessor'
|
10
|
+
require 'api_blueprint/collect/renderer'
|
11
|
+
require 'api_blueprint/collect/spec_hook'
|
12
|
+
require 'api_blueprint/collect/storage'
|
13
|
+
require 'api_blueprint/compile/compile'
|
14
|
+
require 'api_blueprint/compile/storage'
|
15
|
+
require 'api_blueprint/railtie'
|
16
|
+
require 'api_blueprint/version'
|
@@ -0,0 +1,140 @@
|
|
1
|
+
def blueprintfile(opts = {})
|
2
|
+
file = Rails.root.join("Blueprintfile")
|
3
|
+
|
4
|
+
if File.exists?(file)
|
5
|
+
file = YAML.load_file(file)
|
6
|
+
|
7
|
+
if ENV['group']
|
8
|
+
hash = file[ENV['group']] || {}
|
9
|
+
else
|
10
|
+
hash = file.any? ? file.first[1] : {}
|
11
|
+
end
|
12
|
+
else
|
13
|
+
hash = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
if opts[:write_blueprint] != false && hash['blueprint'].present? && File.exists?(hash['blueprint'])
|
17
|
+
hash.delete('blueprint')
|
18
|
+
end
|
19
|
+
|
20
|
+
['spec', 'blueprint', 'html'].each do |param|
|
21
|
+
hash[param] = ENV[param] if ENV[param].present?
|
22
|
+
end
|
23
|
+
|
24
|
+
hash
|
25
|
+
end
|
26
|
+
|
27
|
+
def compile(source, target)
|
28
|
+
compiler = ApiBlueprint::Compile::Compile.new(:source => source, :target => target, :logger => :stdout)
|
29
|
+
compiler.compile
|
30
|
+
|
31
|
+
compiler
|
32
|
+
end
|
33
|
+
|
34
|
+
def regenerate_dumps
|
35
|
+
Rake::Task["blueprint:collect:clear"].execute
|
36
|
+
puts
|
37
|
+
Rake::Task["blueprint:collect:generate"].execute
|
38
|
+
puts
|
39
|
+
end
|
40
|
+
|
41
|
+
namespace :blueprint do
|
42
|
+
desc 'Clear, generate and merge dumps for specified request spec(s)'
|
43
|
+
task :collect => :environment do
|
44
|
+
regenerate_dumps
|
45
|
+
|
46
|
+
Rake::Task["blueprint:collect:merge"].execute
|
47
|
+
end
|
48
|
+
|
49
|
+
namespace :collect do
|
50
|
+
desc 'Remove all generated request dumps'
|
51
|
+
task :clear => :environment do
|
52
|
+
files = ApiBlueprint::Collect::Storage.request_dumps
|
53
|
+
|
54
|
+
puts "Clearing #{files.count} request dumps..."
|
55
|
+
|
56
|
+
File.unlink(*files)
|
57
|
+
end
|
58
|
+
|
59
|
+
desc 'Generate request dumps for specified request spec(s)'
|
60
|
+
task :generate => :environment do
|
61
|
+
args = blueprintfile['spec'] || "spec/requests/#{ENV['group'] || 'api'}"
|
62
|
+
opts = { :order => 'default', :format => 'documentation' }
|
63
|
+
cmd = "API_BLUEPRINT_DUMP=1 bundle exec rspec #{opts.map{|k,v| "--#{k} #{v}"}.join(' ')} #{args}"
|
64
|
+
|
65
|
+
puts "Invoking '#{cmd}'..."
|
66
|
+
|
67
|
+
system(cmd)
|
68
|
+
end
|
69
|
+
|
70
|
+
desc 'Merge all existing request dumps into single blueprint'
|
71
|
+
task :merge => :environment do
|
72
|
+
target = blueprintfile['blueprint'] || Rails.root.join('tmp', 'merge.md')
|
73
|
+
|
74
|
+
ApiBlueprint::Collect::Merge.new(:target => target, :logger => :stdout, :naming => blueprintfile['naming']).merge
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
namespace :examples do
|
79
|
+
desc 'Clear existing examples in blueprint'
|
80
|
+
task :clear => :environment do
|
81
|
+
target = blueprintfile(:write_blueprint => false)['blueprint'] || Rails.root.join('tmp', 'merge.md')
|
82
|
+
|
83
|
+
ApiBlueprint::Collect::Merge.new(:target => target, :logger => :stdout).clear_examples
|
84
|
+
end
|
85
|
+
|
86
|
+
desc 'Uuse dumps to update examples in blueprint'
|
87
|
+
task :update => :environment do
|
88
|
+
target = blueprintfile(:write_blueprint => false)['blueprint'] || Rails.root.join('tmp', 'merge.md')
|
89
|
+
|
90
|
+
ApiBlueprint::Collect::Merge.new(:target => target, :logger => :stdout).update_examples
|
91
|
+
end
|
92
|
+
|
93
|
+
desc 'Use dumps to replace examples in blueprint'
|
94
|
+
task :replace => :environment do
|
95
|
+
target = blueprintfile(:write_blueprint => false)['blueprint'] || Rails.root.join('tmp', 'merge.md')
|
96
|
+
|
97
|
+
ApiBlueprint::Collect::Merge.new(:target => target, :logger => :stdout).clear_examples
|
98
|
+
ApiBlueprint::Collect::Merge.new(:target => target, :logger => :stdout).update_examples
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
desc 'Compile the blueprint into complete HTML documentation'
|
103
|
+
task :compile => :environment do
|
104
|
+
source = blueprintfile(:write_blueprint => false)['blueprint'] || Rails.root.join('tmp', 'merge.md')
|
105
|
+
target = blueprintfile(:write_blueprint => false)['html'] || source.to_s.sub(/\.md$/, '.html')
|
106
|
+
|
107
|
+
compile(source, target)
|
108
|
+
end
|
109
|
+
|
110
|
+
desc 'Watch for changes in the blueprint and compile it into HTML on every change'
|
111
|
+
task :watch => :environment do
|
112
|
+
source = blueprintfile(:write_blueprint => false)['blueprint'] || Rails.root.join('tmp', 'merge.md')
|
113
|
+
target = blueprintfile(:write_blueprint => false)['html'] || source.to_s.sub(/\.md$/, '.html')
|
114
|
+
|
115
|
+
files = compile(source, target).partials
|
116
|
+
|
117
|
+
FileWatcher.new(files).watch do |filename|
|
118
|
+
puts "\n--- #{Time.now} [#{filename.split('/').last}] ---\n\n"
|
119
|
+
compile(source, target)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
desc 'Deploy the HTML documentation on remote target'
|
124
|
+
task :deploy => :environment do
|
125
|
+
Rake::Task["blueprint:compile"].execute
|
126
|
+
|
127
|
+
source = blueprintfile(:write_blueprint => false)['html']
|
128
|
+
target = blueprintfile(:write_blueprint => false)['deploy']
|
129
|
+
|
130
|
+
if source.present? && target.present?
|
131
|
+
cmd = "scp -q #{source} #{target}"
|
132
|
+
|
133
|
+
puts "\nDeploying to '#{target}'..."
|
134
|
+
|
135
|
+
system(cmd)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
|
metadata
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: api_blueprint
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Karol Słuszniak
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-10-27 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: filewatcher
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: nokogiri
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rails
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: redcarpet
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec-rails
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description: You start with method list generated from RSpec request specs. For each
|
84
|
+
method, you get a list of parameters and examples. Then, you can extend it in whatever
|
85
|
+
way you need using Markdown syntax. You can organize documentation files into partials.
|
86
|
+
Upon any API change, like serializer change that changes responses, you can update
|
87
|
+
automatically generated parts of docs. Once done, you can compile your documentation
|
88
|
+
into single, nicely styled HTML file. You can also auto-deploy it via SSH.
|
89
|
+
email: k.sluszniak@visuality.pl
|
90
|
+
executables: []
|
91
|
+
extensions: []
|
92
|
+
extra_rdoc_files:
|
93
|
+
- README.md
|
94
|
+
files:
|
95
|
+
- README.md
|
96
|
+
- lib/api_blueprint.rb
|
97
|
+
- lib/api_blueprint/collect/controller_hook.rb
|
98
|
+
- lib/api_blueprint/collect/merge.rb
|
99
|
+
- lib/api_blueprint/collect/preprocessor.rb
|
100
|
+
- lib/api_blueprint/collect/renderer.rb
|
101
|
+
- lib/api_blueprint/collect/spec_hook.rb
|
102
|
+
- lib/api_blueprint/collect/storage.rb
|
103
|
+
- lib/api_blueprint/compile/compile.rb
|
104
|
+
- lib/api_blueprint/compile/storage.rb
|
105
|
+
- lib/api_blueprint/railtie.rb
|
106
|
+
- lib/api_blueprint/version.rb
|
107
|
+
- lib/tasks/blueprint.rake
|
108
|
+
homepage: http://github.com/visualitypl/api-blueprint
|
109
|
+
licenses:
|
110
|
+
- MIT
|
111
|
+
metadata: {}
|
112
|
+
post_install_message:
|
113
|
+
rdoc_options: []
|
114
|
+
require_paths:
|
115
|
+
- lib
|
116
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
117
|
+
requirements:
|
118
|
+
- - ">="
|
119
|
+
- !ruby/object:Gem::Version
|
120
|
+
version: '0'
|
121
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - ">="
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
requirements: []
|
127
|
+
rubyforge_project:
|
128
|
+
rubygems_version: 2.2.2
|
129
|
+
signing_key:
|
130
|
+
specification_version: 4
|
131
|
+
summary: Semi-automatic solution for creating Rails app's API documentation based
|
132
|
+
on RSpec request specs.
|
133
|
+
test_files: []
|