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 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: []