media_types-serialization 1.4.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +32 -32
  3. data/.github/workflows/publish-bookworm.yml +33 -0
  4. data/.github/workflows/publish-sid.yml +33 -0
  5. data/.gitignore +22 -14
  6. data/.idea/.rakeTasks +7 -7
  7. data/.idea/dictionaries/Derk_Jan.xml +6 -6
  8. data/.idea/encodings.xml +3 -3
  9. data/.idea/inspectionProfiles/Project_Default.xml +5 -5
  10. data/.idea/media_types-serialization.iml +76 -76
  11. data/.idea/misc.xml +6 -6
  12. data/.idea/modules.xml +7 -7
  13. data/.idea/runConfigurations/test.xml +19 -19
  14. data/.idea/vcs.xml +5 -5
  15. data/CHANGELOG.md +190 -182
  16. data/CODE_OF_CONDUCT.md +74 -74
  17. data/Gemfile +4 -4
  18. data/LICENSE.txt +21 -21
  19. data/README.md +1048 -1048
  20. data/Rakefile +10 -10
  21. data/bin/console +14 -14
  22. data/bin/setup +8 -8
  23. data/lib/media_types/problem.rb +67 -67
  24. data/lib/media_types/serialization/base.rb +269 -216
  25. data/lib/media_types/serialization/error.rb +193 -193
  26. data/lib/media_types/serialization/fake_validator.rb +53 -53
  27. data/lib/media_types/serialization/serialization_dsl.rb +135 -135
  28. data/lib/media_types/serialization/serialization_registration.rb +245 -245
  29. data/lib/media_types/serialization/serializers/api_viewer.rb +383 -136
  30. data/lib/media_types/serialization/serializers/common_css.rb +212 -168
  31. data/lib/media_types/serialization/serializers/endpoint_description_serializer.rb +80 -80
  32. data/lib/media_types/serialization/serializers/fallback_not_acceptable_serializer.rb +85 -85
  33. data/lib/media_types/serialization/serializers/fallback_unsupported_media_type_serializer.rb +58 -58
  34. data/lib/media_types/serialization/serializers/input_validation_error_serializer.rb +93 -93
  35. data/lib/media_types/serialization/serializers/problem_serializer.rb +111 -111
  36. data/lib/media_types/serialization/utils/accept_header.rb +77 -77
  37. data/lib/media_types/serialization/utils/accept_language_header.rb +82 -82
  38. data/lib/media_types/serialization/version.rb +7 -7
  39. data/lib/media_types/serialization.rb +682 -675
  40. data/media_types-serialization.gemspec +48 -48
  41. metadata +9 -8
  42. data/Gemfile.lock +0 -167
@@ -1,136 +1,383 @@
1
- # frozen_string_literal: true
2
-
3
- require 'media_types/serialization/base'
4
- require 'erb'
5
- require 'cgi'
6
-
7
- module MediaTypes
8
- module Serialization
9
- module Serializers
10
- class ApiViewer < MediaTypes::Serialization::Base
11
- unvalidated 'text/html'
12
-
13
- def self.viewerify(uri, current_host, type: 'last')
14
- viewer = URI.parse(uri)
15
-
16
- return uri unless viewer.host == current_host
17
-
18
- query_parts = viewer.query&.split('&') || []
19
- query_parts = query_parts.reject { |p| p.starts_with?('api_viewer=') }
20
- query_parts.append("api_viewer=#{type}")
21
- viewer.query = query_parts.join('&')
22
- viewer.to_s
23
- end
24
-
25
- output_raw do |obj, version, context|
26
- original_identifier = obj[:identifier]
27
- registrations = obj[:registrations]
28
- original_output = obj[:output]
29
- original_links = obj[:links]
30
-
31
- api_fied_links = original_links.map do |l|
32
- new = l.dup
33
- new[:invalid] = false
34
- begin
35
- uri = viewerify(new[:href], context.request.host)
36
- new[:href] = uri.to_s
37
- rescue URI::InvalidURIError
38
- new[:invalid] = true
39
- end
40
-
41
- new
42
- end
43
-
44
- media_types = registrations.registrations.keys.map do |identifier|
45
- result = {
46
- identifier: identifier,
47
- href: viewerify(context.request.original_url, context.request.host, type: identifier),
48
- selected: identifier == original_identifier
49
- }
50
- result[:href] = '#output' if identifier == original_identifier
51
-
52
- result
53
- end
54
-
55
- escaped_output = original_output
56
- &.split("\n")
57
- &.map { |l| CGI.escapeHTML(l).gsub(/ (?= )/, '&nbsp;') }
58
- &.map do |l|
59
- l.gsub(/\bhttps?:\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;{}]*[-A-Z0-9+@#\/%=}~_|](?![a-z]*;)/i) do |m|
60
- converted = m
61
- invalid = false
62
- begin
63
- converted = viewerify(m, context.request.host)
64
- rescue URI::InvalidURIError
65
- invalid = true
66
- end
67
- style = ''
68
- style = ' style="color: red"' if invalid
69
- "<a#{style} href=\"#{converted}\">#{m}</a>"
70
- end
71
- end
72
- &.join("<br>\n")
73
-
74
- input = OpenStruct.new(
75
- original_identifier: original_identifier,
76
- escaped_output: escaped_output,
77
- api_fied_links: api_fied_links,
78
- media_types: media_types,
79
- css: CommonCSS.css
80
- )
81
-
82
- template = ERB.new <<-TEMPLATE
83
- <html lang="en">
84
- <head>
85
- <meta content="width=device-width, initial-scale=1" name="viewport">
86
-
87
- <title>API Viewer [<%= CGI::escapeHTML(original_identifier) %>]</title>
88
- <style>
89
- <%= css.split("\n").join("\n ") %>
90
- </style>
91
- </head>
92
- <body>
93
- <header>
94
- <div id="logo"></div>
95
- <h1>Api Viewer - <%= CGI::escapeHTML(original_identifier) %></h1>
96
- </header>
97
- <section id="content">
98
- <nav>
99
- <section id="representations">
100
- <h2>Representations:</h2>
101
- <ul>
102
- <% media_types.each do |m| %>
103
- <li>
104
- <a href="<%= m[:href] %>" <%= m[:selected] ? 'class="active" ' : '' %>>
105
- <%= CGI::escapeHTML(m[:identifier]) %>
106
- </a>
107
- </li>
108
- <% end %>
109
- </ul>
110
- <hr>
111
- </section>
112
- <section id="links">
113
- <span class="label">Links:&nbsp</span>
114
- <ul>
115
- <% api_fied_links.each do |l| %>
116
- <li><a <% if l[:invalid] %> style="color: red" <% end %>href="<%= l[:href] %>"><%= CGI::escapeHTML(l[:rel].to_s) %></a></li>
117
- <% end %>
118
- </ul>
119
- </section>
120
- </nav>
121
- <main>
122
- <code id="output">
123
- <%= escaped_output %>
124
- </code>
125
- </main>
126
- </section>
127
- <!-- API viewer made with ❤ by: https://delftsolutions.com -->
128
- </body>
129
- </html>
130
- TEMPLATE
131
- template.result(input.instance_eval { binding })
132
- end
133
- end
134
- end
135
- end
136
- end
1
+ # frozen_string_literal: true
2
+
3
+ require 'media_types/serialization/base'
4
+ require 'erb'
5
+ require 'cgi'
6
+
7
+ module MediaTypes
8
+ module Serialization
9
+ module Serializers
10
+ class ApiViewer < MediaTypes::Serialization::Base
11
+ unvalidated 'text/html'
12
+
13
+ def self.viewerify(uri, current_host, type: 'last')
14
+ viewer = URI.parse(uri)
15
+
16
+ return uri unless viewer.host == current_host
17
+
18
+ query_parts = viewer.query&.split('&') || []
19
+ query_parts = query_parts.reject { |p| p.starts_with?('api_viewer=') }
20
+ query_parts.append("api_viewer=#{type}")
21
+ viewer.query = query_parts.join('&')
22
+ viewer.to_s
23
+ end
24
+
25
+ def self.to_input_identifiers(serializers)
26
+ serializers.flat_map do |s|
27
+ s[:serializer].inputs_for(views: [s[:view]]).registrations.keys
28
+ end
29
+ end
30
+ def self.to_output_identifiers(serializers)
31
+ serializers.flat_map do |s|
32
+ s[:serializer].outputs_for(views: [s[:view]]).registrations.keys
33
+ end
34
+ end
35
+
36
+ def self.allowed_replies(context, actions)
37
+ request_path = context.request.original_fullpath.split('?')[0]
38
+
39
+ path_prefix = ENV.fetch('RAILS_RELATIVE_URL_ROOT') { '' }
40
+ request_path = request_path.sub(path_prefix, '')
41
+
42
+ my_controller = Rails.application.routes.recognize_path request_path
43
+ possible_replies = ['POST', 'PUT', 'DELETE']
44
+ enabled_replies = {}
45
+ possible_replies.each do |m|
46
+ begin
47
+ found_controller = Rails.application.routes.recognize_path request_path, method: m
48
+ if found_controller[:controller] == my_controller[:controller]
49
+ enabled_replies[m] = found_controller[:action]
50
+ end
51
+ rescue ActionController::RoutingError
52
+ # not available
53
+ end
54
+ end
55
+
56
+ input_definitions = actions[:input] || {}
57
+ output_definitions = actions[:output] || {}
58
+
59
+ result = {}
60
+ global_in = input_definitions['all_actions'] || []
61
+ global_out = output_definitions['all_actions'] || []
62
+
63
+ viewer_uri = URI.parse(context.request.original_url)
64
+ query_parts = viewer_uri.query&.split('&') || []
65
+ query_parts = query_parts.select { |q| !q.start_with? 'api_viewer=' }
66
+ viewer_uri.query = (query_parts + ["api_viewer=last"]).join('&')
67
+
68
+ enabled_replies.each do |method, action|
69
+ input_serializers = global_in + (input_definitions[action] || [])
70
+ output_serializers = global_out + (output_definitions[action] || [])
71
+ result[method] = {
72
+ input: to_input_identifiers(input_serializers),
73
+ output: to_output_identifiers(output_serializers),
74
+ }
75
+ end
76
+
77
+ result
78
+ end
79
+
80
+ def self.escape_javascript(value)
81
+ escape_map = {
82
+ "\\" => "\\\\",
83
+ "</" => '<\/',
84
+ "\r\n" => '\n',
85
+ "\n" => '\n',
86
+ "\r" => '\n',
87
+ '"' => '\\"',
88
+ "'" => "\\'",
89
+ "`" => "\\`",
90
+ "$" => "\\$"
91
+ }
92
+ escape_map[(+"\342\200\250").force_encoding(Encoding::UTF_8).encode!] = "&#x2028;"
93
+ escape_map[(+"\342\200\251").force_encoding(Encoding::UTF_8).encode!] = "&#x2029;"
94
+
95
+ value ||= ""
96
+
97
+ return value.gsub(/(\\|<\/|\r\n|\342\200\250|\342\200\251|[\n\r"']|[`]|[$])/u, escape_map).html_safe
98
+ end
99
+
100
+ output_raw do |obj, version, context|
101
+ original_identifier = obj[:identifier]
102
+ registrations = obj[:registrations]
103
+ original_output = obj[:output]
104
+ original_links = obj[:links]
105
+
106
+ api_fied_links = original_links.map do |l|
107
+ new = l.dup
108
+ new[:invalid] = false
109
+ begin
110
+ uri = viewerify(new[:href], context.request.host)
111
+ new[:href] = uri.to_s
112
+ rescue URI::InvalidURIError
113
+ new[:invalid] = true
114
+ end
115
+
116
+ new
117
+ end
118
+
119
+ media_types = registrations.registrations.keys.map do |identifier|
120
+ result = {
121
+ identifier: identifier,
122
+ href: viewerify(context.request.original_url, context.request.host, type: identifier),
123
+ selected: identifier == original_identifier
124
+ }
125
+ result[:href] = '#output' if identifier == original_identifier
126
+
127
+ result
128
+ end
129
+
130
+ escaped_output = original_output
131
+ &.split("\n")
132
+ &.map { |l| CGI.escapeHTML(l).gsub(/ (?= )/, '&nbsp;') }
133
+ &.map do |l|
134
+ l.gsub(/\bhttps?:\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;{}]*[-A-Z0-9+@#\/%=}~_|](?![a-z]*;)/i) do |m|
135
+ converted = m
136
+ invalid = false
137
+ begin
138
+ converted = viewerify(m, context.request.host)
139
+ rescue URI::InvalidURIError
140
+ invalid = true
141
+ end
142
+ style = ''
143
+ style = ' style="color: red"' if invalid
144
+ "<a#{style} href=\"#{converted}\">#{m}</a>"
145
+ end
146
+ end
147
+ &.join("<br>\n")
148
+
149
+ unviewered_uri = URI.parse(context.request.original_url)
150
+ query_parts = unviewered_uri.query&.split('&') || []
151
+ query_parts = query_parts.select { |q| !q.start_with? 'api_viewer=' }
152
+ unviewered_uri.query = query_parts.join('&')
153
+
154
+ input = OpenStruct.new(
155
+ original_identifier: original_identifier,
156
+ escaped_output: escaped_output,
157
+ api_fied_links: api_fied_links,
158
+ media_types: media_types,
159
+ css: CommonCSS.css,
160
+ etag: obj[:etag],
161
+ allowed_replies: allowed_replies(context, obj[:actions]),
162
+ escape_javascript: method(:escape_javascript),
163
+ unviewered_uri: unviewered_uri
164
+ )
165
+
166
+ template = ERB.new <<-TEMPLATE
167
+ <!DOCTYPE html>
168
+ <html lang="en">
169
+ <head>
170
+ <meta content="width=device-width, initial-scale=1" name="viewport">
171
+
172
+ <title>API Viewer [<%= CGI::escapeHTML(original_identifier) %>]</title>
173
+ <style>
174
+ <%= css.split("\n").join("\n ") %>
175
+ </style>
176
+ </head>
177
+ <body>
178
+ <header>
179
+ <div id="logo"></div>
180
+ <h1>Api Viewer - <%= CGI::escapeHTML(original_identifier) %></h1>
181
+ </header>
182
+ <section id="content">
183
+ <nav>
184
+ <section id="representations">
185
+ <h2>Representations:</h2>
186
+ <ul>
187
+ <% media_types.each do |m| %>
188
+ <li>
189
+ <a href="<%= m[:href] %>" <%= m[:selected] ? 'class="active" ' : '' %>>
190
+ <%= CGI::escapeHTML(m[:identifier]) %>
191
+ </a>
192
+ </li>
193
+ <% end %>
194
+ </ul>
195
+ <hr>
196
+ </section>
197
+ <section id="links">
198
+ <span class="label">Links:&nbsp</span>
199
+ <ul>
200
+ <% api_fied_links.each do |l| %>
201
+ <li><a <% if l[:invalid] %> style="color: red" <% end %>href="<%= l[:href] %>"><%= CGI::escapeHTML(l[:rel].to_s) %></a></li>
202
+ <% end %>
203
+ </ul>
204
+ </section>
205
+ </nav>
206
+ <% if allowed_replies.any? %>
207
+ <section id="reply">
208
+ <details>
209
+ <summary>Reply</summary>
210
+ <div class="reply-indent">
211
+ <noscript>Javascript is required to submit custom responses back to the server</noscript>
212
+ <form id="reply-form" hidden>
213
+ <div class="form-table">
214
+ <label class="form-row">
215
+ <div class="cell label">Method:</div>
216
+ <% if allowed_replies.keys.count == 1 %>
217
+ <input type="hidden" name="method" value="<%= allowed_replies.keys[0] %>">
218
+ <div class="cell"><%= allowed_replies.keys[0] %></div>
219
+ <% else %>
220
+ <select class="cell" name="method">
221
+ <% allowed_replies.keys.each do |method| %>
222
+ <option value="<%= method %>"><%= method %></option>
223
+ <% end %>
224
+ </select>
225
+ <% end %>
226
+ </label>
227
+ <label class="form-row"><div class="cell label">Send:</div> <select class="cell" name="request-content-type"></select></label>
228
+ <label class="form-row"><div class="cell label">Receive:</div> <select class="cell" name="response-content-type"></select></label>
229
+ </div>
230
+ <textarea name="request-content"></textarea>
231
+ <input type="button" name="submit" value="Reply"><span id="reply-status-code" hidden> - sending...</span>
232
+ <hr>
233
+ <code id="reply-response" hidden>
234
+ </code>
235
+ </form>
236
+ <script>
237
+ {
238
+ let form = document.getElementById("reply-form")
239
+ form.removeAttribute('hidden')
240
+
241
+ let action_data = JSON.parse("<%= escape_javascript.call(allowed_replies.to_json) %>")
242
+
243
+ let methodElem = form.elements["method"]
244
+ let requestTypeElem = form.elements["request-content-type"]
245
+ let responseTypeElem = form.elements["response-content-type"]
246
+ let contentElem = form.elements["request-content"]
247
+ let submitElem = form.elements["submit"]
248
+ let replyResponseElem = document.getElementById("reply-response")
249
+ let replyStatusCodeElem = document.getElementById("reply-status-code")
250
+ let selectRequestType = function() {
251
+ let selected = requestTypeElem.value
252
+
253
+ if (selected == "")
254
+ contentElem.setAttribute("hidden", "")
255
+ else
256
+ contentElem.removeAttribute("hidden")
257
+
258
+ if (methodElem.value == "PUT" && contentElem.value.trim() == "") {
259
+ let currentRequestType = document.querySelector("#representations .active").textContent.trim()
260
+ if (currentRequestType == requestTypeElem.value) {
261
+ let outputElem = document.getElementById("output")
262
+ contentElem.value = outputElem.
263
+ textContent.
264
+ trim().
265
+ replaceAll(String.fromCharCode(160), " ")
266
+ }
267
+ }
268
+ }
269
+
270
+ let selectMethod = function() {
271
+ let selected = methodElem.value
272
+ submitElem.setAttribute("value", selected)
273
+
274
+ let mediatypes = action_data[selected]
275
+
276
+ while(requestTypeElem.firstChild)
277
+ requestTypeElem.removeChild(requestTypeElem.lastChild)
278
+ mediatypes["input"].forEach(mediatype => {
279
+ let option = document.createElement("option")
280
+ option.setAttribute("value", mediatype)
281
+ option.textContent = mediatype
282
+ requestTypeElem.appendChild(option)
283
+ })
284
+ let noneOption = document.createElement("option")
285
+ noneOption.setAttribute("value", "")
286
+ noneOption.textContent = "None"
287
+ requestTypeElem.appendChild(noneOption)
288
+
289
+ while(responseTypeElem.firstChild)
290
+ responseTypeElem.removeChild(responseTypeElem.lastChild)
291
+ mediatypes["output"].forEach(mediatype => {
292
+ let option = document.createElement("option")
293
+ option.setAttribute("value", mediatype)
294
+ option.textContent = mediatype
295
+ responseTypeElem.appendChild(option)
296
+ })
297
+ let anyOption = document.createElement("option")
298
+ anyOption.setAttribute("value", "")
299
+ anyOption.textContent = "Any"
300
+ responseTypeElem.appendChild(anyOption)
301
+
302
+ selectRequestType()
303
+ }
304
+
305
+ let onSubmit = async function() {
306
+ submitElem.setAttribute("disabled", "")
307
+ let method = methodElem.value
308
+ let requestContentType = requestTypeElem.value
309
+ let requestContent = contentElem.value
310
+ var responseAccept = responseTypeElem.value + ", application/problem+json; q=0.2, */*; q=0.1"
311
+ if (responseTypeElem.value == "")
312
+ responseAccept = "application/problem+json, */*; q=0.1"
313
+
314
+ let headers = {
315
+ Accept: responseAccept,
316
+ }
317
+ if (method == "PUT") {
318
+ let etag = "<%= escape_javascript.call(etag) %>"
319
+ if (etag != "") {
320
+ headers['If-Match'] = etag
321
+ }
322
+ }
323
+ let body = undefined
324
+ if (requestContentType != "") {
325
+ headers["Content-Type"] = requestContentType
326
+ body = requestContent
327
+ }
328
+
329
+ replyResponseElem.textContent = ""
330
+ replyStatusCodeElem.textContent = " - sending..."
331
+ replyStatusCodeElem.removeAttribute("hidden")
332
+
333
+ try {
334
+ let response = await fetch("<%= escape_javascript.call(unviewered_uri.to_s) %>", {
335
+ method: method,
336
+ mode: "same-origin",
337
+ credentials: "same-origin",
338
+ redirect: "follow",
339
+ headers: headers,
340
+ body: body
341
+ })
342
+
343
+ replyStatusCodeElem.textContent = " - Status " + response.status + " " + response.statusText
344
+ replyResponseElem.removeAttribute("hidden")
345
+ replyResponseElem.textContent = await response.text()
346
+ replyResponseElem.innerHTML = replyResponseElem.
347
+ innerHTML.
348
+ replaceAll("\\n", "<br>\\n").
349
+ replaceAll(" ", "&nbsp; ")
350
+ } catch (error) {
351
+ replyStatusCodeElem.textContent = " - Failed: " + error.message
352
+ } finally {
353
+ submitElem.removeAttribute("disabled")
354
+ }
355
+ }
356
+
357
+ requestTypeElem.addEventListener("change", (e) => selectRequestType())
358
+ methodElem.addEventListener("change", (e) => selectMethod())
359
+ submitElem.addEventListener("click", (e) => onSubmit())
360
+
361
+ addEventListener("DOMContentLoaded", (event) => selectMethod());
362
+ }
363
+ </script>
364
+ </div>
365
+ </details>
366
+ </section>
367
+ <% end %>
368
+ <main>
369
+ <code id="output">
370
+ <%= escaped_output %>
371
+ </code>
372
+ </main>
373
+ </section>
374
+ <!-- API viewer made with ❤ by: https://delftsolutions.com -->
375
+ </body>
376
+ </html>
377
+ TEMPLATE
378
+ template.result(input.instance_eval { binding })
379
+ end
380
+ end
381
+ end
382
+ end
383
+ end