media_types-serialization 2.0.0 → 2.0.4

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 +34 -33
  4. data/.github/workflows/publish-sid.yml +34 -33
  5. data/.gitignore +22 -22
  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 +200 -190
  16. data/CODE_OF_CONDUCT.md +74 -74
  17. data/Gemfile +4 -4
  18. data/Gemfile.lock +169 -0
  19. data/LICENSE.txt +21 -21
  20. data/README.md +1048 -1048
  21. data/Rakefile +10 -10
  22. data/bin/console +14 -14
  23. data/bin/setup +8 -8
  24. data/lib/media_types/problem.rb +67 -67
  25. data/lib/media_types/serialization/base.rb +269 -269
  26. data/lib/media_types/serialization/error.rb +193 -193
  27. data/lib/media_types/serialization/fake_validator.rb +53 -53
  28. data/lib/media_types/serialization/serialization_dsl.rb +135 -135
  29. data/lib/media_types/serialization/serialization_registration.rb +245 -245
  30. data/lib/media_types/serialization/serializers/api_viewer.rb +383 -383
  31. data/lib/media_types/serialization/serializers/common_css.rb +212 -212
  32. data/lib/media_types/serialization/serializers/endpoint_description_serializer.rb +80 -80
  33. data/lib/media_types/serialization/serializers/fallback_not_acceptable_serializer.rb +85 -85
  34. data/lib/media_types/serialization/serializers/fallback_unsupported_media_type_serializer.rb +58 -58
  35. data/lib/media_types/serialization/serializers/input_validation_error_serializer.rb +93 -93
  36. data/lib/media_types/serialization/serializers/problem_serializer.rb +111 -111
  37. data/lib/media_types/serialization/utils/accept_header.rb +77 -77
  38. data/lib/media_types/serialization/utils/accept_language_header.rb +82 -82
  39. data/lib/media_types/serialization/version.rb +7 -7
  40. data/lib/media_types/serialization.rb +689 -682
  41. data/media_types-serialization.gemspec +48 -48
  42. metadata +10 -9
@@ -1,383 +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
- 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
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