media_types-serialization 2.0.4 → 2.1.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.
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 -34
  4. data/.github/workflows/publish-sid.yml +34 -34
  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 +207 -200
  16. data/CODE_OF_CONDUCT.md +74 -74
  17. data/Gemfile +4 -4
  18. data/Gemfile.lock +176 -169
  19. data/LICENSE.txt +21 -21
  20. data/README.md +1058 -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 +139 -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 +95 -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 -689
  41. data/media_types-serialization.gemspec +48 -48
  42. metadata +3 -3
@@ -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