rspec-document_requests 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/.gitignore +10 -0
- data/.rspec +2 -0
- data/.travis.yml +3 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +4 -0
- data/README.md +245 -0
- data/Rakefile +1 -0
- data/UNLICENSE +24 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/lib/rspec/document_requests.rb +21 -0
- data/lib/rspec/document_requests/builder.rb +98 -0
- data/lib/rspec/document_requests/configuration.rb +35 -0
- data/lib/rspec/document_requests/dsl.rb +63 -0
- data/lib/rspec/document_requests/explanation.rb +39 -0
- data/lib/rspec/document_requests/organized_request.rb +56 -0
- data/lib/rspec/document_requests/request.rb +107 -0
- data/lib/rspec/document_requests/version.rb +5 -0
- data/lib/rspec/document_requests/writers.rb +8 -0
- data/lib/rspec/document_requests/writers/base.rb +33 -0
- data/lib/rspec/document_requests/writers/markdown.rb +108 -0
- data/rspec-document_requests.gemspec +26 -0
- metadata +108 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 07dd25c82e673ad273e0c18f127ec929c0869b28
|
4
|
+
data.tar.gz: b9b67877628275e67bdf87e8d2729ca7c1babcd1
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5d09018dd25cd1e11a5a8e196bf544f93a51e211323a95930387c9245a11ecbaf152e1bcc084e4ae0a04a26fd7ded43d6a62f2ace38693dda9e05c16203b9352
|
7
|
+
data.tar.gz: 5162824b78e0486125a648b75c7e395842b94119f99481b4db66bccf758b6d9514f8ca73d18ab2287b19d417f52cdf9dc7810d7948436902bf76f67c9824fe78
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# Contributor Code of Conduct
|
2
|
+
|
3
|
+
As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
|
4
|
+
|
5
|
+
We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion.
|
6
|
+
|
7
|
+
Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
|
8
|
+
|
9
|
+
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
|
10
|
+
|
11
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
|
12
|
+
|
13
|
+
This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,245 @@
|
|
1
|
+
# RSpec::DocumentRequests [![Gem Version](https://badge.fury.io/rb/rspec-document_requests.svg)](http://badge.fury.io/rb/rspec-document_requests)
|
2
|
+
|
3
|
+
This gem is an extension to [rspec-rails](https://github.com/rspec/rspec-rails),
|
4
|
+
which adds the capability to automatically document requests made by your
|
5
|
+
request specs. This will help you document your API effortlessly.
|
6
|
+
|
7
|
+
This was made after checking out
|
8
|
+
[rspec_api_documentation](https://github.com/zipmark/rspec_api_documentation),
|
9
|
+
in which I didn't like the fact that it forces you into its own DSL (which is
|
10
|
+
basically a small subset of RSpec DSL). If you liked it, you'll probably like
|
11
|
+
this one more.
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
|
15
|
+
Add this line to your application's Gemfile:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
gem 'rspec-document_requests'
|
19
|
+
```
|
20
|
+
|
21
|
+
And then execute:
|
22
|
+
|
23
|
+
$ bundle
|
24
|
+
|
25
|
+
Or install it yourself as:
|
26
|
+
|
27
|
+
$ gem install rspec-document_requests
|
28
|
+
|
29
|
+
## Usage
|
30
|
+
|
31
|
+
### Require
|
32
|
+
|
33
|
+
Require the DSL _after_ you require `rspec/rails` (most likely in your
|
34
|
+
`spec/rails_helper.rb`):
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
# spec/rails_helper.rb
|
38
|
+
|
39
|
+
...
|
40
|
+
require 'rspec/rails'
|
41
|
+
require 'rspec/document_requests/dsl' # <- this line
|
42
|
+
...
|
43
|
+
```
|
44
|
+
|
45
|
+
### Marking code to document
|
46
|
+
|
47
|
+
In your example group (`describe`/`context`), simply add the `doc: true` metadata:
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
# spec/requests/session_spec.rb
|
51
|
+
|
52
|
+
RSpec.describe "Session resource", type: :request, doc: true do
|
53
|
+
describe "Create session" do
|
54
|
+
# User creation, will not be documented in "Session resource" documentation (nodoc)
|
55
|
+
before do
|
56
|
+
nodoc { post "/users", user: { username: "myuser", password: "123123" } }
|
57
|
+
end
|
58
|
+
|
59
|
+
context "Correct password" do
|
60
|
+
before { post "/session", session: { username: "myuser", password: "123123" } }
|
61
|
+
it { ... }
|
62
|
+
end
|
63
|
+
|
64
|
+
context "Incorrect password" do
|
65
|
+
before { post "/session", session: { username: "myuser", password: "456456" } }
|
66
|
+
it { ... }
|
67
|
+
end
|
68
|
+
|
69
|
+
# Extra test, will not be documented (doc: false)
|
70
|
+
context "Incorrect username", doc: false do
|
71
|
+
before { post "/session", session: { username: "wronguser", password: "123123" } }
|
72
|
+
it { ... }
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
```
|
77
|
+
|
78
|
+
### Running in documentation mode
|
79
|
+
|
80
|
+
To prevent every rspec run deleting all your documentation, this gem only
|
81
|
+
documents the requests when `DOC=true` environment variable is set. This will
|
82
|
+
also exclude any specs without `doc: true` metadata to make this run faster.
|
83
|
+
|
84
|
+
DOC=true rake spec
|
85
|
+
|
86
|
+
### Explaining the request
|
87
|
+
|
88
|
+
**NOTE:** This DSL is not available in `doc: false` example groups (`describe`/`context`).
|
89
|
+
|
90
|
+
Just before your request, it's a good idea to explain (everything is optional):
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
# spec/requests/session_spec.rb
|
94
|
+
|
95
|
+
RSpec.describe "Session resource", type: :request, doc: true do
|
96
|
+
describe "Create session" do
|
97
|
+
before do
|
98
|
+
explain "Creating the user"
|
99
|
+
post "/users", user: { username: "myuser", password: "123123" }
|
100
|
+
end
|
101
|
+
|
102
|
+
before do
|
103
|
+
explain do # No request explanation
|
104
|
+
request do
|
105
|
+
parameter 'session[username]', "The username", required: true, type: :string
|
106
|
+
parameter 'session[password]', required: true, type: :string # No explanation
|
107
|
+
header 'Content-Type', ... # you get the point
|
108
|
+
end
|
109
|
+
response do
|
110
|
+
parameter 'message', "Message from the server", required: true # No type
|
111
|
+
parameter 'session_id', "The session ID" # Not required and no type
|
112
|
+
header 'Set-Cookie', ...
|
113
|
+
end
|
114
|
+
end
|
115
|
+
post "/session", session: { username: "myuser", password: "123123" }
|
116
|
+
end
|
117
|
+
it { ... }
|
118
|
+
end
|
119
|
+
end
|
120
|
+
```
|
121
|
+
|
122
|
+
**NOTE:** Explaining response parameters only works when this gem can
|
123
|
+
parse the response body, see [here](#configuration) how to configure it.
|
124
|
+
|
125
|
+
### Configuration
|
126
|
+
|
127
|
+
These are the possible configurations:
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
# spec/document_requests_helper.rb
|
131
|
+
|
132
|
+
RSpec::DocumentRequests.configure do |config|
|
133
|
+
# These are the default values
|
134
|
+
|
135
|
+
config.directory = "doc" # From Rails.root. CAREFUL: directory/root gets deleted!
|
136
|
+
config.root = "Requests" # I actually use API, figured this is a better default
|
137
|
+
# Example groups with less than this amount of example group (describe/context)
|
138
|
+
# levels under it will be grouped under its parent example group.
|
139
|
+
config.group_levels = 1
|
140
|
+
# Currently only markdown available with the gem.
|
141
|
+
# Contribute more by checking out lib/rspec/document_requests/writers/base.rb.
|
142
|
+
config.writer = RSpec::DocumentRequests:::Writers::Markdown
|
143
|
+
# Converts example groups (describe/context) to filenames (and directories), the default simple
|
144
|
+
# lower-cases and uses dash (-) for spaces.
|
145
|
+
config.filename_generator = -> (name) { name.downcase.gsub(/[_ ]+/, '-') }
|
146
|
+
# Allows showing response body as a table of parameters (with explanations).
|
147
|
+
# Don't forget to contribute more!
|
148
|
+
config.response_parser = -> (response) {
|
149
|
+
case
|
150
|
+
when response.content_type.json? then JSON.parse(response.body)
|
151
|
+
end
|
152
|
+
}
|
153
|
+
|
154
|
+
config.include_request_parameters = nil # nil means not used
|
155
|
+
config.exclude_request_parameters = []
|
156
|
+
config.hide_request_parameters = [] # Displays '...' instead of actual value
|
157
|
+
config.include_response_parameters = nil
|
158
|
+
config.exclude_response_parameters = []
|
159
|
+
config.hide_response_parameters = []
|
160
|
+
end
|
161
|
+
```
|
162
|
+
|
163
|
+
Don't forget to require your file:
|
164
|
+
|
165
|
+
```ruby
|
166
|
+
# .rspec
|
167
|
+
|
168
|
+
--require document_requests_helper
|
169
|
+
|
170
|
+
```
|
171
|
+
|
172
|
+
## REQUIRED best practices
|
173
|
+
|
174
|
+
It's always a good idea to follow this best-practice, but for this gem to work
|
175
|
+
it's necessary.
|
176
|
+
|
177
|
+
The implementation documents example groups (`describe`/`context`), and not
|
178
|
+
examples (`it`/`specify`).
|
179
|
+
|
180
|
+
It is important that you do not make requests
|
181
|
+
(`get`/`post`/`put`/`patch`/`delete`/`head`) from inside an example
|
182
|
+
(`it`/`specify`). It will only document requests from the first example of each
|
183
|
+
example group (`describe`/`context`).
|
184
|
+
|
185
|
+
It does however work only from inside examples (`it`/`specify`)
|
186
|
+
so requests from any form of `before`/`after`/`around` that is _not_ `:each`
|
187
|
+
will not be documented.
|
188
|
+
|
189
|
+
This best practice has other upsides other than making this gem work which I
|
190
|
+
will not describe here. But here is a nice example for this best practice to follow:
|
191
|
+
|
192
|
+
```ruby
|
193
|
+
RSpec.describe "Some interface (class/feature/API resource)", doc: true do
|
194
|
+
subject { response }
|
195
|
+
|
196
|
+
describe "An interface within the interface (method/action/sub-feature)" do
|
197
|
+
before { post "/api/action", param: param_value }
|
198
|
+
|
199
|
+
context "Some scenario (attributes/params/prerequisite)" do
|
200
|
+
let(:param_value) { "scenario value" }
|
201
|
+
|
202
|
+
it { should have_http_status :ok }
|
203
|
+
# "body is not wrong" will not be documented
|
204
|
+
specify("body is not wrong") { expect(response.body).to eq "something" }
|
205
|
+
end
|
206
|
+
|
207
|
+
context "Another scenario" do
|
208
|
+
let(:param_value) { "another scenario" }
|
209
|
+
|
210
|
+
...
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
context "Some scenario" do
|
215
|
+
before { nodoc { post "/api/scenario_prerequisite" } }
|
216
|
+
|
217
|
+
describe "An interface" do
|
218
|
+
before { get "/api/result" }
|
219
|
+
|
220
|
+
...
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
```
|
225
|
+
|
226
|
+
## Development
|
227
|
+
|
228
|
+
After checking out the repo, run `bin/setup` to install dependencies.
|
229
|
+
Then, run `bin/console` for an interactive prompt that will allow you to experiment.
|
230
|
+
|
231
|
+
## Contributing
|
232
|
+
|
233
|
+
The gem works, but it's missing some basics:
|
234
|
+
|
235
|
+
* Unit tests (specs).
|
236
|
+
* Example generated documentations.
|
237
|
+
* More writers (see `lib/rspec/document_requests/writers/base.rb`).
|
238
|
+
|
239
|
+
So...
|
240
|
+
|
241
|
+
1. Fork it ( https://github.com/odedniv/rspec-document_requests/fork )
|
242
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
243
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
244
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
245
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/UNLICENSE
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
This is free and unencumbered software released into the public domain.
|
2
|
+
|
3
|
+
Anyone is free to copy, modify, publish, use, compile, sell, or
|
4
|
+
distribute this software, either in source code form or as a compiled
|
5
|
+
binary, for any purpose, commercial or non-commercial, and by any
|
6
|
+
means.
|
7
|
+
|
8
|
+
In jurisdictions that recognize copyright laws, the author or authors
|
9
|
+
of this software dedicate any and all copyright interest in the
|
10
|
+
software to the public domain. We make this dedication for the benefit
|
11
|
+
of the public at large and to the detriment of our heirs and
|
12
|
+
successors. We intend this dedication to be an overt act of
|
13
|
+
relinquishment in perpetuity of all present and future rights to this
|
14
|
+
software under copyright law.
|
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 NONINFRINGEMENT.
|
19
|
+
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
20
|
+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
21
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
23
|
+
|
24
|
+
For more information, please refer to <http://unlicense.org/>
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "rspec/document_requests"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'rspec/document_requests/version'
|
2
|
+
|
3
|
+
module RSpec
|
4
|
+
module DocumentRequests
|
5
|
+
autoload :Configuration, 'rspec/document_requests/configuration'
|
6
|
+
autoload :Request, 'rspec/document_requests/request'
|
7
|
+
autoload :Explanation, 'rspec/document_requests/explanation'
|
8
|
+
autoload :DSL, 'rspec/document_requests/dsl'
|
9
|
+
autoload :OrganizedRequest, 'rspec/document_requests/organized_request'
|
10
|
+
autoload :Builder, 'rspec/document_requests/builder'
|
11
|
+
autoload :Writers, 'rspec/document_requests/writers'
|
12
|
+
|
13
|
+
def self.configuration
|
14
|
+
@configuration ||= Configuration.new
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.configure
|
18
|
+
yield self.configuration
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module RSpec
|
2
|
+
module DocumentRequests
|
3
|
+
class Builder
|
4
|
+
def initialize
|
5
|
+
clean
|
6
|
+
@root = OrganizedRequest.organize
|
7
|
+
write
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def config
|
13
|
+
DocumentRequests.configuration
|
14
|
+
end
|
15
|
+
|
16
|
+
def clean
|
17
|
+
root_directory = config.directory.join(config.filename_generator.call(config.root))
|
18
|
+
root_filename = root_directory.sub_ext(config.writer::EXTENSION)
|
19
|
+
|
20
|
+
root_directory.rmtree if root_directory.exist?
|
21
|
+
root_filename.delete if root_filename.exist?
|
22
|
+
end
|
23
|
+
|
24
|
+
def write(organized_request = @root, fullpath: config.directory)
|
25
|
+
@current = organized_request
|
26
|
+
@current_path = fullpath
|
27
|
+
|
28
|
+
@current_path.mkpath
|
29
|
+
@current_path.join(@current.filename).sub_ext(config.writer::EXTENSION).open('wb') do |file|
|
30
|
+
@writer = config.writer.new(file)
|
31
|
+
write_breadcrumb
|
32
|
+
write_title
|
33
|
+
@current.ungrouped_children.each { |child| write_child(child) }
|
34
|
+
write_recursive_requests(@current)
|
35
|
+
end
|
36
|
+
|
37
|
+
@current.ungrouped_children.each do |child|
|
38
|
+
# @current unusable from here on-end
|
39
|
+
write(child, fullpath: fullpath.join(organized_request.filename))
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def write_recursive_requests(child)
|
44
|
+
missing_levels = []
|
45
|
+
if not child == @current
|
46
|
+
missing = child
|
47
|
+
missing_levels.unshift(missing.description) while (missing = missing.parent) and missing != @current
|
48
|
+
end
|
49
|
+
|
50
|
+
child.example_requests.to_a.uniq { |e,| e.example_group }.each do |example, requests|
|
51
|
+
write_example_title(example, missing_levels: missing_levels) unless child == @current
|
52
|
+
requests.each { |request| write_request(request, missing_levels: missing_levels) }
|
53
|
+
end
|
54
|
+
|
55
|
+
child.grouped_children.each_with_index do |grandchild, i|
|
56
|
+
write_recursive_requests(grandchild)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def write_breadcrumb
|
61
|
+
current = @current
|
62
|
+
parent_tree = []
|
63
|
+
parent_tree.unshift(current) while current = current.parent
|
64
|
+
|
65
|
+
return if parent_tree.empty?
|
66
|
+
|
67
|
+
parent_path = Pathname.new('.').join(*parent_tree.length.times.map { '..' })
|
68
|
+
parent_tree.each do |parent|
|
69
|
+
@writer.breadcrumb(
|
70
|
+
description: parent.description,
|
71
|
+
filename: parent_path.join(parent.filename).sub_ext(config.writer::EXTENSION),
|
72
|
+
last: parent == @current.parent,
|
73
|
+
)
|
74
|
+
parent_path = parent_path.split[0]
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def write_title
|
79
|
+
@writer.title(@current.description)
|
80
|
+
end
|
81
|
+
|
82
|
+
def write_child(child)
|
83
|
+
@writer.child(
|
84
|
+
description: child.description,
|
85
|
+
filename: @current.filename.join(child.filename).sub_ext(config.writer::EXTENSION),
|
86
|
+
)
|
87
|
+
end
|
88
|
+
|
89
|
+
def write_example_title(example, missing_levels:)
|
90
|
+
@writer.example_title(example.example_group.metadata[:description], missing_levels: missing_levels)
|
91
|
+
end
|
92
|
+
|
93
|
+
def write_request(request, missing_levels:)
|
94
|
+
@writer.request(request, missing_levels: missing_levels)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module RSpec
|
2
|
+
module DocumentRequests
|
3
|
+
class Configuration
|
4
|
+
def self.add_property(name, default = nil)
|
5
|
+
attr_writer name
|
6
|
+
define_method name do
|
7
|
+
instance_variable_get(:"@#{name}") || default
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
add_property :directory, ::Rails.root.join("doc")
|
12
|
+
add_property :root, "Requests"
|
13
|
+
add_property :group_levels, 1
|
14
|
+
add_property :writer, Writers::Markdown
|
15
|
+
add_property :filename_generator, -> (name) { name.downcase.gsub(/[_ ]+/, '-') }
|
16
|
+
add_property :response_parser, -> (response) {
|
17
|
+
case
|
18
|
+
when response.content_type.json? then JSON.parse(response.body)
|
19
|
+
end
|
20
|
+
}
|
21
|
+
|
22
|
+
[:request, :response].each do |side|
|
23
|
+
[:parameters, :headers].each do |part|
|
24
|
+
add_property :"include_#{side}_#{part}", nil
|
25
|
+
add_property :"exclude_#{side}_#{part}", []
|
26
|
+
add_property :"hide_#{side}_#{part}", []
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def directory=(directory)
|
31
|
+
@directory = ::Rails.root.join(directory)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module RSpec
|
2
|
+
module DocumentRequests
|
3
|
+
module DSL
|
4
|
+
class << self
|
5
|
+
attr_accessor :documented_requests, :currently_documented_example
|
6
|
+
end
|
7
|
+
self.documented_requests = []
|
8
|
+
self.currently_documented_example = nil
|
9
|
+
|
10
|
+
[:get, :post, :patch, :put, :delete, :head].each do |method|
|
11
|
+
define_method(method) do |path, parameters = nil, headers_or_env = nil|
|
12
|
+
result = super(path, parameters, headers_or_env)
|
13
|
+
|
14
|
+
if not @document_request_prevented and DSL.currently_documented_example
|
15
|
+
DSL.documented_requests << Request.new(
|
16
|
+
explanation: document_request_explanation,
|
17
|
+
example: DSL.currently_documented_example,
|
18
|
+
method: method.to_s.upcase,
|
19
|
+
path: path,
|
20
|
+
request_parameters: parameters,
|
21
|
+
request_headers: headers,
|
22
|
+
response: response,
|
23
|
+
)
|
24
|
+
end
|
25
|
+
@document_request_explanation = Explanation.new
|
26
|
+
DSL.currently_documented_example = nil
|
27
|
+
|
28
|
+
result
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def explain(message = nil, &block)
|
33
|
+
document_request_explanation.message = message
|
34
|
+
document_request_explanation.instance_eval(&block) if block_given?
|
35
|
+
end
|
36
|
+
|
37
|
+
def document_request_explanation
|
38
|
+
@document_request_explanation ||= Explanation.new
|
39
|
+
end
|
40
|
+
|
41
|
+
def nodoc
|
42
|
+
@document_request_prevented = true
|
43
|
+
begin
|
44
|
+
yield
|
45
|
+
ensure
|
46
|
+
@document_request_prevented = false
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
RSpec.configure do |config|
|
54
|
+
if ENV['DOC']
|
55
|
+
config.filter_run :doc
|
56
|
+
config.run_all_when_everything_filtered = false
|
57
|
+
|
58
|
+
config.after(:suite) { RSpec::DocumentRequests::Builder.new }
|
59
|
+
config.before { |ex| RSpec::DocumentRequests::DSL.currently_documented_example = ex }
|
60
|
+
end
|
61
|
+
|
62
|
+
config.include RSpec::DocumentRequests::DSL, doc: true
|
63
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module RSpec
|
2
|
+
module DocumentRequests
|
3
|
+
class Explanation
|
4
|
+
class Side
|
5
|
+
attr_accessor :parameters, :headers
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@parameters = {}
|
9
|
+
@headers = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def parameter(name, *args)
|
13
|
+
@parameters[name] = Request::Parameter.new(*args)
|
14
|
+
end
|
15
|
+
|
16
|
+
def header(name, *args)
|
17
|
+
@headers[name] = Request::Parameter.new(*args)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_accessor :message
|
22
|
+
|
23
|
+
def initialize
|
24
|
+
@request = Side.new
|
25
|
+
@response = Side.new
|
26
|
+
end
|
27
|
+
|
28
|
+
def request(&block)
|
29
|
+
return @request if not block_given?
|
30
|
+
@request.instance_eval(&block)
|
31
|
+
end
|
32
|
+
|
33
|
+
def response(&block)
|
34
|
+
return @request if not block_given?
|
35
|
+
@response.instance_eval(&block)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module RSpec
|
2
|
+
module DocumentRequests
|
3
|
+
class OrganizedRequest
|
4
|
+
attr_reader :parent, :filename, :description, :example_requests, :children, :levels
|
5
|
+
def initialize(description:, parent: nil)
|
6
|
+
@description = description
|
7
|
+
@parent = parent
|
8
|
+
@filename = Pathname.new(DocumentRequests.configuration.filename_generator.call(description))
|
9
|
+
@example_requests = Hash.new { |h, k| h[k] = [] }
|
10
|
+
@children = {}
|
11
|
+
@levels = Hash.new { |h, k| h[k] = 0 }
|
12
|
+
@parent.increase_level(self) if @parent
|
13
|
+
end
|
14
|
+
|
15
|
+
def child(description)
|
16
|
+
@children[DocumentRequests.configuration.filename_generator.call(description)] ||= OrganizedRequest.new(description: description, parent: self)
|
17
|
+
end
|
18
|
+
|
19
|
+
def increase_level(child)
|
20
|
+
@grouped_children = @ungrouped_children = nil
|
21
|
+
@levels[child] += 1
|
22
|
+
@parent.increase_level(self) if @parent
|
23
|
+
end
|
24
|
+
|
25
|
+
def max_level
|
26
|
+
@levels.values.max || 0
|
27
|
+
end
|
28
|
+
|
29
|
+
def grouped_children
|
30
|
+
@grouped_children ||= @children.values.select { |child| child.max_level < DocumentRequests.configuration.group_levels }.sort_by(&:filename)
|
31
|
+
end
|
32
|
+
|
33
|
+
def ungrouped_children
|
34
|
+
@ungrouped_children ||= (@children.values - grouped_children).sort_by(&:filename)
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.organize
|
38
|
+
root = OrganizedRequest.new(description: DocumentRequests.configuration.root)
|
39
|
+
|
40
|
+
DSL.documented_requests.each do |request|
|
41
|
+
current = request.example.example_group.metadata
|
42
|
+
metadata_tree = [current]
|
43
|
+
metadata_tree.unshift(current) while current = current[:parent_example_group]
|
44
|
+
|
45
|
+
organized_request = root
|
46
|
+
metadata_tree.each do |metadata|
|
47
|
+
organized_request = organized_request.child(metadata[:description])
|
48
|
+
end
|
49
|
+
organized_request.example_requests[request.example] << request
|
50
|
+
end
|
51
|
+
|
52
|
+
root
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
module RSpec
|
2
|
+
module DocumentRequests
|
3
|
+
class Request
|
4
|
+
class Parameter
|
5
|
+
attr_accessor :message, :required, :type, :value
|
6
|
+
def initialize(message = nil, required: false, type: nil, value: nil)
|
7
|
+
@message = message
|
8
|
+
@required = required
|
9
|
+
@type = type
|
10
|
+
@value = value
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :explanation, :example, :method, :path
|
15
|
+
attr_reader :request_parameters, :request_headers
|
16
|
+
attr_reader :response, :parsed_response, :response_parameters, :response_headers
|
17
|
+
def initialize(explanation:, example:, method:, path:, request_parameters:, request_headers:, response:)
|
18
|
+
@explanation = explanation
|
19
|
+
@example = example
|
20
|
+
@method = method
|
21
|
+
@path = path
|
22
|
+
@response = response
|
23
|
+
|
24
|
+
process_request_parameters(request_parameters)
|
25
|
+
process_request_headers(request_headers)
|
26
|
+
process_response_parameters
|
27
|
+
process_response_headers
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def self.filter_values(name)
|
33
|
+
define_method(name) do
|
34
|
+
values = instance_variable_get(:"@#{name}")
|
35
|
+
next values if values.nil?
|
36
|
+
|
37
|
+
included_values = DocumentRequests.configuration.send(:"include_#{name}")
|
38
|
+
excluded_values = DocumentRequests.configuration.send(:"exclude_#{name}")
|
39
|
+
hidden_values = DocumentRequests.configuration.send(:"hide_#{name}")
|
40
|
+
|
41
|
+
values.select! do |k, v|
|
42
|
+
next false if included_values and included_values.exclude?(k)
|
43
|
+
next false if excluded_values.include?(k)
|
44
|
+
values[k].value = "..." if hidden_values.include?(k)
|
45
|
+
true
|
46
|
+
end
|
47
|
+
values
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
public
|
52
|
+
|
53
|
+
filter_values :request_parameters
|
54
|
+
filter_values :request_headers
|
55
|
+
filter_values :response_parameters
|
56
|
+
filter_values :response_headers
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def process_request_parameters(parameters, prefix: nil)
|
61
|
+
@request_parameters = {}
|
62
|
+
process_parameters(request_parameters, @request_parameters, explanation: @explanation.request.parameters)
|
63
|
+
end
|
64
|
+
|
65
|
+
def process_request_headers(headers)
|
66
|
+
@request_headers = {}
|
67
|
+
headers.each do |name, value|
|
68
|
+
@request_headers[name] = @explanation.request.headers[name] || Parameter.new
|
69
|
+
@request_headers[name].value = value
|
70
|
+
end
|
71
|
+
@explanation.request.headers.each { |name, header| @request_headers[name] ||= header }
|
72
|
+
end
|
73
|
+
|
74
|
+
def process_response_parameters(parameters = nil)
|
75
|
+
@parsed_response = DocumentRequests.configuration.response_parser.call(response)
|
76
|
+
if @parsed_response
|
77
|
+
@response_parameters = {}
|
78
|
+
process_parameters(@parsed_response, @response_parameters, explanation: @explanation.response.parameters)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def process_response_headers
|
83
|
+
@response_headers = {}
|
84
|
+
@response.headers.each do |name, value|
|
85
|
+
@response_headers[name] = @explanation.response.headers[name] || Parameter.new
|
86
|
+
@response_headers[name].value = value
|
87
|
+
end
|
88
|
+
@explanation.response.headers.each { |name, header| @response_headers[name] ||= header }
|
89
|
+
end
|
90
|
+
|
91
|
+
def process_parameters(input, output, explanation:, prefix: nil)
|
92
|
+
input.each do |key, value|
|
93
|
+
name = prefix ? "#{prefix}[#{key}]" : key
|
94
|
+
if value.is_a?(Hash)
|
95
|
+
process_parameters(value, output, explanation: explanation, prefix: name)
|
96
|
+
else
|
97
|
+
output[name] = explanation[name] || Parameter.new
|
98
|
+
output[name].value = value
|
99
|
+
end
|
100
|
+
end
|
101
|
+
if prefix.nil?
|
102
|
+
explanation.each { |name, parameter| output[name] ||= parameter }
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module RSpec
|
2
|
+
module DocumentRequests
|
3
|
+
module Writers
|
4
|
+
class Base
|
5
|
+
#EXTENSION = ".something"
|
6
|
+
|
7
|
+
def initialize(file)
|
8
|
+
@file = file
|
9
|
+
end
|
10
|
+
|
11
|
+
def breadcrumb(description:, filename:, last:)
|
12
|
+
raise NotImplementedError
|
13
|
+
end
|
14
|
+
|
15
|
+
def title(description)
|
16
|
+
raise NotImplementedError
|
17
|
+
end
|
18
|
+
|
19
|
+
def child(description:, filename:)
|
20
|
+
raise NotImplementedError
|
21
|
+
end
|
22
|
+
|
23
|
+
def request_title(description, missing_levels:)
|
24
|
+
raise NotImplementedError
|
25
|
+
end
|
26
|
+
|
27
|
+
def request(request, missing_levels:)
|
28
|
+
raise NotImplementedError
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
module RSpec
|
2
|
+
module DocumentRequests
|
3
|
+
module Writers
|
4
|
+
class Markdown < Base
|
5
|
+
EXTENSION = ".md"
|
6
|
+
|
7
|
+
def breadcrumb(description:, filename:, last:)
|
8
|
+
@file.write "[#{description}](#{filename})"
|
9
|
+
if not last
|
10
|
+
@file.write " > "
|
11
|
+
else
|
12
|
+
@file.puts
|
13
|
+
@file.puts
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def title(description)
|
18
|
+
@file.puts "# #{description}"
|
19
|
+
@file.puts
|
20
|
+
end
|
21
|
+
|
22
|
+
def child(description:, filename:)
|
23
|
+
@file.puts "* [#{description}](#{filename})"
|
24
|
+
end
|
25
|
+
|
26
|
+
def example_title(description, missing_levels:)
|
27
|
+
@file.puts "## #{missing_levels.map { |l| "#{l} > " }.join} #{description}"
|
28
|
+
@file.puts
|
29
|
+
end
|
30
|
+
|
31
|
+
def request(request, missing_levels:)
|
32
|
+
@file.write <<FILE
|
33
|
+
### Request #{"(#{request.explanation.message})" if request.explanation.message}
|
34
|
+
|
35
|
+
#{request.method} #{request.path}
|
36
|
+
|
37
|
+
FILE
|
38
|
+
|
39
|
+
if request.request_parameters.any?
|
40
|
+
@file.write <<FILE
|
41
|
+
|
42
|
+
#### Parameters
|
43
|
+
|
44
|
+
FILE
|
45
|
+
parameters_table(request.request_parameters)
|
46
|
+
end
|
47
|
+
|
48
|
+
if request.request_headers.any?
|
49
|
+
@file.write <<FILE
|
50
|
+
|
51
|
+
#### Headers
|
52
|
+
|
53
|
+
FILE
|
54
|
+
parameters_table(request.request_headers)
|
55
|
+
end
|
56
|
+
|
57
|
+
@file.write <<FILE
|
58
|
+
|
59
|
+
### Response
|
60
|
+
|
61
|
+
#### Status
|
62
|
+
|
63
|
+
#{request.response.status} #{request.response.status_message}
|
64
|
+
|
65
|
+
FILE
|
66
|
+
|
67
|
+
if request.response_parameters and request.response_parameters.any?
|
68
|
+
@file.write <<FILE
|
69
|
+
#### Parameters
|
70
|
+
|
71
|
+
FILE
|
72
|
+
parameters_table(request.response_parameters)
|
73
|
+
end
|
74
|
+
|
75
|
+
@file.write <<FILE
|
76
|
+
|
77
|
+
#### Body
|
78
|
+
|
79
|
+
#{request.response.body}
|
80
|
+
|
81
|
+
FILE
|
82
|
+
|
83
|
+
if request.response_headers.any?
|
84
|
+
@file.write <<FILE
|
85
|
+
|
86
|
+
#### Headers
|
87
|
+
|
88
|
+
FILE
|
89
|
+
parameters_table(request.response_headers)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
def parameters_table(parameters)
|
96
|
+
@file.write <<FILE
|
97
|
+
| Name | Type | Required? | Value | Explanation |
|
98
|
+
|------|------|-----------|-------|-------------|
|
99
|
+
FILE
|
100
|
+
|
101
|
+
parameters.sort.each do |name, parameter|
|
102
|
+
@file.puts "| #{name} | #{parameter.type} | #{"Required" if parameter.required} | #{parameter.value} | #{parameter.message} |"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'rspec/document_requests/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "rspec-document_requests"
|
8
|
+
spec.version = RSpec::DocumentRequests::VERSION
|
9
|
+
spec.authors = ["Oded Niv"]
|
10
|
+
spec.email = ["oded.niv@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Automatically documents requests generated by RSpec examples.}
|
13
|
+
spec.description = %q{Use this gem to document your API with your specs.}
|
14
|
+
spec.homepage = "https://github.com/odedniv/rspec-document_requests"
|
15
|
+
spec.license = "UNLICENSE"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
18
|
+
spec.bindir = "exe"
|
19
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_runtime_dependency "rspec-rails", ">= 3.0"
|
23
|
+
|
24
|
+
spec.add_development_dependency "bundler", "~> 1.9"
|
25
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
26
|
+
end
|
metadata
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rspec-document_requests
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Oded Niv
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-06-23 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rspec-rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.9'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.9'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
description: Use this gem to document your API with your specs.
|
56
|
+
email:
|
57
|
+
- oded.niv@gmail.com
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- ".gitignore"
|
63
|
+
- ".rspec"
|
64
|
+
- ".travis.yml"
|
65
|
+
- CODE_OF_CONDUCT.md
|
66
|
+
- Gemfile
|
67
|
+
- README.md
|
68
|
+
- Rakefile
|
69
|
+
- UNLICENSE
|
70
|
+
- bin/console
|
71
|
+
- bin/setup
|
72
|
+
- lib/rspec/document_requests.rb
|
73
|
+
- lib/rspec/document_requests/builder.rb
|
74
|
+
- lib/rspec/document_requests/configuration.rb
|
75
|
+
- lib/rspec/document_requests/dsl.rb
|
76
|
+
- lib/rspec/document_requests/explanation.rb
|
77
|
+
- lib/rspec/document_requests/organized_request.rb
|
78
|
+
- lib/rspec/document_requests/request.rb
|
79
|
+
- lib/rspec/document_requests/version.rb
|
80
|
+
- lib/rspec/document_requests/writers.rb
|
81
|
+
- lib/rspec/document_requests/writers/base.rb
|
82
|
+
- lib/rspec/document_requests/writers/markdown.rb
|
83
|
+
- rspec-document_requests.gemspec
|
84
|
+
homepage: https://github.com/odedniv/rspec-document_requests
|
85
|
+
licenses:
|
86
|
+
- UNLICENSE
|
87
|
+
metadata: {}
|
88
|
+
post_install_message:
|
89
|
+
rdoc_options: []
|
90
|
+
require_paths:
|
91
|
+
- lib
|
92
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- - ">="
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
requirements: []
|
103
|
+
rubyforge_project:
|
104
|
+
rubygems_version: 2.4.7
|
105
|
+
signing_key:
|
106
|
+
specification_version: 4
|
107
|
+
summary: Automatically documents requests generated by RSpec examples.
|
108
|
+
test_files: []
|