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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +32 -32
- data/.github/workflows/publish-bookworm.yml +34 -34
- data/.github/workflows/publish-sid.yml +34 -34
- data/.gitignore +22 -22
- data/.idea/.rakeTasks +7 -7
- data/.idea/dictionaries/Derk_Jan.xml +6 -6
- data/.idea/encodings.xml +3 -3
- data/.idea/inspectionProfiles/Project_Default.xml +5 -5
- data/.idea/media_types-serialization.iml +76 -76
- data/.idea/misc.xml +6 -6
- data/.idea/modules.xml +7 -7
- data/.idea/runConfigurations/test.xml +19 -19
- data/.idea/vcs.xml +5 -5
- data/CHANGELOG.md +207 -200
- data/CODE_OF_CONDUCT.md +74 -74
- data/Gemfile +4 -4
- data/Gemfile.lock +176 -169
- data/LICENSE.txt +21 -21
- data/README.md +1058 -1048
- data/Rakefile +10 -10
- data/bin/console +14 -14
- data/bin/setup +8 -8
- data/lib/media_types/problem.rb +67 -67
- data/lib/media_types/serialization/base.rb +269 -269
- data/lib/media_types/serialization/error.rb +193 -193
- data/lib/media_types/serialization/fake_validator.rb +53 -53
- data/lib/media_types/serialization/serialization_dsl.rb +139 -135
- data/lib/media_types/serialization/serialization_registration.rb +245 -245
- data/lib/media_types/serialization/serializers/api_viewer.rb +383 -383
- data/lib/media_types/serialization/serializers/common_css.rb +212 -212
- data/lib/media_types/serialization/serializers/endpoint_description_serializer.rb +80 -80
- data/lib/media_types/serialization/serializers/fallback_not_acceptable_serializer.rb +85 -85
- data/lib/media_types/serialization/serializers/fallback_unsupported_media_type_serializer.rb +58 -58
- data/lib/media_types/serialization/serializers/input_validation_error_serializer.rb +95 -93
- data/lib/media_types/serialization/serializers/problem_serializer.rb +111 -111
- data/lib/media_types/serialization/utils/accept_header.rb +77 -77
- data/lib/media_types/serialization/utils/accept_language_header.rb +82 -82
- data/lib/media_types/serialization/version.rb +7 -7
- data/lib/media_types/serialization.rb +689 -689
- data/media_types-serialization.gemspec +48 -48
- metadata +3 -3
@@ -1,689 +1,689 @@
|
|
1
|
-
require 'media_types/serialization/version'
|
2
|
-
require 'media_types/serialization/serializers/common_css'
|
3
|
-
require 'media_types/serialization/serializers/fallback_not_acceptable_serializer'
|
4
|
-
require 'media_types/serialization/serializers/fallback_unsupported_media_type_serializer'
|
5
|
-
require 'media_types/serialization/serializers/input_validation_error_serializer'
|
6
|
-
require 'media_types/serialization/serializers/endpoint_description_serializer'
|
7
|
-
require 'media_types/serialization/serializers/problem_serializer'
|
8
|
-
require 'media_types/serialization/serializers/api_viewer'
|
9
|
-
require 'media_types/problem'
|
10
|
-
|
11
|
-
require 'abstract_controller'
|
12
|
-
require 'action_controller/metal/mime_responds'
|
13
|
-
require 'action_dispatch/http/mime_type'
|
14
|
-
require 'active_support/concern'
|
15
|
-
require 'active_support/core_ext/module/attribute_accessors'
|
16
|
-
require 'active_support/core_ext/object/blank'
|
17
|
-
|
18
|
-
require 'media_types/serialization/utils/accept_header'
|
19
|
-
require 'media_types/serialization/base'
|
20
|
-
require 'media_types/serialization/error'
|
21
|
-
require 'media_types/serialization/serialization_dsl'
|
22
|
-
|
23
|
-
require 'delegate'
|
24
|
-
|
25
|
-
class SerializationSelectorDsl < SimpleDelegator
|
26
|
-
def initialize(controller, selected_serializer)
|
27
|
-
@serializer = selected_serializer
|
28
|
-
self.value = nil
|
29
|
-
self.matched = false
|
30
|
-
super controller
|
31
|
-
end
|
32
|
-
|
33
|
-
attr_accessor :value, :matched
|
34
|
-
|
35
|
-
def serializer(klazz, obj = nil, &block)
|
36
|
-
return if klazz != @serializer
|
37
|
-
|
38
|
-
self.matched = true
|
39
|
-
self.value = block.nil? ? obj : yield
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
module MediaTypes
|
44
|
-
module Serialization
|
45
|
-
|
46
|
-
HEADER_ACCEPT = 'HTTP_ACCEPT'.freeze
|
47
|
-
HEADER_ACCEPT_LANGUAGE = 'HTTP_ACCEPT_LANGUAGE'.freeze
|
48
|
-
|
49
|
-
mattr_accessor :json_encoder, :json_decoder
|
50
|
-
if defined?(::Oj)
|
51
|
-
self.json_encoder = ->(obj) {
|
52
|
-
Oj.dump(obj,
|
53
|
-
mode: :compat,
|
54
|
-
indent: ' ',
|
55
|
-
space: ' ',
|
56
|
-
array_nl: "\n",
|
57
|
-
object_nl: "\n",
|
58
|
-
ascii_only: false,
|
59
|
-
allow_nan: false,
|
60
|
-
symbol_keys: true,
|
61
|
-
allow_nil: false,
|
62
|
-
allow_invalid_unicode: false,
|
63
|
-
array_class: ::Array,
|
64
|
-
create_additions: false,
|
65
|
-
hash_class: ::Hash,
|
66
|
-
nilnil: false,
|
67
|
-
quirks_mode: false
|
68
|
-
)
|
69
|
-
}
|
70
|
-
self.json_decoder = ->(obj) {
|
71
|
-
Oj.load(obj,
|
72
|
-
mode: :compat,
|
73
|
-
ascii_only: false,
|
74
|
-
allow_nan: false,
|
75
|
-
symbol_keys: true,
|
76
|
-
allow_nil: false,
|
77
|
-
allow_invalid_unicode: false,
|
78
|
-
array_class: ::Array,
|
79
|
-
create_additions: false,
|
80
|
-
hash_class: ::Hash,
|
81
|
-
nilnil: false,
|
82
|
-
quirks_mode: false
|
83
|
-
)
|
84
|
-
}
|
85
|
-
else
|
86
|
-
require 'json'
|
87
|
-
self.json_encoder = JSON.method(:pretty_generate)
|
88
|
-
self.json_decoder = ->(txt) {
|
89
|
-
JSON.parse(txt, {
|
90
|
-
symbolize_names: true,
|
91
|
-
allow_nan: false,
|
92
|
-
create_additions: false,
|
93
|
-
object_class: ::Hash,
|
94
|
-
array_class: ::Array,
|
95
|
-
})
|
96
|
-
}
|
97
|
-
end
|
98
|
-
|
99
|
-
extend ActiveSupport::Concern
|
100
|
-
|
101
|
-
# rubocop:disable Metrics/BlockLength
|
102
|
-
class_methods do
|
103
|
-
def not_acceptable_serializer(serializer, **filter_opts)
|
104
|
-
before_action(**filter_opts) do
|
105
|
-
raise SerializersAlreadyFrozenError if defined? @serialization_frozen
|
106
|
-
|
107
|
-
@serialization_not_acceptable_serializer = serializer
|
108
|
-
end
|
109
|
-
end
|
110
|
-
|
111
|
-
def unsupported_media_type_serializer(serializer, **filter_opts)
|
112
|
-
before_action(**filter_opts) do
|
113
|
-
raise SerializersAlreadyFrozenError if defined? @serialization_frozen
|
114
|
-
|
115
|
-
@serialization_unsupported_media_type_serializer ||= []
|
116
|
-
@serialization_unsupported_media_type_serializer.append(serializer)
|
117
|
-
end
|
118
|
-
end
|
119
|
-
|
120
|
-
def clear_unsupported_media_type_serializer!(**filter_opts)
|
121
|
-
before_action(**filter_opts) do
|
122
|
-
raise SerializersAlreadyFrozenError if defined? @serialization_frozen
|
123
|
-
|
124
|
-
@serialization_unsupported_media_type_serializer = []
|
125
|
-
end
|
126
|
-
end
|
127
|
-
|
128
|
-
def input_validation_failed_serializer(serializer, **filter_opts)
|
129
|
-
before_action(**filter_opts) do
|
130
|
-
raise SerializersAlreadyFrozenError if defined? @serialization_frozen
|
131
|
-
|
132
|
-
@serialization_input_validation_failed_serializer ||= []
|
133
|
-
@serialization_input_validation_failed_serializer.append(serializer)
|
134
|
-
end
|
135
|
-
end
|
136
|
-
|
137
|
-
def clear_input_validation_failed_serializers!(**filter_opts)
|
138
|
-
before_action(**filter_opts) do
|
139
|
-
raise SerializersAlreadyFrozenError if defined? @serialization_frozen
|
140
|
-
|
141
|
-
@serialization_input_validation_failed_serializer = []
|
142
|
-
end
|
143
|
-
end
|
144
|
-
|
145
|
-
##
|
146
|
-
# Allow output serialization using the passed in +serializer+ for the given +view+
|
147
|
-
#
|
148
|
-
# @see #freeze_io!
|
149
|
-
#
|
150
|
-
# @param serializer the serializer to use for serialization.
|
151
|
-
# @param [(String | NilClass|)] view the view it should use the serializer for. Use nil for no view
|
152
|
-
# @param [(String | NilClass|)[]|NilClass] views the views it should use the serializer for. Use nil for no view
|
153
|
-
#
|
154
|
-
def allow_output_serializer(serializer, view: nil, views: nil, **filter_opts)
|
155
|
-
raise SerializersAlreadyFrozenError if defined? @serialization_frozen
|
156
|
-
raise ArrayInViewParameterError, :allow_output_serializer if view.is_a? Array
|
157
|
-
|
158
|
-
views = [view] if views.nil?
|
159
|
-
raise ViewsNotAnArrayError unless views.is_a? Array
|
160
|
-
|
161
|
-
before_action do
|
162
|
-
@serialization_available_serializers ||= {}
|
163
|
-
@serialization_available_serializers[:output] ||= {}
|
164
|
-
actions = filter_opts[:only] || :all_actions
|
165
|
-
actions = [actions] unless actions.is_a?(Array)
|
166
|
-
actions.each do |action|
|
167
|
-
@serialization_available_serializers[:output][action.to_s] ||= []
|
168
|
-
views.each do |v|
|
169
|
-
@serialization_available_serializers[:output][action.to_s].push({serializer: serializer, view: v})
|
170
|
-
end
|
171
|
-
end
|
172
|
-
end
|
173
|
-
|
174
|
-
before_action(**filter_opts) do
|
175
|
-
raise SerializersAlreadyFrozenError if defined? @serialization_frozen
|
176
|
-
|
177
|
-
@serialization_output_registrations ||= SerializationRegistration.new(:output)
|
178
|
-
|
179
|
-
mergeable_outputs = serializer.outputs_for(views: views)
|
180
|
-
raise AddedEmptyOutputSerializer, serializer.name if mergeable_outputs.registrations.empty?
|
181
|
-
|
182
|
-
@serialization_output_registrations = @serialization_output_registrations.merge(mergeable_outputs)
|
183
|
-
end
|
184
|
-
end
|
185
|
-
|
186
|
-
def allow_output_html(as: nil, view: nil, layout: nil, formats: [:html], variants: nil, **filter_opts)
|
187
|
-
before_action(**filter_opts) do
|
188
|
-
raise SerializersAlreadyFrozenError if defined? @serialization_frozen
|
189
|
-
|
190
|
-
@serialization_output_registrations ||= SerializationRegistration.new(:output)
|
191
|
-
|
192
|
-
html_registration = SerializationRegistration.new(:output)
|
193
|
-
output_identifier = 'text/html'
|
194
|
-
output_identifier += "; variant=#{as}" unless as.nil?
|
195
|
-
|
196
|
-
validator = FakeValidator.new(as.nil? ? 'text/html' : as)
|
197
|
-
|
198
|
-
block = lambda { |victim, _, controller|
|
199
|
-
options = {}
|
200
|
-
options[:layout] = layout unless layout.nil?
|
201
|
-
options[:template] = view unless view.nil?
|
202
|
-
options[:formats] = formats unless formats.nil?
|
203
|
-
options[:variants] = variants unless variants.nil?
|
204
|
-
options[:locals] = victim if victim.is_a?(::Hash)
|
205
|
-
options[:assigns] = { media: victim }
|
206
|
-
|
207
|
-
controller.render_to_string(**options)
|
208
|
-
}
|
209
|
-
|
210
|
-
html_registration.register_block(nil, validator, nil, block, true, wildcards: true)
|
211
|
-
html_registration.registrations[validator.identifier].display_identifier = output_identifier
|
212
|
-
html_registration.registrations["#{validator.identifier.split('/')[0]}/*"].display_identifier = output_identifier
|
213
|
-
html_registration.registrations['*/*'].display_identifier = output_identifier
|
214
|
-
|
215
|
-
@serialization_output_registrations = @serialization_output_registrations.merge(html_registration)
|
216
|
-
end
|
217
|
-
end
|
218
|
-
|
219
|
-
def allow_output_docs(description, **filter_opts)
|
220
|
-
before_action(**filter_opts) do
|
221
|
-
raise SerializersAlreadyFrozenError if defined? @serialization_frozen
|
222
|
-
|
223
|
-
@serialization_output_registrations ||= SerializationRegistration.new(:output)
|
224
|
-
|
225
|
-
docs_registration = SerializationRegistration.new(:output)
|
226
|
-
validator = FakeValidator.new('text/vnd.delftsolutions.docs')
|
227
|
-
|
228
|
-
block = lambda { |_, _, _|
|
229
|
-
description
|
230
|
-
}
|
231
|
-
|
232
|
-
docs_registration.register_block(nil, validator, nil, block, true, wildcards: true)
|
233
|
-
docs_registration.registrations['text/vnd.delftsolutions.docs'].display_identifier = 'text/plain; charset=utf-8'
|
234
|
-
docs_registration.registrations['text/*'].display_identifier = 'text/plain; charset=utf-8'
|
235
|
-
docs_registration.registrations['*/*'].display_identifier = 'text/plain; charset=utf-8'
|
236
|
-
|
237
|
-
@serialization_output_registrations = @serialization_output_registrations.merge(docs_registration)
|
238
|
-
end
|
239
|
-
end
|
240
|
-
|
241
|
-
def allow_api_viewer(serializer: MediaTypes::Serialization::Serializers::ApiViewer, **filter_opts)
|
242
|
-
before_action do
|
243
|
-
@serialization_api_viewer_enabled ||= {}
|
244
|
-
actions = filter_opts[:only] || :all_actions
|
245
|
-
actions = [actions] unless actions.kind_of?(Array)
|
246
|
-
actions.each do |action|
|
247
|
-
@serialization_api_viewer_enabled[action.to_s] = true
|
248
|
-
end
|
249
|
-
end
|
250
|
-
|
251
|
-
before_action(**filter_opts) do
|
252
|
-
if request.query_parameters['api_viewer']
|
253
|
-
@serialization_override_accept = request.query_parameters['api_viewer'].sub ' ', '+'
|
254
|
-
@serialization_wrapping_renderer = serializer
|
255
|
-
end
|
256
|
-
end
|
257
|
-
end
|
258
|
-
|
259
|
-
##
|
260
|
-
# Allow input serialization using the passed in +serializer+ for the given +view+
|
261
|
-
#
|
262
|
-
# @see #freeze_io!
|
263
|
-
#
|
264
|
-
# @param serializer the serializer to use for deserialization
|
265
|
-
# @param [(String | NilClass|)] view the view it should serializer for. Use nil for no view
|
266
|
-
# @param [(String | NilClass|)[]|NilClass] views the views it should serializer for. Use nil for no view
|
267
|
-
#
|
268
|
-
def allow_input_serializer(serializer, view: nil, views: nil, **filter_opts)
|
269
|
-
raise SerializersAlreadyFrozenError if defined? @serialization_frozen
|
270
|
-
raise ArrayInViewParameterError, :allow_input_serializer if view.is_a? Array
|
271
|
-
views = [view] if views.nil?
|
272
|
-
raise ViewsNotAnArrayError unless views.is_a? Array
|
273
|
-
|
274
|
-
before_action do
|
275
|
-
@serialization_available_serializers ||= {}
|
276
|
-
@serialization_available_serializers[:input] ||= {}
|
277
|
-
actions = filter_opts[:only] || :all_actions
|
278
|
-
actions = [actions] unless actions.is_a?(Array)
|
279
|
-
actions.each do |action|
|
280
|
-
@serialization_available_serializers[:input][action.to_s] ||= []
|
281
|
-
views.each do |v|
|
282
|
-
@serialization_available_serializers[:input][action.to_s].push({serializer: serializer, view: v})
|
283
|
-
end
|
284
|
-
end
|
285
|
-
end
|
286
|
-
|
287
|
-
before_action(**filter_opts) do
|
288
|
-
raise SerializersAlreadyFrozenError if defined? @serialization_frozen
|
289
|
-
|
290
|
-
@serialization_input_registrations ||= SerializationRegistration.new(:input)
|
291
|
-
|
292
|
-
mergeable_inputs = serializer.inputs_for(views: views)
|
293
|
-
raise AddedEmptyInputSerializer, serializer.name if mergeable_inputs.registrations.empty?
|
294
|
-
|
295
|
-
@serialization_input_registrations = @serialization_input_registrations.merge(mergeable_inputs)
|
296
|
-
end
|
297
|
-
end
|
298
|
-
|
299
|
-
def allow_all_output(**filter_opts)
|
300
|
-
before_action(**filter_opts) do
|
301
|
-
@serialization_output_registrations ||= SerializationRegistration.new(:output)
|
302
|
-
@serialization_output_allow_all ||= true
|
303
|
-
end
|
304
|
-
end
|
305
|
-
|
306
|
-
def allow_all_input(**filter_opts)
|
307
|
-
before_action(**filter_opts) do
|
308
|
-
@serialization_input_registrations ||= SerializationRegistration.new(:input)
|
309
|
-
@serialization_input_allow_all ||= true
|
310
|
-
end
|
311
|
-
end
|
312
|
-
|
313
|
-
##
|
314
|
-
# Freezes additions to the serializes and notifies the controller what it will be able to respond to.
|
315
|
-
#
|
316
|
-
def freeze_io!(**filter_opts)
|
317
|
-
before_action :serializer_freeze_io_internal, **filter_opts
|
318
|
-
|
319
|
-
output_error MediaTypes::Serialization::NoInputReceivedError do |p, _error|
|
320
|
-
p.title 'Providing input is mandatory. Please set a Content-Type', lang: 'en'
|
321
|
-
|
322
|
-
p.status_code :bad_request
|
323
|
-
end
|
324
|
-
end
|
325
|
-
|
326
|
-
def output_error(klazz, serializers: [])
|
327
|
-
rescue_from klazz do |error|
|
328
|
-
problem = Problem.new(error)
|
329
|
-
instance_exec { yield problem, error, self } if block_given?
|
330
|
-
|
331
|
-
serializer = MediaTypes::Serialization::Serializers::ProblemSerializer
|
332
|
-
registrations = serializer.outputs_for(views: [:html, nil])
|
333
|
-
|
334
|
-
render_media(
|
335
|
-
problem,
|
336
|
-
serializers: [registrations] + serializers.map { |s| s.outputs_for(views: [nil])},
|
337
|
-
status: problem.response_status_code
|
338
|
-
)
|
339
|
-
end
|
340
|
-
end
|
341
|
-
end
|
342
|
-
# rubocop:enable Metrics/BlockLength
|
343
|
-
|
344
|
-
protected
|
345
|
-
|
346
|
-
def serialize(victim, media_type, serializer: Object.new, links: [], vary: ['Accept'])
|
347
|
-
context = SerializationDSL.new(serializer, links, vary, context: self)
|
348
|
-
context.instance_exec { @serialization_output_registrations.call(victim, media_type, context) }
|
349
|
-
end
|
350
|
-
|
351
|
-
MEDIA_TYPES_SERIALIZATION_OBJ_IS_UNDEFINED = ::Object.new
|
352
|
-
|
353
|
-
def render_media(obj = MEDIA_TYPES_SERIALIZATION_OBJ_IS_UNDEFINED, **options, &block)
|
354
|
-
serializers = options.delete(:serializers)
|
355
|
-
not_acceptable_serializer = options.delete(:not_acceptable_serializer)
|
356
|
-
|
357
|
-
if obj == MEDIA_TYPES_SERIALIZATION_OBJ_IS_UNDEFINED && options.keys.any? && !block
|
358
|
-
# options is too greedy :(
|
359
|
-
obj = options
|
360
|
-
options = {}
|
361
|
-
end
|
362
|
-
|
363
|
-
if obj == MEDIA_TYPES_SERIALIZATION_OBJ_IS_UNDEFINED && block.nil?
|
364
|
-
raise 'render_media was called without an object. Please provide one or supply a block to match the serializer.'
|
365
|
-
end
|
366
|
-
|
367
|
-
obj = nil if obj == MEDIA_TYPES_SERIALIZATION_OBJ_IS_UNDEFINED
|
368
|
-
|
369
|
-
raise SerializersNotFrozenError unless defined? @serialization_frozen
|
370
|
-
|
371
|
-
if defined? @serialization_not_acceptable_serializer
|
372
|
-
not_acceptable_serializer ||= @serialization_not_acceptable_serializer
|
373
|
-
end
|
374
|
-
|
375
|
-
@serialization_output_registrations ||= SerializationRegistration.new(:output)
|
376
|
-
registration = @serialization_output_registrations
|
377
|
-
unless serializers.nil?
|
378
|
-
registration = SerializationRegistration.new(:output)
|
379
|
-
serializers.each do |s|
|
380
|
-
registration = registration.merge(s)
|
381
|
-
end
|
382
|
-
end
|
383
|
-
|
384
|
-
identifier = resolve_media_type(request, registration)
|
385
|
-
|
386
|
-
if identifier.nil?
|
387
|
-
serialization_render_not_acceptable(registration, not_acceptable_serializer)
|
388
|
-
return
|
389
|
-
end
|
390
|
-
|
391
|
-
serializer = resolve_serializer(request, identifier, registration)
|
392
|
-
|
393
|
-
unless block.nil?
|
394
|
-
selector = SerializationSelectorDsl.new(self, serializer)
|
395
|
-
selector.instance_exec(&block)
|
396
|
-
|
397
|
-
raise UnmatchedSerializerError, serializer unless selector.matched
|
398
|
-
|
399
|
-
obj = selector.value
|
400
|
-
end
|
401
|
-
|
402
|
-
serialization_render_resolved(
|
403
|
-
obj: obj,
|
404
|
-
serializer: serializer,
|
405
|
-
identifier: identifier,
|
406
|
-
registrations: registration,
|
407
|
-
options: options
|
408
|
-
)
|
409
|
-
end
|
410
|
-
|
411
|
-
def deserialize(request)
|
412
|
-
raise SerializersNotFrozenError unless defined?(@serialization_frozen)
|
413
|
-
|
414
|
-
result = nil
|
415
|
-
begin
|
416
|
-
result = deserialize!(request)
|
417
|
-
rescue NoInputReceivedError
|
418
|
-
return nil
|
419
|
-
end
|
420
|
-
result
|
421
|
-
end
|
422
|
-
|
423
|
-
def deserialize!(request)
|
424
|
-
raise SerializersNotFrozenError unless defined?(@serialization_frozen)
|
425
|
-
raise NoInputReceivedError if request.content_type.blank?
|
426
|
-
raise InputNotAcceptableError unless @serialization_input_registrations.has? request.content_type
|
427
|
-
|
428
|
-
@serialization_input_registrations.call(@serialization_decoded_input, request.content_type, self)
|
429
|
-
end
|
430
|
-
|
431
|
-
def resolve_serializer(request, identifier = nil, registration = @serialization_output_registrations)
|
432
|
-
identifier = resolve_media_type(request, registration) if identifier.nil?
|
433
|
-
return nil if identifier.nil?
|
434
|
-
|
435
|
-
registration = registration.registrations[identifier]
|
436
|
-
|
437
|
-
raise 'Assertion failed, inconsistent answer from resolve_media_type' if registration.nil?
|
438
|
-
|
439
|
-
registration.serializer
|
440
|
-
end
|
441
|
-
|
442
|
-
private
|
443
|
-
|
444
|
-
def resolve_media_type(request, registration, allow_last: true)
|
445
|
-
if defined? @serialization_override_accept
|
446
|
-
if allow_last && @serialization_override_accept == 'last'
|
447
|
-
@serialization_override_accept = registration.registrations.keys.last
|
448
|
-
end
|
449
|
-
|
450
|
-
return @serialization_override_accept if registration.has? @serialization_override_accept
|
451
|
-
|
452
|
-
# Always render problems in api viewer if we can't show chosen override
|
453
|
-
return 'application/problem+json' if registration.has? 'application/problem+json'
|
454
|
-
|
455
|
-
return nil
|
456
|
-
end
|
457
|
-
|
458
|
-
# Ruby negotiation
|
459
|
-
#
|
460
|
-
# This is similar to the respond_to logic. It sorts the accept values and tries to match against each option.
|
461
|
-
#
|
462
|
-
#
|
463
|
-
|
464
|
-
accept_header = Utils::AcceptHeader.new(request.get_header(HEADER_ACCEPT)) || ''
|
465
|
-
accept_header.each do |mime_type|
|
466
|
-
stripped = mime_type.to_s.split(';')[0]
|
467
|
-
next unless registration.has? stripped
|
468
|
-
|
469
|
-
return stripped
|
470
|
-
end
|
471
|
-
|
472
|
-
nil
|
473
|
-
end
|
474
|
-
|
475
|
-
def serialization_render_not_acceptable(registrations, override = nil)
|
476
|
-
serializer = override
|
477
|
-
serializer ||= MediaTypes::Serialization::Serializers::FallbackNotAcceptableSerializer
|
478
|
-
identifier = serializer.validator.identifier
|
479
|
-
obj = { request: request, registrations: registrations }
|
480
|
-
new_registrations = serializer.outputs_for(views: [nil])
|
481
|
-
|
482
|
-
serialization_render_resolved(
|
483
|
-
obj: obj,
|
484
|
-
serializer: serializer,
|
485
|
-
identifier: identifier,
|
486
|
-
registrations: new_registrations,
|
487
|
-
options: {}
|
488
|
-
)
|
489
|
-
|
490
|
-
response.status = :not_acceptable
|
491
|
-
end
|
492
|
-
|
493
|
-
def serializer_freeze_io_internal
|
494
|
-
raise UnableToRefreezeError if defined? @serialization_frozen
|
495
|
-
|
496
|
-
@serialization_frozen = true
|
497
|
-
@serialization_input_registrations ||= SerializationRegistration.new(:input)
|
498
|
-
|
499
|
-
raise NoOutputSerializersDefinedError unless defined? @serialization_output_registrations
|
500
|
-
|
501
|
-
# Input content-type negotiation and validation
|
502
|
-
all_allowed = false
|
503
|
-
all_allowed ||= @serialization_input_allow_all if defined?(@serialization_input_allow_all)
|
504
|
-
|
505
|
-
input_is_allowed = true
|
506
|
-
input_is_allowed = @serialization_input_registrations.has? request.content_type unless request.content_type.blank?
|
507
|
-
|
508
|
-
unless input_is_allowed || all_allowed
|
509
|
-
serializers = @serialization_unsupported_media_type_serializer || [
|
510
|
-
MediaTypes::Serialization::Serializers::ProblemSerializer,
|
511
|
-
MediaTypes::Serialization::Serializers::FallbackUnsupportedMediaTypeSerializer
|
512
|
-
]
|
513
|
-
registrations = SerializationRegistration.new(:output)
|
514
|
-
serializers.each do |s|
|
515
|
-
registrations = registrations.merge(s.outputs_for(views: [nil, :html]))
|
516
|
-
end
|
517
|
-
|
518
|
-
input = {
|
519
|
-
registrations: @serialization_input_registrations
|
520
|
-
}
|
521
|
-
|
522
|
-
render_media nil, serializers: [registrations], status: :unsupported_media_type do
|
523
|
-
serializer MediaTypes::Serialization::Serializers::FallbackUnsupportedMediaTypeSerializer, input
|
524
|
-
serializer MediaTypes::Serialization::Serializers::ProblemSerializer do
|
525
|
-
error = UnsupportedMediaTypeError.new(input[:registrations].registrations.keys)
|
526
|
-
problem = Problem.new(error)
|
527
|
-
problem.title 'Unable to process your body Content-Type.', lang: 'en'
|
528
|
-
|
529
|
-
problem
|
530
|
-
end
|
531
|
-
end
|
532
|
-
return
|
533
|
-
end
|
534
|
-
|
535
|
-
if input_is_allowed && !request.content_type.blank?
|
536
|
-
begin
|
537
|
-
input_data = request.body.read
|
538
|
-
@serialization_decoded_input = @serialization_input_registrations.decode(
|
539
|
-
input_data,
|
540
|
-
request.content_type,
|
541
|
-
self
|
542
|
-
)
|
543
|
-
rescue InputValidationFailedError => e
|
544
|
-
serializers = if defined?(@serialization_input_validation_failed_serializer)
|
545
|
-
@serialization_input_validation_failed_serializer
|
546
|
-
else
|
547
|
-
[
|
548
|
-
MediaTypes::Serialization::Serializers::ProblemSerializer,
|
549
|
-
MediaTypes::Serialization::Serializers::InputValidationErrorSerializer
|
550
|
-
]
|
551
|
-
end
|
552
|
-
registrations = SerializationRegistration.new(:output)
|
553
|
-
serializers.each do |s|
|
554
|
-
registrations = registrations.merge(s.outputs_for(views: [nil, :html]))
|
555
|
-
end
|
556
|
-
|
557
|
-
input = {
|
558
|
-
identifier: request.content_type,
|
559
|
-
input: input_data,
|
560
|
-
error: e
|
561
|
-
}
|
562
|
-
|
563
|
-
render_media nil, serializers: [registrations], status: :bad_request do
|
564
|
-
serializer MediaTypes::Serialization::Serializers::InputValidationErrorSerializer, input
|
565
|
-
serializer MediaTypes::Serialization::Serializers::ProblemSerializer do
|
566
|
-
problem = Problem.new(e)
|
567
|
-
problem.title 'Input failed to validate.', lang: 'en'
|
568
|
-
|
569
|
-
problem
|
570
|
-
end
|
571
|
-
end
|
572
|
-
return
|
573
|
-
end
|
574
|
-
end
|
575
|
-
|
576
|
-
# Endpoint description media type
|
577
|
-
|
578
|
-
description_serializer = MediaTypes::Serialization::Serializers::EndpointDescriptionSerializer
|
579
|
-
|
580
|
-
# All endpoints have endpoint description.
|
581
|
-
# Placed in front of the list to make sure the api viewer doesn't pick it.
|
582
|
-
@serialization_output_registrations =
|
583
|
-
description_serializer
|
584
|
-
.outputs_for(views: [nil])
|
585
|
-
.merge(@serialization_output_registrations)
|
586
|
-
|
587
|
-
endpoint_matched_identifier = resolve_media_type(
|
588
|
-
request,
|
589
|
-
description_serializer.serializer_output_registrations[nil],
|
590
|
-
allow_last: false
|
591
|
-
)
|
592
|
-
|
593
|
-
if endpoint_matched_identifier
|
594
|
-
# We picked an endpoint description media type
|
595
|
-
#
|
596
|
-
@serialization_available_serializers ||= {}
|
597
|
-
@serialization_available_serializers[:output] ||= {}
|
598
|
-
@serialization_api_viewer_enabled ||= {}
|
599
|
-
|
600
|
-
input = {
|
601
|
-
api_viewer: @serialization_api_viewer_enabled,
|
602
|
-
actions: @serialization_available_serializers
|
603
|
-
}
|
604
|
-
|
605
|
-
serialization_render_resolved(
|
606
|
-
obj: input,
|
607
|
-
serializer: description_serializer,
|
608
|
-
identifier: endpoint_matched_identifier,
|
609
|
-
registrations: @serialization_output_registrations,
|
610
|
-
options: {}
|
611
|
-
)
|
612
|
-
return
|
613
|
-
end
|
614
|
-
|
615
|
-
# Output content negotiation
|
616
|
-
resolved_identifier = resolve_media_type(request, @serialization_output_registrations)
|
617
|
-
|
618
|
-
not_acceptable_serializer = nil
|
619
|
-
|
620
|
-
if defined? @serialization_not_acceptable_serializer
|
621
|
-
not_acceptable_serializer = @serialization_not_acceptable_serializer
|
622
|
-
end
|
623
|
-
|
624
|
-
not_acceptable_serializer ||= MediaTypes::Serialization::Serializers::FallbackNotAcceptableSerializer
|
625
|
-
|
626
|
-
can_satisfy_allow = !resolved_identifier.nil?
|
627
|
-
can_satisfy_allow ||= @serialization_output_allow_all if defined?(@serialization_output_allow_all)
|
628
|
-
|
629
|
-
unless can_satisfy_allow
|
630
|
-
serialization_render_not_acceptable(@serialization_output_registrations, not_acceptable_serializer)
|
631
|
-
end
|
632
|
-
end
|
633
|
-
|
634
|
-
def serialization_render_resolved(obj:, identifier:, serializer:, registrations:, options:)
|
635
|
-
links = []
|
636
|
-
vary = []
|
637
|
-
context = SerializationDSL.new(serializer, links, vary, context: self)
|
638
|
-
result = registrations.call(obj, identifier, self, dsl: context)
|
639
|
-
|
640
|
-
if links.any?
|
641
|
-
items = links.map do |l|
|
642
|
-
href_part = "<#{l[:href]}>"
|
643
|
-
tags = l.to_a.reject { |k, _| k == :href }.map { |k, v| "#{k}=#{v}" }
|
644
|
-
([href_part] + tags).join('; ')
|
645
|
-
end
|
646
|
-
response.set_header('Link', items.join(', '))
|
647
|
-
end
|
648
|
-
|
649
|
-
if vary.any?
|
650
|
-
current_vary =
|
651
|
-
(response.headers['Vary'] || '')
|
652
|
-
.split(',')
|
653
|
-
.map(&:strip)
|
654
|
-
.reject(&:empty?)
|
655
|
-
.sort
|
656
|
-
merged_vary = (vary.sort + current_vary).uniq
|
657
|
-
|
658
|
-
response.set_header('Vary', merged_vary.join(', '))
|
659
|
-
end
|
660
|
-
|
661
|
-
if defined? @serialization_wrapping_renderer
|
662
|
-
input = {
|
663
|
-
identifier: identifier,
|
664
|
-
registrations: registrations,
|
665
|
-
output: result,
|
666
|
-
links: links,
|
667
|
-
etag: response.get_header('ETag'),
|
668
|
-
actions: @serialization_available_serializers
|
669
|
-
}
|
670
|
-
wrapped = @serialization_wrapping_renderer.serialize input, '*/*', context: self
|
671
|
-
render body: wrapped
|
672
|
-
|
673
|
-
response.content_type = 'text/html'
|
674
|
-
return
|
675
|
-
end
|
676
|
-
|
677
|
-
if context.serialization_custom_render.nil?
|
678
|
-
status = options.delete(:status)
|
679
|
-
response.status = status if status
|
680
|
-
|
681
|
-
render body: result, **options
|
682
|
-
|
683
|
-
response.content_type = registrations.identifier_for(identifier)
|
684
|
-
else
|
685
|
-
context.serialization_custom_render.call(result)
|
686
|
-
end
|
687
|
-
end
|
688
|
-
end
|
689
|
-
end
|
1
|
+
require 'media_types/serialization/version'
|
2
|
+
require 'media_types/serialization/serializers/common_css'
|
3
|
+
require 'media_types/serialization/serializers/fallback_not_acceptable_serializer'
|
4
|
+
require 'media_types/serialization/serializers/fallback_unsupported_media_type_serializer'
|
5
|
+
require 'media_types/serialization/serializers/input_validation_error_serializer'
|
6
|
+
require 'media_types/serialization/serializers/endpoint_description_serializer'
|
7
|
+
require 'media_types/serialization/serializers/problem_serializer'
|
8
|
+
require 'media_types/serialization/serializers/api_viewer'
|
9
|
+
require 'media_types/problem'
|
10
|
+
|
11
|
+
require 'abstract_controller'
|
12
|
+
require 'action_controller/metal/mime_responds'
|
13
|
+
require 'action_dispatch/http/mime_type'
|
14
|
+
require 'active_support/concern'
|
15
|
+
require 'active_support/core_ext/module/attribute_accessors'
|
16
|
+
require 'active_support/core_ext/object/blank'
|
17
|
+
|
18
|
+
require 'media_types/serialization/utils/accept_header'
|
19
|
+
require 'media_types/serialization/base'
|
20
|
+
require 'media_types/serialization/error'
|
21
|
+
require 'media_types/serialization/serialization_dsl'
|
22
|
+
|
23
|
+
require 'delegate'
|
24
|
+
|
25
|
+
class SerializationSelectorDsl < SimpleDelegator
|
26
|
+
def initialize(controller, selected_serializer)
|
27
|
+
@serializer = selected_serializer
|
28
|
+
self.value = nil
|
29
|
+
self.matched = false
|
30
|
+
super controller
|
31
|
+
end
|
32
|
+
|
33
|
+
attr_accessor :value, :matched
|
34
|
+
|
35
|
+
def serializer(klazz, obj = nil, &block)
|
36
|
+
return if klazz != @serializer
|
37
|
+
|
38
|
+
self.matched = true
|
39
|
+
self.value = block.nil? ? obj : yield
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
module MediaTypes
|
44
|
+
module Serialization
|
45
|
+
|
46
|
+
HEADER_ACCEPT = 'HTTP_ACCEPT'.freeze
|
47
|
+
HEADER_ACCEPT_LANGUAGE = 'HTTP_ACCEPT_LANGUAGE'.freeze
|
48
|
+
|
49
|
+
mattr_accessor :json_encoder, :json_decoder
|
50
|
+
if defined?(::Oj)
|
51
|
+
self.json_encoder = ->(obj) {
|
52
|
+
Oj.dump(obj,
|
53
|
+
mode: :compat,
|
54
|
+
indent: ' ',
|
55
|
+
space: ' ',
|
56
|
+
array_nl: "\n",
|
57
|
+
object_nl: "\n",
|
58
|
+
ascii_only: false,
|
59
|
+
allow_nan: false,
|
60
|
+
symbol_keys: true,
|
61
|
+
allow_nil: false,
|
62
|
+
allow_invalid_unicode: false,
|
63
|
+
array_class: ::Array,
|
64
|
+
create_additions: false,
|
65
|
+
hash_class: ::Hash,
|
66
|
+
nilnil: false,
|
67
|
+
quirks_mode: false
|
68
|
+
)
|
69
|
+
}
|
70
|
+
self.json_decoder = ->(obj) {
|
71
|
+
Oj.load(obj,
|
72
|
+
mode: :compat,
|
73
|
+
ascii_only: false,
|
74
|
+
allow_nan: false,
|
75
|
+
symbol_keys: true,
|
76
|
+
allow_nil: false,
|
77
|
+
allow_invalid_unicode: false,
|
78
|
+
array_class: ::Array,
|
79
|
+
create_additions: false,
|
80
|
+
hash_class: ::Hash,
|
81
|
+
nilnil: false,
|
82
|
+
quirks_mode: false
|
83
|
+
)
|
84
|
+
}
|
85
|
+
else
|
86
|
+
require 'json'
|
87
|
+
self.json_encoder = JSON.method(:pretty_generate)
|
88
|
+
self.json_decoder = ->(txt) {
|
89
|
+
JSON.parse(txt, {
|
90
|
+
symbolize_names: true,
|
91
|
+
allow_nan: false,
|
92
|
+
create_additions: false,
|
93
|
+
object_class: ::Hash,
|
94
|
+
array_class: ::Array,
|
95
|
+
})
|
96
|
+
}
|
97
|
+
end
|
98
|
+
|
99
|
+
extend ActiveSupport::Concern
|
100
|
+
|
101
|
+
# rubocop:disable Metrics/BlockLength
|
102
|
+
class_methods do
|
103
|
+
def not_acceptable_serializer(serializer, **filter_opts)
|
104
|
+
before_action(**filter_opts) do
|
105
|
+
raise SerializersAlreadyFrozenError if defined? @serialization_frozen
|
106
|
+
|
107
|
+
@serialization_not_acceptable_serializer = serializer
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def unsupported_media_type_serializer(serializer, **filter_opts)
|
112
|
+
before_action(**filter_opts) do
|
113
|
+
raise SerializersAlreadyFrozenError if defined? @serialization_frozen
|
114
|
+
|
115
|
+
@serialization_unsupported_media_type_serializer ||= []
|
116
|
+
@serialization_unsupported_media_type_serializer.append(serializer)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def clear_unsupported_media_type_serializer!(**filter_opts)
|
121
|
+
before_action(**filter_opts) do
|
122
|
+
raise SerializersAlreadyFrozenError if defined? @serialization_frozen
|
123
|
+
|
124
|
+
@serialization_unsupported_media_type_serializer = []
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def input_validation_failed_serializer(serializer, **filter_opts)
|
129
|
+
before_action(**filter_opts) do
|
130
|
+
raise SerializersAlreadyFrozenError if defined? @serialization_frozen
|
131
|
+
|
132
|
+
@serialization_input_validation_failed_serializer ||= []
|
133
|
+
@serialization_input_validation_failed_serializer.append(serializer)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def clear_input_validation_failed_serializers!(**filter_opts)
|
138
|
+
before_action(**filter_opts) do
|
139
|
+
raise SerializersAlreadyFrozenError if defined? @serialization_frozen
|
140
|
+
|
141
|
+
@serialization_input_validation_failed_serializer = []
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
##
|
146
|
+
# Allow output serialization using the passed in +serializer+ for the given +view+
|
147
|
+
#
|
148
|
+
# @see #freeze_io!
|
149
|
+
#
|
150
|
+
# @param serializer the serializer to use for serialization.
|
151
|
+
# @param [(String | NilClass|)] view the view it should use the serializer for. Use nil for no view
|
152
|
+
# @param [(String | NilClass|)[]|NilClass] views the views it should use the serializer for. Use nil for no view
|
153
|
+
#
|
154
|
+
def allow_output_serializer(serializer, view: nil, views: nil, **filter_opts)
|
155
|
+
raise SerializersAlreadyFrozenError if defined? @serialization_frozen
|
156
|
+
raise ArrayInViewParameterError, :allow_output_serializer if view.is_a? Array
|
157
|
+
|
158
|
+
views = [view] if views.nil?
|
159
|
+
raise ViewsNotAnArrayError unless views.is_a? Array
|
160
|
+
|
161
|
+
before_action do
|
162
|
+
@serialization_available_serializers ||= {}
|
163
|
+
@serialization_available_serializers[:output] ||= {}
|
164
|
+
actions = filter_opts[:only] || :all_actions
|
165
|
+
actions = [actions] unless actions.is_a?(Array)
|
166
|
+
actions.each do |action|
|
167
|
+
@serialization_available_serializers[:output][action.to_s] ||= []
|
168
|
+
views.each do |v|
|
169
|
+
@serialization_available_serializers[:output][action.to_s].push({serializer: serializer, view: v})
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
before_action(**filter_opts) do
|
175
|
+
raise SerializersAlreadyFrozenError if defined? @serialization_frozen
|
176
|
+
|
177
|
+
@serialization_output_registrations ||= SerializationRegistration.new(:output)
|
178
|
+
|
179
|
+
mergeable_outputs = serializer.outputs_for(views: views)
|
180
|
+
raise AddedEmptyOutputSerializer, serializer.name if mergeable_outputs.registrations.empty?
|
181
|
+
|
182
|
+
@serialization_output_registrations = @serialization_output_registrations.merge(mergeable_outputs)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def allow_output_html(as: nil, view: nil, layout: nil, formats: [:html], variants: nil, **filter_opts)
|
187
|
+
before_action(**filter_opts) do
|
188
|
+
raise SerializersAlreadyFrozenError if defined? @serialization_frozen
|
189
|
+
|
190
|
+
@serialization_output_registrations ||= SerializationRegistration.new(:output)
|
191
|
+
|
192
|
+
html_registration = SerializationRegistration.new(:output)
|
193
|
+
output_identifier = 'text/html'
|
194
|
+
output_identifier += "; variant=#{as}" unless as.nil?
|
195
|
+
|
196
|
+
validator = FakeValidator.new(as.nil? ? 'text/html' : as)
|
197
|
+
|
198
|
+
block = lambda { |victim, _, controller|
|
199
|
+
options = {}
|
200
|
+
options[:layout] = layout unless layout.nil?
|
201
|
+
options[:template] = view unless view.nil?
|
202
|
+
options[:formats] = formats unless formats.nil?
|
203
|
+
options[:variants] = variants unless variants.nil?
|
204
|
+
options[:locals] = victim if victim.is_a?(::Hash)
|
205
|
+
options[:assigns] = { media: victim }
|
206
|
+
|
207
|
+
controller.render_to_string(**options)
|
208
|
+
}
|
209
|
+
|
210
|
+
html_registration.register_block(nil, validator, nil, block, true, wildcards: true)
|
211
|
+
html_registration.registrations[validator.identifier].display_identifier = output_identifier
|
212
|
+
html_registration.registrations["#{validator.identifier.split('/')[0]}/*"].display_identifier = output_identifier
|
213
|
+
html_registration.registrations['*/*'].display_identifier = output_identifier
|
214
|
+
|
215
|
+
@serialization_output_registrations = @serialization_output_registrations.merge(html_registration)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
def allow_output_docs(description, **filter_opts)
|
220
|
+
before_action(**filter_opts) do
|
221
|
+
raise SerializersAlreadyFrozenError if defined? @serialization_frozen
|
222
|
+
|
223
|
+
@serialization_output_registrations ||= SerializationRegistration.new(:output)
|
224
|
+
|
225
|
+
docs_registration = SerializationRegistration.new(:output)
|
226
|
+
validator = FakeValidator.new('text/vnd.delftsolutions.docs')
|
227
|
+
|
228
|
+
block = lambda { |_, _, _|
|
229
|
+
description
|
230
|
+
}
|
231
|
+
|
232
|
+
docs_registration.register_block(nil, validator, nil, block, true, wildcards: true)
|
233
|
+
docs_registration.registrations['text/vnd.delftsolutions.docs'].display_identifier = 'text/plain; charset=utf-8'
|
234
|
+
docs_registration.registrations['text/*'].display_identifier = 'text/plain; charset=utf-8'
|
235
|
+
docs_registration.registrations['*/*'].display_identifier = 'text/plain; charset=utf-8'
|
236
|
+
|
237
|
+
@serialization_output_registrations = @serialization_output_registrations.merge(docs_registration)
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
def allow_api_viewer(serializer: MediaTypes::Serialization::Serializers::ApiViewer, **filter_opts)
|
242
|
+
before_action do
|
243
|
+
@serialization_api_viewer_enabled ||= {}
|
244
|
+
actions = filter_opts[:only] || :all_actions
|
245
|
+
actions = [actions] unless actions.kind_of?(Array)
|
246
|
+
actions.each do |action|
|
247
|
+
@serialization_api_viewer_enabled[action.to_s] = true
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
before_action(**filter_opts) do
|
252
|
+
if request.query_parameters['api_viewer']
|
253
|
+
@serialization_override_accept = request.query_parameters['api_viewer'].sub ' ', '+'
|
254
|
+
@serialization_wrapping_renderer = serializer
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
##
|
260
|
+
# Allow input serialization using the passed in +serializer+ for the given +view+
|
261
|
+
#
|
262
|
+
# @see #freeze_io!
|
263
|
+
#
|
264
|
+
# @param serializer the serializer to use for deserialization
|
265
|
+
# @param [(String | NilClass|)] view the view it should serializer for. Use nil for no view
|
266
|
+
# @param [(String | NilClass|)[]|NilClass] views the views it should serializer for. Use nil for no view
|
267
|
+
#
|
268
|
+
def allow_input_serializer(serializer, view: nil, views: nil, **filter_opts)
|
269
|
+
raise SerializersAlreadyFrozenError if defined? @serialization_frozen
|
270
|
+
raise ArrayInViewParameterError, :allow_input_serializer if view.is_a? Array
|
271
|
+
views = [view] if views.nil?
|
272
|
+
raise ViewsNotAnArrayError unless views.is_a? Array
|
273
|
+
|
274
|
+
before_action do
|
275
|
+
@serialization_available_serializers ||= {}
|
276
|
+
@serialization_available_serializers[:input] ||= {}
|
277
|
+
actions = filter_opts[:only] || :all_actions
|
278
|
+
actions = [actions] unless actions.is_a?(Array)
|
279
|
+
actions.each do |action|
|
280
|
+
@serialization_available_serializers[:input][action.to_s] ||= []
|
281
|
+
views.each do |v|
|
282
|
+
@serialization_available_serializers[:input][action.to_s].push({serializer: serializer, view: v})
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
before_action(**filter_opts) do
|
288
|
+
raise SerializersAlreadyFrozenError if defined? @serialization_frozen
|
289
|
+
|
290
|
+
@serialization_input_registrations ||= SerializationRegistration.new(:input)
|
291
|
+
|
292
|
+
mergeable_inputs = serializer.inputs_for(views: views)
|
293
|
+
raise AddedEmptyInputSerializer, serializer.name if mergeable_inputs.registrations.empty?
|
294
|
+
|
295
|
+
@serialization_input_registrations = @serialization_input_registrations.merge(mergeable_inputs)
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
def allow_all_output(**filter_opts)
|
300
|
+
before_action(**filter_opts) do
|
301
|
+
@serialization_output_registrations ||= SerializationRegistration.new(:output)
|
302
|
+
@serialization_output_allow_all ||= true
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
def allow_all_input(**filter_opts)
|
307
|
+
before_action(**filter_opts) do
|
308
|
+
@serialization_input_registrations ||= SerializationRegistration.new(:input)
|
309
|
+
@serialization_input_allow_all ||= true
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
##
|
314
|
+
# Freezes additions to the serializes and notifies the controller what it will be able to respond to.
|
315
|
+
#
|
316
|
+
def freeze_io!(**filter_opts)
|
317
|
+
before_action :serializer_freeze_io_internal, **filter_opts
|
318
|
+
|
319
|
+
output_error MediaTypes::Serialization::NoInputReceivedError do |p, _error|
|
320
|
+
p.title 'Providing input is mandatory. Please set a Content-Type', lang: 'en'
|
321
|
+
|
322
|
+
p.status_code :bad_request
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
def output_error(klazz, serializers: [])
|
327
|
+
rescue_from klazz do |error|
|
328
|
+
problem = Problem.new(error)
|
329
|
+
instance_exec { yield problem, error, self } if block_given?
|
330
|
+
|
331
|
+
serializer = MediaTypes::Serialization::Serializers::ProblemSerializer
|
332
|
+
registrations = serializer.outputs_for(views: [:html, nil])
|
333
|
+
|
334
|
+
render_media(
|
335
|
+
problem,
|
336
|
+
serializers: [registrations] + serializers.map { |s| s.outputs_for(views: [nil])},
|
337
|
+
status: problem.response_status_code
|
338
|
+
)
|
339
|
+
end
|
340
|
+
end
|
341
|
+
end
|
342
|
+
# rubocop:enable Metrics/BlockLength
|
343
|
+
|
344
|
+
protected
|
345
|
+
|
346
|
+
def serialize(victim, media_type, serializer: Object.new, links: [], vary: ['Accept'])
|
347
|
+
context = SerializationDSL.new(serializer, links, vary, context: self)
|
348
|
+
context.instance_exec { @serialization_output_registrations.call(victim, media_type, context) }
|
349
|
+
end
|
350
|
+
|
351
|
+
MEDIA_TYPES_SERIALIZATION_OBJ_IS_UNDEFINED = ::Object.new
|
352
|
+
|
353
|
+
def render_media(obj = MEDIA_TYPES_SERIALIZATION_OBJ_IS_UNDEFINED, **options, &block)
|
354
|
+
serializers = options.delete(:serializers)
|
355
|
+
not_acceptable_serializer = options.delete(:not_acceptable_serializer)
|
356
|
+
|
357
|
+
if obj == MEDIA_TYPES_SERIALIZATION_OBJ_IS_UNDEFINED && options.keys.any? && !block
|
358
|
+
# options is too greedy :(
|
359
|
+
obj = options
|
360
|
+
options = {}
|
361
|
+
end
|
362
|
+
|
363
|
+
if obj == MEDIA_TYPES_SERIALIZATION_OBJ_IS_UNDEFINED && block.nil?
|
364
|
+
raise 'render_media was called without an object. Please provide one or supply a block to match the serializer.'
|
365
|
+
end
|
366
|
+
|
367
|
+
obj = nil if obj == MEDIA_TYPES_SERIALIZATION_OBJ_IS_UNDEFINED
|
368
|
+
|
369
|
+
raise SerializersNotFrozenError unless defined? @serialization_frozen
|
370
|
+
|
371
|
+
if defined? @serialization_not_acceptable_serializer
|
372
|
+
not_acceptable_serializer ||= @serialization_not_acceptable_serializer
|
373
|
+
end
|
374
|
+
|
375
|
+
@serialization_output_registrations ||= SerializationRegistration.new(:output)
|
376
|
+
registration = @serialization_output_registrations
|
377
|
+
unless serializers.nil?
|
378
|
+
registration = SerializationRegistration.new(:output)
|
379
|
+
serializers.each do |s|
|
380
|
+
registration = registration.merge(s)
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
identifier = resolve_media_type(request, registration)
|
385
|
+
|
386
|
+
if identifier.nil?
|
387
|
+
serialization_render_not_acceptable(registration, not_acceptable_serializer)
|
388
|
+
return
|
389
|
+
end
|
390
|
+
|
391
|
+
serializer = resolve_serializer(request, identifier, registration)
|
392
|
+
|
393
|
+
unless block.nil?
|
394
|
+
selector = SerializationSelectorDsl.new(self, serializer)
|
395
|
+
selector.instance_exec(&block)
|
396
|
+
|
397
|
+
raise UnmatchedSerializerError, serializer unless selector.matched
|
398
|
+
|
399
|
+
obj = selector.value
|
400
|
+
end
|
401
|
+
|
402
|
+
serialization_render_resolved(
|
403
|
+
obj: obj,
|
404
|
+
serializer: serializer,
|
405
|
+
identifier: identifier,
|
406
|
+
registrations: registration,
|
407
|
+
options: options
|
408
|
+
)
|
409
|
+
end
|
410
|
+
|
411
|
+
def deserialize(request)
|
412
|
+
raise SerializersNotFrozenError unless defined?(@serialization_frozen)
|
413
|
+
|
414
|
+
result = nil
|
415
|
+
begin
|
416
|
+
result = deserialize!(request)
|
417
|
+
rescue NoInputReceivedError
|
418
|
+
return nil
|
419
|
+
end
|
420
|
+
result
|
421
|
+
end
|
422
|
+
|
423
|
+
def deserialize!(request)
|
424
|
+
raise SerializersNotFrozenError unless defined?(@serialization_frozen)
|
425
|
+
raise NoInputReceivedError if request.content_type.blank?
|
426
|
+
raise InputNotAcceptableError unless @serialization_input_registrations.has? request.content_type
|
427
|
+
|
428
|
+
@serialization_input_registrations.call(@serialization_decoded_input, request.content_type, self)
|
429
|
+
end
|
430
|
+
|
431
|
+
def resolve_serializer(request, identifier = nil, registration = @serialization_output_registrations)
|
432
|
+
identifier = resolve_media_type(request, registration) if identifier.nil?
|
433
|
+
return nil if identifier.nil?
|
434
|
+
|
435
|
+
registration = registration.registrations[identifier]
|
436
|
+
|
437
|
+
raise 'Assertion failed, inconsistent answer from resolve_media_type' if registration.nil?
|
438
|
+
|
439
|
+
registration.serializer
|
440
|
+
end
|
441
|
+
|
442
|
+
private
|
443
|
+
|
444
|
+
def resolve_media_type(request, registration, allow_last: true)
|
445
|
+
if defined? @serialization_override_accept
|
446
|
+
if allow_last && @serialization_override_accept == 'last'
|
447
|
+
@serialization_override_accept = registration.registrations.keys.last
|
448
|
+
end
|
449
|
+
|
450
|
+
return @serialization_override_accept if registration.has? @serialization_override_accept
|
451
|
+
|
452
|
+
# Always render problems in api viewer if we can't show chosen override
|
453
|
+
return 'application/problem+json' if registration.has? 'application/problem+json'
|
454
|
+
|
455
|
+
return nil
|
456
|
+
end
|
457
|
+
|
458
|
+
# Ruby negotiation
|
459
|
+
#
|
460
|
+
# This is similar to the respond_to logic. It sorts the accept values and tries to match against each option.
|
461
|
+
#
|
462
|
+
#
|
463
|
+
|
464
|
+
accept_header = Utils::AcceptHeader.new(request.get_header(HEADER_ACCEPT)) || ''
|
465
|
+
accept_header.each do |mime_type|
|
466
|
+
stripped = mime_type.to_s.split(';')[0]
|
467
|
+
next unless registration.has? stripped
|
468
|
+
|
469
|
+
return stripped
|
470
|
+
end
|
471
|
+
|
472
|
+
nil
|
473
|
+
end
|
474
|
+
|
475
|
+
def serialization_render_not_acceptable(registrations, override = nil)
|
476
|
+
serializer = override
|
477
|
+
serializer ||= MediaTypes::Serialization::Serializers::FallbackNotAcceptableSerializer
|
478
|
+
identifier = serializer.validator.identifier
|
479
|
+
obj = { request: request, registrations: registrations }
|
480
|
+
new_registrations = serializer.outputs_for(views: [nil])
|
481
|
+
|
482
|
+
serialization_render_resolved(
|
483
|
+
obj: obj,
|
484
|
+
serializer: serializer,
|
485
|
+
identifier: identifier,
|
486
|
+
registrations: new_registrations,
|
487
|
+
options: {}
|
488
|
+
)
|
489
|
+
|
490
|
+
response.status = :not_acceptable
|
491
|
+
end
|
492
|
+
|
493
|
+
def serializer_freeze_io_internal
|
494
|
+
raise UnableToRefreezeError if defined? @serialization_frozen
|
495
|
+
|
496
|
+
@serialization_frozen = true
|
497
|
+
@serialization_input_registrations ||= SerializationRegistration.new(:input)
|
498
|
+
|
499
|
+
raise NoOutputSerializersDefinedError unless defined? @serialization_output_registrations
|
500
|
+
|
501
|
+
# Input content-type negotiation and validation
|
502
|
+
all_allowed = false
|
503
|
+
all_allowed ||= @serialization_input_allow_all if defined?(@serialization_input_allow_all)
|
504
|
+
|
505
|
+
input_is_allowed = true
|
506
|
+
input_is_allowed = @serialization_input_registrations.has? request.content_type unless request.content_type.blank?
|
507
|
+
|
508
|
+
unless input_is_allowed || all_allowed
|
509
|
+
serializers = @serialization_unsupported_media_type_serializer || [
|
510
|
+
MediaTypes::Serialization::Serializers::ProblemSerializer,
|
511
|
+
MediaTypes::Serialization::Serializers::FallbackUnsupportedMediaTypeSerializer
|
512
|
+
]
|
513
|
+
registrations = SerializationRegistration.new(:output)
|
514
|
+
serializers.each do |s|
|
515
|
+
registrations = registrations.merge(s.outputs_for(views: [nil, :html]))
|
516
|
+
end
|
517
|
+
|
518
|
+
input = {
|
519
|
+
registrations: @serialization_input_registrations
|
520
|
+
}
|
521
|
+
|
522
|
+
render_media nil, serializers: [registrations], status: :unsupported_media_type do
|
523
|
+
serializer MediaTypes::Serialization::Serializers::FallbackUnsupportedMediaTypeSerializer, input
|
524
|
+
serializer MediaTypes::Serialization::Serializers::ProblemSerializer do
|
525
|
+
error = UnsupportedMediaTypeError.new(input[:registrations].registrations.keys)
|
526
|
+
problem = Problem.new(error)
|
527
|
+
problem.title 'Unable to process your body Content-Type.', lang: 'en'
|
528
|
+
|
529
|
+
problem
|
530
|
+
end
|
531
|
+
end
|
532
|
+
return
|
533
|
+
end
|
534
|
+
|
535
|
+
if input_is_allowed && !request.content_type.blank?
|
536
|
+
begin
|
537
|
+
input_data = request.body.read
|
538
|
+
@serialization_decoded_input = @serialization_input_registrations.decode(
|
539
|
+
input_data,
|
540
|
+
request.content_type,
|
541
|
+
self
|
542
|
+
)
|
543
|
+
rescue InputValidationFailedError => e
|
544
|
+
serializers = if defined?(@serialization_input_validation_failed_serializer)
|
545
|
+
@serialization_input_validation_failed_serializer
|
546
|
+
else
|
547
|
+
[
|
548
|
+
MediaTypes::Serialization::Serializers::ProblemSerializer,
|
549
|
+
MediaTypes::Serialization::Serializers::InputValidationErrorSerializer
|
550
|
+
]
|
551
|
+
end
|
552
|
+
registrations = SerializationRegistration.new(:output)
|
553
|
+
serializers.each do |s|
|
554
|
+
registrations = registrations.merge(s.outputs_for(views: [nil, :html]))
|
555
|
+
end
|
556
|
+
|
557
|
+
input = {
|
558
|
+
identifier: request.content_type,
|
559
|
+
input: input_data,
|
560
|
+
error: e
|
561
|
+
}
|
562
|
+
|
563
|
+
render_media nil, serializers: [registrations], status: :bad_request do
|
564
|
+
serializer MediaTypes::Serialization::Serializers::InputValidationErrorSerializer, input
|
565
|
+
serializer MediaTypes::Serialization::Serializers::ProblemSerializer do
|
566
|
+
problem = Problem.new(e)
|
567
|
+
problem.title 'Input failed to validate.', lang: 'en'
|
568
|
+
|
569
|
+
problem
|
570
|
+
end
|
571
|
+
end
|
572
|
+
return
|
573
|
+
end
|
574
|
+
end
|
575
|
+
|
576
|
+
# Endpoint description media type
|
577
|
+
|
578
|
+
description_serializer = MediaTypes::Serialization::Serializers::EndpointDescriptionSerializer
|
579
|
+
|
580
|
+
# All endpoints have endpoint description.
|
581
|
+
# Placed in front of the list to make sure the api viewer doesn't pick it.
|
582
|
+
@serialization_output_registrations =
|
583
|
+
description_serializer
|
584
|
+
.outputs_for(views: [nil])
|
585
|
+
.merge(@serialization_output_registrations)
|
586
|
+
|
587
|
+
endpoint_matched_identifier = resolve_media_type(
|
588
|
+
request,
|
589
|
+
description_serializer.serializer_output_registrations[nil],
|
590
|
+
allow_last: false
|
591
|
+
)
|
592
|
+
|
593
|
+
if endpoint_matched_identifier
|
594
|
+
# We picked an endpoint description media type
|
595
|
+
#
|
596
|
+
@serialization_available_serializers ||= {}
|
597
|
+
@serialization_available_serializers[:output] ||= {}
|
598
|
+
@serialization_api_viewer_enabled ||= {}
|
599
|
+
|
600
|
+
input = {
|
601
|
+
api_viewer: @serialization_api_viewer_enabled,
|
602
|
+
actions: @serialization_available_serializers
|
603
|
+
}
|
604
|
+
|
605
|
+
serialization_render_resolved(
|
606
|
+
obj: input,
|
607
|
+
serializer: description_serializer,
|
608
|
+
identifier: endpoint_matched_identifier,
|
609
|
+
registrations: @serialization_output_registrations,
|
610
|
+
options: {}
|
611
|
+
)
|
612
|
+
return
|
613
|
+
end
|
614
|
+
|
615
|
+
# Output content negotiation
|
616
|
+
resolved_identifier = resolve_media_type(request, @serialization_output_registrations)
|
617
|
+
|
618
|
+
not_acceptable_serializer = nil
|
619
|
+
|
620
|
+
if defined? @serialization_not_acceptable_serializer
|
621
|
+
not_acceptable_serializer = @serialization_not_acceptable_serializer
|
622
|
+
end
|
623
|
+
|
624
|
+
not_acceptable_serializer ||= MediaTypes::Serialization::Serializers::FallbackNotAcceptableSerializer
|
625
|
+
|
626
|
+
can_satisfy_allow = !resolved_identifier.nil?
|
627
|
+
can_satisfy_allow ||= @serialization_output_allow_all if defined?(@serialization_output_allow_all)
|
628
|
+
|
629
|
+
unless can_satisfy_allow
|
630
|
+
serialization_render_not_acceptable(@serialization_output_registrations, not_acceptable_serializer)
|
631
|
+
end
|
632
|
+
end
|
633
|
+
|
634
|
+
def serialization_render_resolved(obj:, identifier:, serializer:, registrations:, options:)
|
635
|
+
links = []
|
636
|
+
vary = []
|
637
|
+
context = SerializationDSL.new(serializer, links, vary, context: self)
|
638
|
+
result = registrations.call(obj, identifier, self, dsl: context)
|
639
|
+
|
640
|
+
if links.any?
|
641
|
+
items = links.map do |l|
|
642
|
+
href_part = "<#{l[:href]}>"
|
643
|
+
tags = l.to_a.reject { |k, _| k == :href }.map { |k, v| "#{k}=#{v}" }
|
644
|
+
([href_part] + tags).join('; ')
|
645
|
+
end
|
646
|
+
response.set_header('Link', items.join(', '))
|
647
|
+
end
|
648
|
+
|
649
|
+
if vary.any?
|
650
|
+
current_vary =
|
651
|
+
(response.headers['Vary'] || '')
|
652
|
+
.split(',')
|
653
|
+
.map(&:strip)
|
654
|
+
.reject(&:empty?)
|
655
|
+
.sort
|
656
|
+
merged_vary = (vary.sort + current_vary).uniq
|
657
|
+
|
658
|
+
response.set_header('Vary', merged_vary.join(', '))
|
659
|
+
end
|
660
|
+
|
661
|
+
if defined? @serialization_wrapping_renderer
|
662
|
+
input = {
|
663
|
+
identifier: identifier,
|
664
|
+
registrations: registrations,
|
665
|
+
output: result,
|
666
|
+
links: links,
|
667
|
+
etag: response.get_header('ETag'),
|
668
|
+
actions: @serialization_available_serializers
|
669
|
+
}
|
670
|
+
wrapped = @serialization_wrapping_renderer.serialize input, '*/*', context: self
|
671
|
+
render body: wrapped
|
672
|
+
|
673
|
+
response.content_type = 'text/html'
|
674
|
+
return
|
675
|
+
end
|
676
|
+
|
677
|
+
if context.serialization_custom_render.nil?
|
678
|
+
status = options.delete(:status)
|
679
|
+
response.status = status if status
|
680
|
+
|
681
|
+
render body: result, **options
|
682
|
+
|
683
|
+
response.content_type = registrations.identifier_for(identifier)
|
684
|
+
else
|
685
|
+
context.serialization_custom_render.call(result)
|
686
|
+
end
|
687
|
+
end
|
688
|
+
end
|
689
|
+
end
|