doc_my_routes 0.9.0
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 +7 -0
- data/etc/css/base.css +285 -0
- data/etc/index.html.erb +164 -0
- data/lib/doc_my_routes.rb +23 -0
- data/lib/doc_my_routes/doc/config.rb +48 -0
- data/lib/doc_my_routes/doc/documentation.rb +69 -0
- data/lib/doc_my_routes/doc/errors.rb +6 -0
- data/lib/doc_my_routes/doc/examples_handler.rb +74 -0
- data/lib/doc_my_routes/doc/mapping.rb +124 -0
- data/lib/doc_my_routes/doc/mixins/annotatable.rb +119 -0
- data/lib/doc_my_routes/doc/route.rb +72 -0
- data/lib/doc_my_routes/doc/route_collection.rb +28 -0
- data/lib/doc_my_routes/doc/route_documentation.rb +78 -0
- data/lib/doc_my_routes/doc/status_code_info.rb +23 -0
- data/lib/doc_my_routes/version.rb +4 -0
- metadata +142 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: bbc3ad0bfc94aa4b5af5ecd1560fb6b2ba7e6e8a
|
4
|
+
data.tar.gz: 2bd15f0a53ac2ae5f0c0377dd409ec286bebf29a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5c9de394718c92d0c895274b492b837a6c337b66ddd372cd93b66bbbd494d4adae634c7ef1c5aaca2221fae2c1f71855044541f3ea48014135d9da69e987a0ba
|
7
|
+
data.tar.gz: 5ba49d62f21405d10a9be69a7bf071bed33f7d5e32bf498a6e3208d960f4eff0c05634a6e9aa4f2d54d7966ca8c00e084e236c32fd7931d86f8444e637b6d9ec
|
data/etc/css/base.css
ADDED
@@ -0,0 +1,285 @@
|
|
1
|
+
/* Hide focus highlightning */
|
2
|
+
:focus
|
3
|
+
{
|
4
|
+
outline:0;
|
5
|
+
}
|
6
|
+
|
7
|
+
body
|
8
|
+
{
|
9
|
+
font-family:Verdana, Geneva, sans-serif;
|
10
|
+
font-size:13px;
|
11
|
+
}
|
12
|
+
|
13
|
+
/* Control the title and description sections */
|
14
|
+
header
|
15
|
+
{
|
16
|
+
border-bottom:1px solid #eee;
|
17
|
+
font-size:1.2em;
|
18
|
+
margin-bottom:1em;
|
19
|
+
padding-bottom:1em;
|
20
|
+
}
|
21
|
+
|
22
|
+
.info_title
|
23
|
+
{
|
24
|
+
font-size:1.2em;
|
25
|
+
font-weight:700;
|
26
|
+
padding:0 0 .5em;
|
27
|
+
}
|
28
|
+
|
29
|
+
section.documentation
|
30
|
+
{
|
31
|
+
padding:1em 10%;
|
32
|
+
width:70%;
|
33
|
+
}
|
34
|
+
|
35
|
+
section.resources
|
36
|
+
{
|
37
|
+
padding:0 0 0 1em;
|
38
|
+
}
|
39
|
+
|
40
|
+
article.resource
|
41
|
+
{
|
42
|
+
border-bottom:1px solid #eee;
|
43
|
+
display:block;
|
44
|
+
margin:0 0 .8em;
|
45
|
+
padding-left:0;
|
46
|
+
}
|
47
|
+
|
48
|
+
article.operation
|
49
|
+
{
|
50
|
+
margin:.8em 0 .8em 1em;
|
51
|
+
}
|
52
|
+
|
53
|
+
article.operation.http_method
|
54
|
+
{
|
55
|
+
text-transform:uppercase;
|
56
|
+
}
|
57
|
+
|
58
|
+
article.operation.get
|
59
|
+
{
|
60
|
+
background-color:#e7f0f7;
|
61
|
+
border:1px solid #c3d9ec;
|
62
|
+
color:#337ab7;
|
63
|
+
}
|
64
|
+
|
65
|
+
article.operation.delete
|
66
|
+
{
|
67
|
+
background-color:#f5e8e8;
|
68
|
+
border:1px solid #e8c6c7;
|
69
|
+
color:#a41e22;
|
70
|
+
}
|
71
|
+
|
72
|
+
article.operation.post
|
73
|
+
{
|
74
|
+
background-color:#e7f6ec;
|
75
|
+
border:1px solid #c3e8d1;
|
76
|
+
color:#10a54a;
|
77
|
+
}
|
78
|
+
|
79
|
+
article.operation.put
|
80
|
+
{
|
81
|
+
background-color:#f9f2e9;
|
82
|
+
border:1px solid #f0e0ca;
|
83
|
+
color:#c5862b;
|
84
|
+
}
|
85
|
+
|
86
|
+
.content.get
|
87
|
+
{
|
88
|
+
border-top:1px solid #c3d9ec;
|
89
|
+
}
|
90
|
+
|
91
|
+
.content.delete
|
92
|
+
{
|
93
|
+
border-top:1px solid #e8c6c7;
|
94
|
+
}
|
95
|
+
|
96
|
+
.content.post
|
97
|
+
{
|
98
|
+
border-top:1px solid #c3e8d1;
|
99
|
+
}
|
100
|
+
|
101
|
+
.content.put
|
102
|
+
{
|
103
|
+
border-top:1px solid #f0e0ca;
|
104
|
+
}
|
105
|
+
|
106
|
+
span.path
|
107
|
+
{
|
108
|
+
color:#000;
|
109
|
+
}
|
110
|
+
|
111
|
+
summary::-webkit-details-marker
|
112
|
+
{
|
113
|
+
display:none;
|
114
|
+
}
|
115
|
+
|
116
|
+
span.http_method
|
117
|
+
{
|
118
|
+
background-color:#337ab7;
|
119
|
+
color:#FFF;
|
120
|
+
display:inline-block;
|
121
|
+
margin-right:1em;
|
122
|
+
text-align: center;
|
123
|
+
padding:.5em;
|
124
|
+
width:7em;
|
125
|
+
}
|
126
|
+
|
127
|
+
span.http_method.get
|
128
|
+
{
|
129
|
+
background-color:#337ab7;
|
130
|
+
}
|
131
|
+
|
132
|
+
span.http_method.delete
|
133
|
+
{
|
134
|
+
background-color:#a41e22;
|
135
|
+
}
|
136
|
+
|
137
|
+
span.http_method.post
|
138
|
+
{
|
139
|
+
background-color:#10a54a;
|
140
|
+
}
|
141
|
+
|
142
|
+
span.http_method.put
|
143
|
+
{
|
144
|
+
background-color:#c5862b;
|
145
|
+
}
|
146
|
+
|
147
|
+
div.content
|
148
|
+
{
|
149
|
+
line-height:1em;
|
150
|
+
padding:0 .5em 1em;
|
151
|
+
}
|
152
|
+
|
153
|
+
span.summary
|
154
|
+
{
|
155
|
+
display:inline-block;
|
156
|
+
float:right;
|
157
|
+
padding:.5em;
|
158
|
+
text-align:right;
|
159
|
+
}
|
160
|
+
|
161
|
+
span.summary.get
|
162
|
+
{
|
163
|
+
color:#337ab7;
|
164
|
+
}
|
165
|
+
|
166
|
+
span.summary.delete
|
167
|
+
{
|
168
|
+
color:#a41e22;
|
169
|
+
}
|
170
|
+
|
171
|
+
span.summary.post
|
172
|
+
{
|
173
|
+
color:#10a54a;
|
174
|
+
}
|
175
|
+
|
176
|
+
span.summary.put
|
177
|
+
{
|
178
|
+
color:#c5862b;
|
179
|
+
}
|
180
|
+
|
181
|
+
.code
|
182
|
+
{
|
183
|
+
padding:0 0 0 1em;
|
184
|
+
}
|
185
|
+
|
186
|
+
.code.get
|
187
|
+
{
|
188
|
+
background-color: #D1DFEB;
|
189
|
+
border: 1px solid #B2C8DB;
|
190
|
+
}
|
191
|
+
|
192
|
+
.code.put
|
193
|
+
{
|
194
|
+
background-color:#EDDBC0;
|
195
|
+
border:1px solid #B2C8DB;
|
196
|
+
}
|
197
|
+
|
198
|
+
.code.delete
|
199
|
+
{
|
200
|
+
background-color:#EDD3D4;
|
201
|
+
border:1px solid #D99193;
|
202
|
+
}
|
203
|
+
|
204
|
+
.code.post
|
205
|
+
{
|
206
|
+
background-color:#C9F0D9;
|
207
|
+
border:1px solid #A8E3BE;
|
208
|
+
}
|
209
|
+
|
210
|
+
/* Ensure the content remains boxed when shrinked */
|
211
|
+
summary {
|
212
|
+
overflow: hidden;
|
213
|
+
}
|
214
|
+
|
215
|
+
summary.resource
|
216
|
+
{
|
217
|
+
color:#555;
|
218
|
+
font-size:1.2em;
|
219
|
+
font-weight:400;
|
220
|
+
}
|
221
|
+
|
222
|
+
/* Character to prepend to list of examples */
|
223
|
+
summary.example::before
|
224
|
+
{
|
225
|
+
content:'› ';
|
226
|
+
}
|
227
|
+
|
228
|
+
summary.example {
|
229
|
+
cursor: pointer;
|
230
|
+
}
|
231
|
+
|
232
|
+
p
|
233
|
+
{
|
234
|
+
color:#000;
|
235
|
+
}
|
236
|
+
|
237
|
+
td
|
238
|
+
{
|
239
|
+
border-top:1px solid #eee;
|
240
|
+
color:#000;
|
241
|
+
}
|
242
|
+
|
243
|
+
table
|
244
|
+
{
|
245
|
+
border-collapse:collapse;
|
246
|
+
}
|
247
|
+
|
248
|
+
table td
|
249
|
+
{
|
250
|
+
border-top:1px solid #ccc;
|
251
|
+
padding:.5em .5em .5em 0;
|
252
|
+
}
|
253
|
+
|
254
|
+
thead
|
255
|
+
{
|
256
|
+
border-bottom:1px solid #eee;
|
257
|
+
color:#888;
|
258
|
+
}
|
259
|
+
|
260
|
+
tbody
|
261
|
+
{
|
262
|
+
border-top:1px solid #eee;
|
263
|
+
}
|
264
|
+
|
265
|
+
th,td
|
266
|
+
{
|
267
|
+
padding:.4em 1em .5em 0;
|
268
|
+
text-align:left;
|
269
|
+
}
|
270
|
+
|
271
|
+
pre
|
272
|
+
{
|
273
|
+
font-family:"Anonymous Pro", Menlo, Consolas, "Bitstream Vera Sans Mono", "Courier New", monospace;
|
274
|
+
}
|
275
|
+
|
276
|
+
article.example
|
277
|
+
{
|
278
|
+
padding:0 0 .5em;
|
279
|
+
}
|
280
|
+
|
281
|
+
.example_content
|
282
|
+
{
|
283
|
+
color:#555;
|
284
|
+
margin:1em;
|
285
|
+
}
|
data/etc/index.html.erb
ADDED
@@ -0,0 +1,164 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<meta charset="utf-8">
|
5
|
+
<title><%= data[:main][:info][:title] %></title>
|
6
|
+
<link href='<%= DocMyRoutes.config.destination_css %>' rel='stylesheet' type='text/css'/>
|
7
|
+
</head>
|
8
|
+
<body>
|
9
|
+
<section class="documentation">
|
10
|
+
<header>
|
11
|
+
<div id="api_info" class="info">
|
12
|
+
<div class="info_title"><%= data[:main][:info][:title] %></div>
|
13
|
+
<div class="info_description"><%= data[:main][:info][:description] %></div>
|
14
|
+
</div>
|
15
|
+
</header>
|
16
|
+
|
17
|
+
<section class="resources">
|
18
|
+
<%# API documentation content %>
|
19
|
+
<%
|
20
|
+
data[:main][:apis].each do |resource_name, operations|
|
21
|
+
%>
|
22
|
+
<article class="resource">
|
23
|
+
<details open>
|
24
|
+
<summary class="resource"><%= resource_name %></summary>
|
25
|
+
<%
|
26
|
+
operations.sort_by { |route| route[:http_method] }.each do |route|
|
27
|
+
verb = route[:http_method]
|
28
|
+
%>
|
29
|
+
<article class="operation <%= verb.downcase %>">
|
30
|
+
<details>
|
31
|
+
<summary class="operation">
|
32
|
+
<span class="http_method <%= verb.downcase %>">
|
33
|
+
<%= verb %><%= ' / HEAD' if verb == 'GET' %>
|
34
|
+
</span>
|
35
|
+
<span class="path <%= verb.downcase %>">
|
36
|
+
<%= route[:path]%>
|
37
|
+
</span>
|
38
|
+
<span class="summary <%= verb.downcase %>">
|
39
|
+
<%= route[:summary]%>
|
40
|
+
</span>
|
41
|
+
</summary>
|
42
|
+
<div class="content <%= verb.downcase %>">
|
43
|
+
<% unless route[:produces].empty? %>
|
44
|
+
<h4>Content Type</h4>
|
45
|
+
<p><%= route[:produces].join(', ') %></p>
|
46
|
+
<% end %>
|
47
|
+
|
48
|
+
<% if route[:notes] && !route[:notes].empty? %>
|
49
|
+
<h4>Implementation Notes</h4>
|
50
|
+
<p><%= route[:notes] %></p>
|
51
|
+
<% end %>
|
52
|
+
|
53
|
+
<%# List of , if present %>
|
54
|
+
<% if route[:parameters] && !route[:parameters].empty? %>
|
55
|
+
<table class="smallwidth">
|
56
|
+
<thead>
|
57
|
+
<tr>
|
58
|
+
<th>Parameter</th>
|
59
|
+
</tr>
|
60
|
+
</thead>
|
61
|
+
<tbody class="operation-params">
|
62
|
+
<% route[:parameters].each do |param| %>
|
63
|
+
<tr>
|
64
|
+
<td><%= param %></td>
|
65
|
+
</tr>
|
66
|
+
<% end %>
|
67
|
+
</tbody>
|
68
|
+
</table>
|
69
|
+
<% end %>
|
70
|
+
|
71
|
+
<%# List of possible response statuses %>
|
72
|
+
<h4>Response statuses</h4>
|
73
|
+
<table class="smallwidth">
|
74
|
+
<thead>
|
75
|
+
<tr>
|
76
|
+
<th class="status_code">HTTP Status Code</th>
|
77
|
+
<th class="status_code_message">Reason</th>
|
78
|
+
</tr>
|
79
|
+
</thead>
|
80
|
+
<tbody class="operation-status">
|
81
|
+
<% route[:status_codes].each do |status_code, description| %>
|
82
|
+
<tr>
|
83
|
+
<td class="status_code"><%= status_code %></td>
|
84
|
+
<td class="status_code_message"><%= description %></td>
|
85
|
+
</tr>
|
86
|
+
<% end %>
|
87
|
+
</tbody>
|
88
|
+
</table>
|
89
|
+
|
90
|
+
<%# Display examples, if present %>
|
91
|
+
<% if route[:examples] %>
|
92
|
+
<h4>Examples</h4>
|
93
|
+
<div>
|
94
|
+
<%# TODO: move this logic outside %>
|
95
|
+
<% route[:examples].each do |example| %>
|
96
|
+
<article class="example <%= verb.downcase %>">
|
97
|
+
<details>
|
98
|
+
<summary class="example <%= verb.downcase %>"><%= example['description'] %></summary>
|
99
|
+
<div class="example_content <%= verb.downcase %>">
|
100
|
+
<div class="request">
|
101
|
+
<h4>Request</h4>
|
102
|
+
<div class="code <%= verb.downcase %>">
|
103
|
+
<div>
|
104
|
+
<h5>Query</h5>
|
105
|
+
<pre class='request-code query'><code class="json"><%= example['request']['query'] %></code></pre>
|
106
|
+
</div>
|
107
|
+
|
108
|
+
<% if example['request']['headers'] %>
|
109
|
+
<div>
|
110
|
+
<h5>Headers</h5>
|
111
|
+
<% example['request']['headers'].each do |key, value| %>
|
112
|
+
<pre class="request-code headers"><code class="json"><%= key %>: <%= value %></code></pre>
|
113
|
+
<% end %>
|
114
|
+
</div>
|
115
|
+
<% end %>
|
116
|
+
|
117
|
+
<% if example['request']['body'] %>
|
118
|
+
<div>
|
119
|
+
<h5>Body</h5>
|
120
|
+
<pre class='request-code body'><code class="json"><%= example['request']['body'] %></code></pre>
|
121
|
+
</div>
|
122
|
+
<% end %>
|
123
|
+
</div>
|
124
|
+
</div>
|
125
|
+
|
126
|
+
<div class="response">
|
127
|
+
<h4>Response (status <%= example['response']['status'] %>)</h4>
|
128
|
+
<div class="code <%= verb.downcase %>">
|
129
|
+
<% if example['response']['headers'] %>
|
130
|
+
<div>
|
131
|
+
<h5>Headers</h5>
|
132
|
+
<% example['response']['headers'].each do |key, value| %>
|
133
|
+
<pre class="request-code headers"><code class="json"><%= key %>: <%= value %></code></pre>
|
134
|
+
<% end %>
|
135
|
+
</div>
|
136
|
+
<% end %>
|
137
|
+
|
138
|
+
<div>
|
139
|
+
<h5>Body</h5>
|
140
|
+
<% if example['response']['body'] %>
|
141
|
+
<pre class='request-code body'><code class="json"><%= example['response']['body'] %></code></pre>
|
142
|
+
<% else %>
|
143
|
+
<pre class='request-code body'><code class="json">No body</code></pre>
|
144
|
+
<% end %>
|
145
|
+
</div>
|
146
|
+
</div>
|
147
|
+
</div>
|
148
|
+
</div>
|
149
|
+
</details>
|
150
|
+
</article>
|
151
|
+
<% end %>
|
152
|
+
</div>
|
153
|
+
<% end %>
|
154
|
+
</div>
|
155
|
+
</details>
|
156
|
+
</article>
|
157
|
+
<% end %>
|
158
|
+
</details>
|
159
|
+
</article>
|
160
|
+
<% end %>
|
161
|
+
</section>
|
162
|
+
</section>
|
163
|
+
</body>
|
164
|
+
</html>
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'doc_my_routes/version'
|
2
|
+
require 'doc_my_routes/doc/errors'
|
3
|
+
require 'doc_my_routes/doc/documentation'
|
4
|
+
require 'doc_my_routes/doc/config'
|
5
|
+
require 'doc_my_routes/doc/mixins/annotatable'
|
6
|
+
|
7
|
+
# General module that defines the base access to DocMyRoutes
|
8
|
+
module DocMyRoutes
|
9
|
+
class << self
|
10
|
+
# Expose logging hook
|
11
|
+
attr_writer :logger
|
12
|
+
attr_accessor :config
|
13
|
+
|
14
|
+
def logger
|
15
|
+
@logger ||= begin
|
16
|
+
require 'logger'
|
17
|
+
Logger.new($stdout).tap do |log|
|
18
|
+
log.progname = name
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# Expose config hook
|
2
|
+
module DocMyRoutes
|
3
|
+
class << self
|
4
|
+
attr_accessor :config
|
5
|
+
|
6
|
+
# Nothing fancy here, just gem configuration inspired by many gems
|
7
|
+
# e.g., https://robots.thoughtbot.com/mygem-configure-block
|
8
|
+
def configure
|
9
|
+
self.config ||= Config.new
|
10
|
+
yield(config)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Inner class to maintain configuration settings
|
14
|
+
class Config
|
15
|
+
attr_accessor :title, # Project title
|
16
|
+
:description, # Project description
|
17
|
+
:destination_dir, # Where to store the documentation
|
18
|
+
:css_file_path, # Path to look for a CSS file
|
19
|
+
:examples_path_regexp # Path regexp to example files
|
20
|
+
attr_reader :index_template_file # Template used for the index.html
|
21
|
+
|
22
|
+
def initialize
|
23
|
+
@title = @description = @examples_path_regexp = nil
|
24
|
+
|
25
|
+
@destination_dir = File.join(Dir.pwd, 'doc', 'api')
|
26
|
+
|
27
|
+
default_static_path = File.join(File.dirname(__FILE__), '..', '..',
|
28
|
+
'..', 'etc')
|
29
|
+
@css_file_path = File.join(default_static_path, 'css', 'base.css')
|
30
|
+
@index_template_file = File.join(default_static_path, 'index.html.erb')
|
31
|
+
end
|
32
|
+
|
33
|
+
def examples
|
34
|
+
@examples_path_regexp.nil? ? [] : Dir.glob(@examples_path_regexp)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Calculate the relative path of the CSS used
|
38
|
+
def destination_css
|
39
|
+
# TODO: make it more robust
|
40
|
+
File.basename(@css_file_path)
|
41
|
+
end
|
42
|
+
|
43
|
+
def index_file
|
44
|
+
File.join(@destination_dir, 'index.html')
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require_relative 'status_code_info'
|
2
|
+
require_relative 'examples_handler'
|
3
|
+
require_relative '../version'
|
4
|
+
require 'ostruct'
|
5
|
+
|
6
|
+
module DocMyRoutes
|
7
|
+
# class which contains the main functions to generate documentation
|
8
|
+
class Documentation
|
9
|
+
attr_reader :routes
|
10
|
+
|
11
|
+
def self.generate
|
12
|
+
Documentation.new(RouteCollection.routes, DocMyRoutes.config).generate
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(routes, config)
|
16
|
+
@routes = routes
|
17
|
+
@config = config
|
18
|
+
end
|
19
|
+
|
20
|
+
def generate
|
21
|
+
generate_content
|
22
|
+
generate_html
|
23
|
+
copy_css_files
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def resource_name(resource)
|
29
|
+
resource.to_s.split('::').last.downcase
|
30
|
+
end
|
31
|
+
|
32
|
+
def generate_content
|
33
|
+
routes.each do |resource, rts|
|
34
|
+
content[:main][:apis][resource_name(resource)] = rts.map(&:to_hash)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def copy_css_files
|
39
|
+
FileUtils.cp_r(@config.css_file_path,
|
40
|
+
@config.destination_dir)
|
41
|
+
end
|
42
|
+
|
43
|
+
def content
|
44
|
+
# Set the content initial structure and default values
|
45
|
+
@content ||= {
|
46
|
+
main: {
|
47
|
+
apiVersion: VERSION,
|
48
|
+
info: {
|
49
|
+
title: @config.title,
|
50
|
+
description: @config.description
|
51
|
+
},
|
52
|
+
apis: {}
|
53
|
+
}
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
def generate_html
|
58
|
+
doc_binding = OpenStruct.new(data: content)
|
59
|
+
.instance_eval { binding }
|
60
|
+
index_file = @config.index_file
|
61
|
+
File.open(index_file, 'w') do |f|
|
62
|
+
template_file = File.read(@config.index_template_file)
|
63
|
+
content = ERB.new(template_file, 0, '<>').result(doc_binding)
|
64
|
+
f.write content
|
65
|
+
end
|
66
|
+
DocMyRoutes.logger.info "Generated HTML file to #{index_file}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# Encoding: UTF-8
|
2
|
+
|
3
|
+
module DocMyRoutes
|
4
|
+
# Base class to parse examples
|
5
|
+
#
|
6
|
+
# Examples are expected to be in YAML format, see README.md for details
|
7
|
+
class ExamplesHandler
|
8
|
+
class << self
|
9
|
+
# Load json examples generated by tests
|
10
|
+
def routes_examples
|
11
|
+
@routes_examples ||= begin
|
12
|
+
examples = {}
|
13
|
+
|
14
|
+
DocMyRoutes.config.examples.each do |example_file|
|
15
|
+
data = parse_example(example_file)
|
16
|
+
(examples[data['name']] ||= []) << data
|
17
|
+
end
|
18
|
+
|
19
|
+
examples
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def parse_example(example_file)
|
24
|
+
data = YAML.load_file(example_file)
|
25
|
+
|
26
|
+
# Validate that mandatory parameters are present
|
27
|
+
fail_unless_present(%w(name description request response action),
|
28
|
+
data)
|
29
|
+
|
30
|
+
{
|
31
|
+
'name' => data['name'],
|
32
|
+
'description' => data['description'],
|
33
|
+
'request' => parse_request(data),
|
34
|
+
'response' => parse_response(data)
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def fail_unless_present(*fields, data)
|
41
|
+
fields.flatten.each do |field|
|
42
|
+
fail "An example doesn't have the required field #{field}: #{data}" \
|
43
|
+
unless data[field]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def parse_request(data)
|
48
|
+
request = {}
|
49
|
+
|
50
|
+
request['query'] = "#{data['action']}"
|
51
|
+
request['query'] += "?#{data['request']['params']}" \
|
52
|
+
unless data['request']['params'].empty?
|
53
|
+
|
54
|
+
request['headers'] = data['request']['headers'] \
|
55
|
+
unless data['request']['headers'].empty?
|
56
|
+
request['body'] = data['request']['body'] \
|
57
|
+
if data['request']['body']
|
58
|
+
request
|
59
|
+
end
|
60
|
+
|
61
|
+
def parse_response(data)
|
62
|
+
data_response = data['response']
|
63
|
+
fail_unless_present(%w(headers status), data_response)
|
64
|
+
|
65
|
+
response = {}
|
66
|
+
response['body'] = data_response['body'] \
|
67
|
+
unless data_response['body'].empty?
|
68
|
+
response['headers'] = data_response['headers']
|
69
|
+
response['status'] = data_response['status']
|
70
|
+
response
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module DocMyRoutes
|
4
|
+
# Class that maintain information about the route mapping, as extracted
|
5
|
+
# from the actual application
|
6
|
+
#
|
7
|
+
# Sinatra applications define routes and can be mapped on different
|
8
|
+
# namespaces as happens with normal Rack applications.
|
9
|
+
#
|
10
|
+
# That means that given an application A that provides:
|
11
|
+
# - GET /my_route
|
12
|
+
#
|
13
|
+
# its actual route might become /my_application/my_route using for instance:
|
14
|
+
#
|
15
|
+
# Rack::Builder.app do
|
16
|
+
# run Rack::URLMap.new ('my_application' => A)
|
17
|
+
# end
|
18
|
+
class Mapping
|
19
|
+
class << self
|
20
|
+
attr_reader :route_mapping
|
21
|
+
|
22
|
+
def extract_mapping(mapping)
|
23
|
+
@route_mapping = {}
|
24
|
+
mapping.each do |_, location, _, app|
|
25
|
+
klass = app.class
|
26
|
+
klass = app.instance_variable_get('@instance').class \
|
27
|
+
if klass == Sinatra::Wrapper
|
28
|
+
|
29
|
+
(@route_mapping[klass.to_s] ||= []) << location
|
30
|
+
end
|
31
|
+
|
32
|
+
assign_namespace
|
33
|
+
end
|
34
|
+
|
35
|
+
def mapping_used?
|
36
|
+
Object.const_defined?('Rack::URLMap')
|
37
|
+
end
|
38
|
+
|
39
|
+
# This method associates to each route its namespace, if detected.
|
40
|
+
#
|
41
|
+
# Note: when application A is inherited by B and only B is mapped, from
|
42
|
+
# the point of view of the mapping only B is defined.
|
43
|
+
#
|
44
|
+
# Technically speaking, this is absolutely correct because B is the
|
45
|
+
# actual application that's registered and used (B provides A's methods).
|
46
|
+
#
|
47
|
+
# This method duplicates routes for applications that are not mapped in
|
48
|
+
# order to list their routes among the ones of the resources that
|
49
|
+
# inherit from them
|
50
|
+
def assign_namespace
|
51
|
+
RouteCollection.routes.each do |class_name, app_routes|
|
52
|
+
# TODO: deal with multiple locations for multi mapping
|
53
|
+
if route_mapping.include?(class_name)
|
54
|
+
app_routes.each do |route|
|
55
|
+
route.namespace = @route_mapping[class_name].first
|
56
|
+
end
|
57
|
+
else
|
58
|
+
remap_resource(class_name)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def remapped_applications
|
64
|
+
@remapped_applications ||= Hash.new { |hash, key| hash[key] = [] }
|
65
|
+
end
|
66
|
+
|
67
|
+
def remap_resource(class_name)
|
68
|
+
DocMyRoutes.logger.debug 'Remapping routes for not mapped ' \
|
69
|
+
"resource #{class_name}"
|
70
|
+
|
71
|
+
find_child_apps(class_name).each do |child, location|
|
72
|
+
DocMyRoutes.logger.debug " - Remapping to #{child}"
|
73
|
+
remapped_applications[class_name] << child
|
74
|
+
|
75
|
+
RouteCollection.routes[class_name].each do |route|
|
76
|
+
# TODO: If an application has multiple namespaces, we should
|
77
|
+
# keep a list of aliases
|
78
|
+
route.namespace = location.first
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Returns the mapped application(s) that inherited from a given class
|
84
|
+
def find_child_apps(class_name)
|
85
|
+
klass = Object.const_get(class_name)
|
86
|
+
route_mapping.select do |mapped_app, _|
|
87
|
+
Object.const_get(mapped_app).ancestors.include?(klass)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def mount_point_for_resource(resource)
|
92
|
+
class_name = resource.to_s
|
93
|
+
unless route_mapping
|
94
|
+
DocMyRoutes.logger.debug 'URLMap not used for resource ' \
|
95
|
+
"#{class_name}, assuming it's not namespaced"
|
96
|
+
return '/'
|
97
|
+
end
|
98
|
+
|
99
|
+
# TODO: support multiple application inheriting
|
100
|
+
class_name = remapped_applications[class_name].first if \
|
101
|
+
remapped_applications.key?(class_name)
|
102
|
+
|
103
|
+
locations = route_mapping[class_name]
|
104
|
+
|
105
|
+
validate_locations(class_name, locations)
|
106
|
+
end
|
107
|
+
|
108
|
+
# Detects if multiple locations are available and for now fail
|
109
|
+
def validate_locations(resource, locations)
|
110
|
+
fail "Resource #{resource} has multiple mappings, but that's not " \
|
111
|
+
"supported yet: #{locations}" if locations.size > 1
|
112
|
+
|
113
|
+
return locations.first if locations.size == 1
|
114
|
+
|
115
|
+
DocMyRoutes.logger.debug 'Unable to extract mapping for resource ' \
|
116
|
+
"#{resource}, it's not mapped! This is not " \
|
117
|
+
'necessarily a bug and might happen ' \
|
118
|
+
"because #{resource} is inherited and its " \
|
119
|
+
'children are mapped.'
|
120
|
+
nil
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# Encoding: UTF-8
|
2
|
+
|
3
|
+
require_relative '../route_collection'
|
4
|
+
require_relative '../route_documentation'
|
5
|
+
require_relative '../mapping'
|
6
|
+
|
7
|
+
module DocMyRoutes
|
8
|
+
# Logic to help with the REST API documentation.
|
9
|
+
# This module provides methods to "decorate" sinatra routes as follows:
|
10
|
+
# summary 'Short definition of the route'
|
11
|
+
# notes 'More detailed explanation of the operation'
|
12
|
+
# status_codes [200, 401, 500]
|
13
|
+
# get '/api/example' do
|
14
|
+
# end
|
15
|
+
module Annotatable
|
16
|
+
class << self
|
17
|
+
# When a class is extended with this module documentation specific
|
18
|
+
# features are enabled
|
19
|
+
def extended(mod)
|
20
|
+
# Wrap sinatra's route method to register the defined routes
|
21
|
+
mod.define_singleton_method(:route) do
|
22
|
+
|verb, route_pattern, conditions = {}, &block|
|
23
|
+
result = super(verb, route_pattern, conditions, &block)
|
24
|
+
track_route(self, verb, route_pattern, conditions)
|
25
|
+
result
|
26
|
+
end
|
27
|
+
|
28
|
+
extract_url_map if Mapping.mapping_used?
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
# Wrap Rack::URLMap to extract the actual mapping when URLMap is used
|
34
|
+
def extract_url_map
|
35
|
+
DocMyRoutes.logger.debug 'Wrapping Rack::URLMap to extract mapping'
|
36
|
+
|
37
|
+
Rack::URLMap.send(:define_method, :initialize) do |map = {}|
|
38
|
+
mapping = remap(map)
|
39
|
+
|
40
|
+
fail 'Used Rack::URLMap, but unable to get a mapping' unless mapping
|
41
|
+
|
42
|
+
# Extract mapping for every class
|
43
|
+
#
|
44
|
+
# Note that the same app could be mapped to different endpoints,
|
45
|
+
# that's why the mapping is instance -> [location...]
|
46
|
+
DocMyRoutes::Mapping.extract_mapping(mapping)
|
47
|
+
|
48
|
+
mapping
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def route_documentation
|
54
|
+
@route_documentation ||= begin
|
55
|
+
DocMyRoutes.logger.debug 'Tracking new route'
|
56
|
+
RouteDocumentation.new
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def no_doc
|
61
|
+
route_documentation.hidden = true
|
62
|
+
end
|
63
|
+
|
64
|
+
def produces(*value)
|
65
|
+
route_documentation.produces = value
|
66
|
+
end
|
67
|
+
|
68
|
+
def summary(value)
|
69
|
+
route_documentation.summary = value
|
70
|
+
end
|
71
|
+
|
72
|
+
def notes(value)
|
73
|
+
route_documentation.notes = value
|
74
|
+
end
|
75
|
+
|
76
|
+
def status_codes(value)
|
77
|
+
route_documentation.status_codes = value
|
78
|
+
end
|
79
|
+
|
80
|
+
# Match interaction examples to this route
|
81
|
+
def examples_regex(value)
|
82
|
+
route_documentation.examples_regex = value
|
83
|
+
end
|
84
|
+
|
85
|
+
# It's possible to provide the route notes using a file to avoid adding
|
86
|
+
# too much text in the route definition
|
87
|
+
def notes_ref(value)
|
88
|
+
route_documentation.notes_ref = value
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def track_route(resource, verb, route_pattern, conditions)
|
94
|
+
unless route_documentation.hidden? || skip_route(verb)
|
95
|
+
route = Route.new(resource, verb, route_pattern, conditions,
|
96
|
+
route_documentation)
|
97
|
+
|
98
|
+
DocMyRoutes::RouteCollection << route
|
99
|
+
end
|
100
|
+
|
101
|
+
# Ensure values are reset
|
102
|
+
reset_doc_values
|
103
|
+
end
|
104
|
+
|
105
|
+
# The summary, notes and status codes are only valid for one route
|
106
|
+
# therefore they should be reset before the next route is processed
|
107
|
+
def reset_doc_values
|
108
|
+
@route_documentation = nil
|
109
|
+
@skip_route = false
|
110
|
+
end
|
111
|
+
|
112
|
+
# Sinatra duplicates the GET route creating an extra HEAD route
|
113
|
+
# The HEAD route is often not decorated with summary, notes and status code
|
114
|
+
# so this method gets those values from the previously defined GET route
|
115
|
+
def skip_route(verb)
|
116
|
+
verb == 'HEAD' && !route_documentation.present?
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module DocMyRoutes
|
6
|
+
# Simple object representing a route
|
7
|
+
class Route
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
attr_accessor :resource, :verb, :route_pattern, :conditions
|
11
|
+
attr_reader :namespace, :documentation
|
12
|
+
|
13
|
+
def_delegators :documentation, :has_docs?
|
14
|
+
|
15
|
+
def initialize(resource, verb, route_pattern, conditions, documentation)
|
16
|
+
@resource = resource
|
17
|
+
@verb = verb
|
18
|
+
@route_pattern = route_pattern
|
19
|
+
# TODO: We could inherit this from the application mapping
|
20
|
+
@namespace = nil
|
21
|
+
@conditions = conditions
|
22
|
+
@documentation = documentation
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_hash
|
26
|
+
{
|
27
|
+
http_method: verb,
|
28
|
+
parameters: param_info,
|
29
|
+
path: path
|
30
|
+
}.merge(documentation.to_hash)
|
31
|
+
end
|
32
|
+
|
33
|
+
def path
|
34
|
+
@path ||= begin
|
35
|
+
path = Mapping.mount_point_for_resource(resource) + route_pattern
|
36
|
+
# Changing double slashes into single slash
|
37
|
+
# Removing the trailing ?
|
38
|
+
# Removing the trailing / only if it's not the only character
|
39
|
+
# FROM /api/nodes/:id/? TO /api/nodes/:id
|
40
|
+
# Change the path variable from Ruby style into brackets style
|
41
|
+
# from /api/nodes/:id/ TO /api/nodes/{id}/
|
42
|
+
path.gsub(%r{//}, '/')
|
43
|
+
.gsub(/(\?+)*$/, '')
|
44
|
+
.gsub(%r{(.+)\/$}, '\\1')
|
45
|
+
.gsub(/:(?<path_var>\w+)/, '{\k<path_var>}')
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def namespace=(value)
|
50
|
+
fail MultipleMappingDetected, "Multiple namespaces detected for #{self}"\
|
51
|
+
'and not supported yet' if namespace && value != namespace
|
52
|
+
@namespace = value
|
53
|
+
end
|
54
|
+
|
55
|
+
def to_s
|
56
|
+
"#{verb} #{namespace}#{route_pattern} #{conditions}"
|
57
|
+
end
|
58
|
+
|
59
|
+
# Return a list of parameters required by this route, if specified.
|
60
|
+
#
|
61
|
+
# Try to extract parameters from the route definition otherwise
|
62
|
+
def param_info
|
63
|
+
if conditions[:parameters]
|
64
|
+
conditions[:parameters]
|
65
|
+
else
|
66
|
+
route_pattern.split('/').map do |part|
|
67
|
+
part.start_with?(':') ? part[1..-1].to_sym : nil
|
68
|
+
end.compact
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require_relative 'route'
|
3
|
+
require_relative 'documentation'
|
4
|
+
|
5
|
+
module DocMyRoutes
|
6
|
+
# Simple object representing all the sinatra routes
|
7
|
+
class RouteCollection
|
8
|
+
class << self
|
9
|
+
def routes
|
10
|
+
@routes ||= {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def <<(route)
|
14
|
+
(routes[route.resource.to_s] ||= []) << route
|
15
|
+
end
|
16
|
+
|
17
|
+
def log_routes
|
18
|
+
routes.sort_by { |name, _| name }.each do |app_name, app_routes|
|
19
|
+
# TODO: move namespace on app?
|
20
|
+
namespace = format('%-50s', app_routes.first.namespace)
|
21
|
+
DocMyRoutes.logger.debug "Adding route to #{namespace} - #{app_name}"
|
22
|
+
|
23
|
+
app_routes.each { |rte| logger.debug " - #{rte}" }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module DocMyRoutes
|
2
|
+
# Class holding documentation information about a given route
|
3
|
+
class RouteDocumentation
|
4
|
+
attr_accessor :summary, :notes, :status_codes, :examples_regex, :hidden,
|
5
|
+
:produces, :notes_ref
|
6
|
+
attr_reader :examples
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@status_codes = { 200 => DocMyRoutes::StatusCodeInfo::STATUS_CODES[200] }
|
10
|
+
@hidden = false
|
11
|
+
@produces = []
|
12
|
+
end
|
13
|
+
|
14
|
+
# A route documentation object MUST have a summary, otherwise is not
|
15
|
+
# considered documented
|
16
|
+
def present?
|
17
|
+
!summary.nil?
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_hash
|
21
|
+
{
|
22
|
+
summary: summary,
|
23
|
+
notes: notes,
|
24
|
+
status_codes: status_codes,
|
25
|
+
examples_regex: examples_regex,
|
26
|
+
produces: produces,
|
27
|
+
examples: examples,
|
28
|
+
hidden: hidden?
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
def produces=(values)
|
33
|
+
@produces = values.flatten.compact
|
34
|
+
end
|
35
|
+
|
36
|
+
def status_codes=(route_status_codes)
|
37
|
+
@status_codes = Hash[route_status_codes.map do |code|
|
38
|
+
[code, DocMyRoutes::StatusCodeInfo::STATUS_CODES[code]]
|
39
|
+
end]
|
40
|
+
end
|
41
|
+
|
42
|
+
def examples
|
43
|
+
@example ||= begin
|
44
|
+
return unless @examples_regex
|
45
|
+
|
46
|
+
examples = ExamplesHandler.routes_examples.values.flatten
|
47
|
+
examples = examples.select { |ex| ex['name'] =~ @examples_regex }
|
48
|
+
|
49
|
+
fail ExampleMissing, 'Unable to find examples matching regexp: ' \
|
50
|
+
"#{@examples_regex} for route '#{summary}'" \
|
51
|
+
if examples.empty?
|
52
|
+
examples.flatten
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Match available examples to the filter for the current route
|
57
|
+
def examples_regex=(value)
|
58
|
+
@examples_regex = Regexp.new(value)
|
59
|
+
end
|
60
|
+
|
61
|
+
def notes
|
62
|
+
@notes ||= begin
|
63
|
+
return unless @notes_ref
|
64
|
+
|
65
|
+
expanded_path = File.expand_path(@notes_ref)
|
66
|
+
fail ScriptError, "Notes file not found: #{@notes_ref}" \
|
67
|
+
if @notes_ref.nil? || !File.exist?(expanded_path)
|
68
|
+
|
69
|
+
notes_content = File.read(expanded_path)
|
70
|
+
notes_content.gsub(/\n/, ' ')
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def hidden?
|
75
|
+
!!@hidden
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module DocMyRoutes
|
4
|
+
# Mapping from HTTP status codes to human readable description
|
5
|
+
module StatusCodeInfo
|
6
|
+
STATUS_CODES = {
|
7
|
+
200 => 'OK',
|
8
|
+
201 => 'Created',
|
9
|
+
202 => 'Accepted',
|
10
|
+
204 => 'No Content',
|
11
|
+
303 => 'See Other',
|
12
|
+
304 => 'Not modified',
|
13
|
+
400 => 'Bad Request',
|
14
|
+
401 => 'Unauthorized',
|
15
|
+
403 => 'Forbidden',
|
16
|
+
404 => 'Not Found',
|
17
|
+
410 => 'Gone',
|
18
|
+
409 => 'Conflict',
|
19
|
+
500 => 'Internal Server Error',
|
20
|
+
503 => 'Service Unavailable'
|
21
|
+
}
|
22
|
+
end
|
23
|
+
end
|
metadata
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: doc_my_routes
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.9.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Workday, Ltd.
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-11-23 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.10'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.10'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 3.0.0
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 3.0.0
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rack-test
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.6.2
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.6.2
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rubocop
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0.16'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0.16'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: sinatra
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
description: DocMyRoutes provides a way to annotate Sinatra routes and generate documentation
|
98
|
+
email:
|
99
|
+
- prd.eng.os@workday.com
|
100
|
+
executables: []
|
101
|
+
extensions: []
|
102
|
+
extra_rdoc_files: []
|
103
|
+
files:
|
104
|
+
- etc/css/base.css
|
105
|
+
- etc/index.html.erb
|
106
|
+
- lib/doc_my_routes.rb
|
107
|
+
- lib/doc_my_routes/doc/config.rb
|
108
|
+
- lib/doc_my_routes/doc/documentation.rb
|
109
|
+
- lib/doc_my_routes/doc/errors.rb
|
110
|
+
- lib/doc_my_routes/doc/examples_handler.rb
|
111
|
+
- lib/doc_my_routes/doc/mapping.rb
|
112
|
+
- lib/doc_my_routes/doc/mixins/annotatable.rb
|
113
|
+
- lib/doc_my_routes/doc/route.rb
|
114
|
+
- lib/doc_my_routes/doc/route_collection.rb
|
115
|
+
- lib/doc_my_routes/doc/route_documentation.rb
|
116
|
+
- lib/doc_my_routes/doc/status_code_info.rb
|
117
|
+
- lib/doc_my_routes/version.rb
|
118
|
+
homepage: https://github.com/Workday/doc_my_routes
|
119
|
+
licenses:
|
120
|
+
- MIT
|
121
|
+
metadata: {}
|
122
|
+
post_install_message:
|
123
|
+
rdoc_options: []
|
124
|
+
require_paths:
|
125
|
+
- lib
|
126
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - ">="
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '0'
|
131
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
132
|
+
requirements:
|
133
|
+
- - ">="
|
134
|
+
- !ruby/object:Gem::Version
|
135
|
+
version: '0'
|
136
|
+
requirements: []
|
137
|
+
rubyforge_project:
|
138
|
+
rubygems_version: 2.4.8
|
139
|
+
signing_key:
|
140
|
+
specification_version: 4
|
141
|
+
summary: A simple gem to document Sinatra routes
|
142
|
+
test_files: []
|