cuke-api-docs 0.0.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: da0bfdf2234fa77fb9d57738038500c0c2bb4965
4
+ data.tar.gz: 450446cc4d5f5a9fdeb2a1665e6fda90e3128a3a
5
+ SHA512:
6
+ metadata.gz: fee43606612a275432f209e657aa075d4befe646489ba37acff826e6c39ea6595740d754cd4b922a72772cc9e96b4f1084f56b002ecb3a264ab735f137469326
7
+ data.tar.gz: 445c9a130517d8242de6b01dff75745cb25675a208b4df3920e46e2fcebf1e0015c6e8a41315d57fe5a0f94724d81d81acd712b79b4577d91d1fd8eec1ea9970
data/.gitignore ADDED
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in cuke-api-docs.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Bruz Marzolf
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # cuke-api-docs
2
+
3
+ A Cucumber formatter that produces API documentation from Cucumber features that have follow a set of rules.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'cuke-api-docs'
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ Either specify it when you use Cucumber:
16
+
17
+ ```bash
18
+ cucumber --format Cucumber::Formatter::ApiDocs
19
+ ```
20
+
21
+ Or, add it to your Rakefile:
22
+
23
+ ```ruby
24
+ Cucumber::Rake::Task.new(:docs) do |t|
25
+ t.cucumber_opts = "features --format Cucumber::Formatter::ApiDocs"
26
+ end
27
+ ```
28
+
29
+ So that you can do:
30
+
31
+ ```bash
32
+ rake docs
33
+ ```
34
+
35
+ Either way, it produces a file named docs.html, which is a self-contained HTML documentation file.
36
+
37
+ ## Contributing
38
+
39
+ 1. Fork it ( https://github.com/[my-github-username]/cuke-api-docs/fork )
40
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
41
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
42
+ 4. Push to the branch (`git push origin my-new-feature`)
43
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'cuke-api-docs/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "cuke-api-docs"
8
+ spec.version = CukeApiDocs::VERSION
9
+ spec.authors = ["Bruz Marzolf"]
10
+ spec.email = ["bruz@bruzilla.com"]
11
+ spec.summary = %q{Cucumber formatter that produces HTML API documentation}
12
+ spec.homepage = ""
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files -z`.split("\x0")
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_dependency "arbre", "~> 1.0"
21
+ spec.add_dependency "cucumber", "~> 1.3"
22
+ spec.add_dependency "json", "~> 1.8"
23
+ spec.add_dependency "deep_merge", "~> 1.0"
24
+ spec.add_dependency "virtus", "~> 1.0"
25
+ spec.add_development_dependency "bundler", "~> 1.7"
26
+ spec.add_development_dependency "rake", "~> 10.0"
27
+ end
@@ -0,0 +1,367 @@
1
+ require 'virtus'
2
+ require 'arbre'
3
+ require 'json'
4
+ require 'deep_merge/rails_compat'
5
+
6
+ module Cucumber
7
+ module Formatter
8
+ class ApiDocs
9
+
10
+ VERB_ORDER = %w(GET POST PUT PATCH DELETE)
11
+
12
+ class Parameter
13
+ include Virtus.model
14
+
15
+ attribute :type, String
16
+ attribute :name, String
17
+ attribute :value, String
18
+ end
19
+
20
+ class Example
21
+ include Virtus.model
22
+
23
+ attribute :name, String
24
+ attribute :prerequisites, Array[String]
25
+ attribute :parameters, Array[Parameter]
26
+ attribute :code, Integer
27
+ attribute :content_type, String
28
+ attribute :json_response, Hash, default: {}
29
+ end
30
+
31
+ class Group
32
+ include Virtus.model
33
+
34
+ attribute :key, String
35
+ attribute :name, String
36
+
37
+ def self.for_tag(tag)
38
+ @groups ||= {}
39
+ @groups[tag] ||= create(tag)
40
+ end
41
+
42
+ def self.create(tag)
43
+ key = tag.gsub("@", "")
44
+ name = key.gsub("_", " ").capitalize
45
+
46
+ new(key: key, name: name)
47
+ end
48
+ end
49
+
50
+ class Endpoint
51
+ include Virtus.model
52
+
53
+ attribute :description, String
54
+ attribute :tag, String
55
+ attribute :group, String
56
+ attribute :verb, String
57
+ attribute :path, String
58
+ attribute :examples, Array[Example]
59
+ attribute :parameters, Array[Parameter]
60
+
61
+ def name
62
+ "#{verb} #{path}"
63
+ end
64
+
65
+ def sort_order
66
+ VERB_ORDER.index(verb) || fail("Unable to look up verb for #{name}")
67
+ end
68
+ end
69
+
70
+ def initialize(step_mother, io, options)
71
+ @endpoints = []
72
+ end
73
+
74
+ def after_features(features)
75
+ write_html
76
+ end
77
+
78
+ def before_feature(feature)
79
+ verb, path = feature.name.split(" ")
80
+ @endpoint = Endpoint.new(verb: verb, path: path)
81
+ end
82
+
83
+ def after_feature(feature)
84
+ @endpoint.parameters.uniq! { |parameter| parameter.name }
85
+ @endpoints << @endpoint
86
+ end
87
+
88
+ def before_comment(comment)
89
+ @endpoint.description = comment.to_sexp[1].gsub(/#\s*/, "")
90
+ end
91
+
92
+ def before_feature_element(feature_element)
93
+ @example = Example.new(name: feature_element.title)
94
+ @prior_keyword = nil
95
+ end
96
+
97
+ def after_feature_element(feature_element)
98
+ @endpoint.examples << @example
99
+ end
100
+
101
+ def before_step(step)
102
+ keyword = if step.keyword.strip == "And" && @prior_keyword
103
+ @prior_keyword
104
+ else
105
+ step.keyword.strip
106
+ end
107
+
108
+ case keyword
109
+ when "Given"
110
+ @example.prerequisites << step.name
111
+ @prior_keyword = "Given"
112
+ when "When"
113
+ if step.multiline_arg
114
+ step.multiline_arg.rows.each do |type, name, value|
115
+ @example.parameters << Parameter.new(type: type, name: name, value: value)
116
+ @endpoint.parameters << Parameter.new(type: type, name: name)
117
+ end
118
+ end
119
+
120
+ @prior_keyword = "When"
121
+ when "Then"
122
+ if matches = step.name.match(/the status code is (\d+)/)
123
+ @example.code = matches[1]
124
+ elsif matches = step.name.match(/Content-Type is (.*)$/)
125
+ @example.content_type = matches[1]
126
+ elsif matches = step.name.match(/the JSON response at "(.*?)" should include (keys)*:/)
127
+ key = matches[1]
128
+
129
+ if step.multiline_arg.respond_to? :rows
130
+ step.multiline_arg.rows.each do |nested_key, value|
131
+ add_json("#{key}/#{nested_key}", json_value(value))
132
+ end
133
+ else
134
+ add_json(key, JSON.parse(step.multiline_arg))
135
+ end
136
+ elsif matches = step.name.match(/the JSON response at "(.*?)" should be ([^ ]*?)$/)
137
+ keys = matches[1]
138
+ value = matches[2]
139
+ add_json(keys, json_value(value))
140
+ elsif matches = step.name.match(/the JSON response at "(.*?)" should be (variable )*"(.*?)"/)
141
+ keys = matches[1]
142
+ value = matches[3]
143
+ add_json(keys, value)
144
+ end
145
+
146
+ @prior_keyword = "Then"
147
+ end
148
+ end
149
+
150
+ def tag_name(tag)
151
+ @endpoint.group = Group.for_tag(tag)
152
+ end
153
+
154
+ private
155
+
156
+ def log(message)
157
+ @log ||= File.open("doc-debug.log", "w")
158
+ @log << message + "\n"
159
+ end
160
+
161
+ def add_json(path, terminal_value)
162
+ keys = path.split("/")
163
+
164
+ return @example.json_response[path] = terminal_value if keys.size == 1
165
+
166
+ keys.each_cons(2).reduce(@example.json_response) do |current, (key_one_string, key_two)|
167
+ key_one = if key_one_string.match(/^\d+$/)
168
+ key_one_string.to_i
169
+ else
170
+ key_one_string
171
+ end
172
+
173
+ value = case key_two
174
+ when /^\d+$/
175
+ if current[key_one].is_a?(Array)
176
+ current[key_one]
177
+ else
178
+ []
179
+ end
180
+ else
181
+ if current[key_one].is_a?(Hash)
182
+ current[key_one].merge(key_two => terminal_value)
183
+ else
184
+ {key_two => terminal_value}
185
+ end
186
+ end
187
+
188
+ current[key_one] = value
189
+
190
+ value
191
+ end
192
+ end
193
+
194
+ def json_value(string)
195
+ JSON.parse('{"key":' + string + '}')["key"]
196
+ end
197
+
198
+ def write_html
199
+ endpoints_by_group = @endpoints.group_by(&:group).sort_by { |group, _| group.name }
200
+
201
+ html = Arbre::Context.new do
202
+ head do
203
+ link rel: "stylesheet", href: "https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css"
204
+ link rel: "stylesheet", href: "https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap-theme.min.css"
205
+ style do
206
+ "
207
+ .summary, .panel-heading {
208
+ cursor: pointer;
209
+ }
210
+
211
+ nav {
212
+ margin-top: 38px;
213
+ min-width: 255px;
214
+ top: 0;
215
+ bottom: 0;
216
+ padding-right: 12px;
217
+ padding-bottom: 12px;
218
+ overflow-y: auto;
219
+ }
220
+ ".html_safe
221
+ end
222
+ script src: "https://code.jquery.com/jquery-2.1.1.min.js"
223
+ script src: "https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"
224
+ end
225
+
226
+ body do
227
+ div class: "container padded" do
228
+ div class: "col-md-3" do
229
+ nav class: "hidden-sm hidden-xs affix nav" do
230
+ div class: "list-group" do
231
+ endpoints_by_group.each do |group, _|
232
+ a group.name, href: "##{group.key}", class: "list-group-item"
233
+ end
234
+ end
235
+ end
236
+ end
237
+
238
+ div class: "col-md-9" do
239
+ endpoints_by_group.each do |group, endpoints|
240
+ a name: group.key
241
+ h1 group.name
242
+
243
+ endpoints.sort_by(&:sort_order).each do |endpoint|
244
+ div class: "endpoint" do
245
+ div class: "summary well well-sm" do
246
+ text_node endpoint.name
247
+ span class: "glyphicon glyphicon-plus pull-right"
248
+ end
249
+
250
+ div class: "detail panel panel-default", style: "display:none" do
251
+ div class: "panel-heading" do
252
+ h2 do
253
+ text_node endpoint.name
254
+ span class: "glyphicon glyphicon-minus pull-right"
255
+ end
256
+ end
257
+
258
+ div class: "panel-body" do
259
+ div endpoint.description
260
+
261
+ h3 "Parameters"
262
+
263
+ table class: "table table-striped table-bordered" do
264
+ thead do
265
+ tr do
266
+ th { "Name" }
267
+ th { "Type" }
268
+ end
269
+ end
270
+
271
+ tbody do
272
+ endpoint.parameters.each do |parameter|
273
+ tr do
274
+ td { parameter.type }
275
+ td { parameter.name }
276
+ end
277
+ end
278
+ end
279
+ end
280
+
281
+ h3 "Examples"
282
+ endpoint.examples.each do |example|
283
+ panel_type = case example.code
284
+ when 200..299
285
+ "success"
286
+ when 400..499
287
+ "warning"
288
+ when 500.599
289
+ "error"
290
+ end
291
+
292
+ div class: "panel panel-#{panel_type}" do
293
+ div example.name, class: "panel-heading"
294
+
295
+ div class: "panel-body" do
296
+ h4 "Request parameters"
297
+ table class: "table table-striped table-bordered" do
298
+ thead do
299
+ tr do
300
+ th { "Name" }
301
+ th { "Type" }
302
+ th { "Value" }
303
+ end
304
+ end
305
+
306
+ tbody do
307
+ example.parameters.each do |parameter|
308
+ tr do
309
+ td { parameter.type }
310
+ td { parameter.name }
311
+ td { parameter.value }
312
+ end
313
+ end
314
+ end
315
+ end
316
+
317
+ h4 "Response"
318
+
319
+ div class: "panel panel-default" do
320
+ div class: "panel-heading" do
321
+ div "Code: #{example.code}"
322
+ div "Content-Type: #{example.content_type}"
323
+ end
324
+
325
+ div class: "panel-body" do
326
+ if example.json_response.empty?
327
+ "Empty body"
328
+ else
329
+ json = JSON.pretty_generate(example.json_response)
330
+ code json.gsub("\n", "<br>").gsub(" ", "&nbsp").html_safe
331
+ end
332
+ end
333
+ end
334
+ end
335
+ end
336
+ end
337
+ end
338
+ end
339
+ end
340
+ end
341
+ end
342
+ end
343
+ end
344
+
345
+ script "
346
+ $(document).ready(function(){
347
+ $('.summary').click(function() {
348
+ $(this).hide();
349
+ $(this).closest('.endpoint').find('.detail').show();
350
+ });
351
+
352
+ $('.detail .panel-heading').click(function() {
353
+ $(this).closest('.detail').hide();
354
+ $(this).closest('.endpoint').find('.summary').show();
355
+ });
356
+ });
357
+ ".html_safe
358
+ end
359
+ end
360
+
361
+ file = File.open("docs.html", "w")
362
+ file << html.to_s
363
+ file.close
364
+ end
365
+ end
366
+ end
367
+ end
@@ -0,0 +1,3 @@
1
+ module CukeApiDocs
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,11 @@
1
+ require "cuke-api-docs/version"
2
+ require "cucumber/formatter/api_docs"
3
+
4
+ # Extend Cucumber's builtin formats, so that this
5
+ # formatter can be used with --format api-docs
6
+ require 'cucumber/cli/main'
7
+
8
+ Cucumber::Cli::Options::BUILTIN_FORMATS["api-docs"] = [
9
+ "Cucumber::Formatter::ApiDocs",
10
+ "Turns specially formatted cucumber features into API documentation"
11
+ ]
metadata ADDED
@@ -0,0 +1,151 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cuke-api-docs
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Bruz Marzolf
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-11-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: arbre
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: cucumber
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.3'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: json
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.8'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.8'
55
+ - !ruby/object:Gem::Dependency
56
+ name: deep_merge
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: virtus
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: bundler
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.7'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.7'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '10.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '10.0'
111
+ description:
112
+ email:
113
+ - bruz@bruzilla.com
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - ".gitignore"
119
+ - Gemfile
120
+ - LICENSE.txt
121
+ - README.md
122
+ - Rakefile
123
+ - cuke-api-docs.gemspec
124
+ - lib/cucumber/formatter/api_docs.rb
125
+ - lib/cuke-api-docs.rb
126
+ - lib/cuke-api-docs/version.rb
127
+ homepage: ''
128
+ licenses:
129
+ - MIT
130
+ metadata: {}
131
+ post_install_message:
132
+ rdoc_options: []
133
+ require_paths:
134
+ - lib
135
+ required_ruby_version: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ version: '0'
140
+ required_rubygems_version: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ requirements: []
146
+ rubyforge_project:
147
+ rubygems_version: 2.4.2
148
+ signing_key:
149
+ specification_version: 4
150
+ summary: Cucumber formatter that produces HTML API documentation
151
+ test_files: []