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 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
+ }
@@ -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,6 @@
1
+ # Define error classes
2
+ module DocMyRoutes
3
+ class ExampleMissing < StandardError; end
4
+ class UnsupportedError < StandardError; end
5
+ class MultipleMappingDetected < UnsupportedError; end
6
+ 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
@@ -0,0 +1,4 @@
1
+ # DocMyRoutes version
2
+ module DocMyRoutes
3
+ VERSION = '0.9.0'
4
+ 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: []