doc_my_routes 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|