media_types-serialization 0.8.0 → 1.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +16 -3
  3. data/.prettierrc +1 -0
  4. data/CHANGELOG.md +42 -0
  5. data/CODE_OF_CONDUCT.md +74 -74
  6. data/Gemfile.lock +74 -83
  7. data/README.md +691 -179
  8. data/lib/media_types/problem.rb +64 -0
  9. data/lib/media_types/serialization.rb +497 -173
  10. data/lib/media_types/serialization/base.rb +115 -91
  11. data/lib/media_types/serialization/error.rb +186 -0
  12. data/lib/media_types/serialization/fake_validator.rb +52 -0
  13. data/lib/media_types/serialization/serialization_dsl.rb +117 -0
  14. data/lib/media_types/serialization/serialization_registration.rb +245 -0
  15. data/lib/media_types/serialization/serializers/api_viewer.rb +133 -0
  16. data/lib/media_types/serialization/serializers/common_css.rb +168 -0
  17. data/lib/media_types/serialization/serializers/endpoint_description_serializer.rb +80 -0
  18. data/lib/media_types/serialization/serializers/fallback_not_acceptable_serializer.rb +85 -0
  19. data/lib/media_types/serialization/serializers/fallback_unsupported_media_type_serializer.rb +58 -0
  20. data/lib/media_types/serialization/serializers/input_validation_error_serializer.rb +89 -0
  21. data/lib/media_types/serialization/serializers/problem_serializer.rb +100 -0
  22. data/lib/media_types/serialization/utils/accept_header.rb +77 -0
  23. data/lib/media_types/serialization/utils/accept_language_header.rb +82 -0
  24. data/lib/media_types/serialization/utils/header_list.rb +89 -0
  25. data/lib/media_types/serialization/version.rb +1 -1
  26. data/media_types-serialization.gemspec +48 -50
  27. metadata +48 -79
  28. data/.travis.yml +0 -17
  29. data/lib/generators/media_types/serialization/api_viewer/api_viewer_generator.rb +0 -25
  30. data/lib/generators/media_types/serialization/api_viewer/templates/api_viewer.html.erb +0 -98
  31. data/lib/generators/media_types/serialization/api_viewer/templates/initializer.rb +0 -33
  32. data/lib/generators/media_types/serialization/api_viewer/templates/template_controller.rb +0 -23
  33. data/lib/media_types/serialization/media_type/register.rb +0 -4
  34. data/lib/media_types/serialization/migrations_command.rb +0 -38
  35. data/lib/media_types/serialization/migrations_support.rb +0 -50
  36. data/lib/media_types/serialization/mime_type_support.rb +0 -64
  37. data/lib/media_types/serialization/no_content_type_given.rb +0 -11
  38. data/lib/media_types/serialization/no_media_type_serializers.rb +0 -11
  39. data/lib/media_types/serialization/no_serializer_for_content_type.rb +0 -15
  40. data/lib/media_types/serialization/renderer.rb +0 -41
  41. data/lib/media_types/serialization/renderer/register.rb +0 -4
  42. data/lib/media_types/serialization/wrapper.rb +0 -13
  43. data/lib/media_types/serialization/wrapper/html_wrapper.rb +0 -45
  44. data/lib/media_types/serialization/wrapper/media_collection_wrapper.rb +0 -59
  45. data/lib/media_types/serialization/wrapper/media_index_wrapper.rb +0 -59
  46. data/lib/media_types/serialization/wrapper/media_object_wrapper.rb +0 -55
  47. data/lib/media_types/serialization/wrapper_support.rb +0 -38
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb'
4
+
5
+ module MediaTypes
6
+ class Problem
7
+
8
+ def initialize(error)
9
+ self.error = error
10
+ self.translations = {}
11
+ self.custom_attributes = {}
12
+ self.response_status_code = 400
13
+ end
14
+
15
+ attr_accessor :error, :translations, :custom_type, :custom_attributes, :response_status_code
16
+
17
+ def type
18
+ return custom_type unless custom_type.nil?
19
+
20
+ "https://docs.delftsolutions.nl/wiki/Error/#{ERB::Util::url_encode(error.class.name)}"
21
+ end
22
+
23
+ def url(href)
24
+ self.custom_type = href
25
+ end
26
+
27
+ def title(title, lang:)
28
+ translations[lang] ||= {}
29
+ translations[lang][:title] = title
30
+ end
31
+
32
+ def override_detail(detail, lang:)
33
+ raise 'Unable to override detail message without having a title in the same language.' unless translations[lang]
34
+ translations[lang][:detail] = title
35
+ end
36
+
37
+ def attribute(name, value)
38
+ str_name = name.to_s
39
+
40
+ raise "Unable to add an attribute with name '#{str_name}'. Name should start with a letter, consist of the letters A-Z, a-z, 0-9 or _ and be at least 3 characters long." unless str_name =~ /^[a-zA-Z][a-zA-Z0-9_]{2,}$/
41
+
42
+ custom_attributes[str_name] = value
43
+ end
44
+
45
+ def status_code(code)
46
+ code = Rack::Utils::SYMBOL_TO_STATUS_CODE[code] if code.is_a? Symbol
47
+
48
+ self.response_status_code = code
49
+ end
50
+
51
+ def instance
52
+ return nil unless custom_type.nil?
53
+
54
+ inner = error.cause
55
+ return nil if inner.nil?
56
+
57
+ "https://docs.delftsolutions.nl/wiki/Error/#{ERB::Util::url_encode(inner.class.name)}"
58
+ end
59
+
60
+ def languages
61
+ translations.keys
62
+ end
63
+ end
64
+ end
@@ -1,4 +1,12 @@
1
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'
2
10
 
3
11
  require 'abstract_controller'
4
12
  require 'action_controller/metal/mime_responds'
@@ -7,139 +15,320 @@ require 'active_support/concern'
7
15
  require 'active_support/core_ext/module/attribute_accessors'
8
16
  require 'active_support/core_ext/object/blank'
9
17
 
10
- require 'http_headers/accept'
11
-
12
- require 'media_types/serialization/no_media_type_serializers'
13
- require 'media_types/serialization/no_serializer_for_content_type'
18
+ require 'media_types/serialization/utils/accept_header'
14
19
  require 'media_types/serialization/base'
15
- require 'media_types/serialization/wrapper/html_wrapper'
16
-
17
- require 'awesome_print'
20
+ require 'media_types/serialization/error'
21
+ require 'media_types/serialization/serialization_dsl'
18
22
 
19
23
  require 'delegate'
20
24
 
21
- class MediaTypeApiViewer < SimpleDelegator
22
- def initialize(inner_media)
23
- super inner_media
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
24
31
  end
25
32
 
26
- def to_s
27
- 'application/vnd.xpbytes.api-viewer.v1'
28
- end
33
+ attr_accessor :value, :matched
29
34
 
30
- def serialize_as
31
- __getobj__
35
+ def serializer(klazz, obj = nil, &block)
36
+ return if klazz != @serializer
37
+
38
+ self.matched = true
39
+ self.value = block.nil? ? obj : yield
32
40
  end
33
41
  end
34
42
 
35
43
  module MediaTypes
36
44
  module Serialization
37
45
 
38
- mattr_accessor :common_suffix, :collect_links_for_collection, :collect_links_for_index,
39
- :html_wrapper_layout, :api_viewer_layout
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
40
98
 
41
99
  extend ActiveSupport::Concern
42
100
 
43
- HEADER_ACCEPT = 'HTTP_ACCEPT'
44
-
45
- MEDIA_TYPE_HTML = 'text/html'
46
- MEDIA_TYPE_API_VIEWER = 'application/vnd.xpbytes.api-viewer.v1'
47
-
48
101
  # rubocop:disable Metrics/BlockLength
49
102
  class_methods do
50
103
 
51
- ##
52
- # Accept serialization using the passed in +serializer+ for the given +view+
53
- #
54
- # By default will also accept the first call to this as HTML
55
- # By default will also accept the first call to this as Api Viewer
56
- #
57
- # @see #freeze_accepted_media!
58
- #
59
- # @param serializer the serializer to use for serialization. Needs to respond to #to_body, but may respond to
60
- # #to_json if the type accepted is ...+json, or #to_xml if the type accepted is ...+xml or #to_html if the type
61
- # accepted is text/html
62
- # @param [(String | NilClass|)[]] view the views it should serializer for. Use nil for no view
63
- # @param [Boolean] accept_api_viewer if true, accepts this serializer as base for the api viewer
64
- # @param [Boolean] accept_html if true, accepts this serializer as the html fallback
65
- #
66
- def accept_serialization(serializer, view: [nil], accept_api_viewer: true, accept_html: accept_api_viewer, **filter_opts)
104
+ def not_acceptable_serializer(serializer, **filter_opts)
67
105
  before_action(**filter_opts) do
68
- resolved_media_types(serializer, view: view) do |media_type, media_view, _, register|
69
- opts = { media_type: media_type, media_view: media_view }
70
- register.call(String(media_type), wrap_media(serializer, **opts))
71
- end
106
+ raise SerializersAlreadyFrozenError if defined? @serialization_frozen
107
+
108
+ @serialization_not_acceptable_serializer = serializer
72
109
  end
110
+ end
111
+
112
+ def unsupported_media_type_serializer(serializer, **filter_opts)
113
+ before_action(**filter_opts) do
114
+ raise SerializersAlreadyFrozenError if defined? @serialization_frozen
73
115
 
74
- accept_html(serializer, view: view, overwrite: false, **filter_opts) if accept_html
75
- accept_api_viewer(serializer, view: view, overwrite: false, **filter_opts) if accept_api_viewer
116
+ @serialization_unsupported_media_type_serializer ||= []
117
+ @serialization_unsupported_media_type_serializer.append(serializer)
118
+ end
119
+ end
120
+
121
+ def clear_unsupported_media_type_serializer!(**filter_opts)
122
+ before_action(**filter_opts) do
123
+ raise SerializersAlreadyFrozenError if defined? @serialization_frozen
124
+
125
+ @serialization_unsupported_media_type_serializer = []
126
+ end
127
+ end
128
+
129
+ def input_validation_failed_serializer(serializer, **filter_opts)
130
+ before_action(**filter_opts) do
131
+ raise SerializersAlreadyFrozenError if defined? @serialization_frozen
132
+
133
+ @serialization_input_validation_failed_serializer ||= []
134
+ @serialization_input_validation_failed_serializer.append(serializer)
135
+ end
136
+ end
137
+
138
+ def clear_input_validation_failed_serializers!(**filter_opts)
139
+ before_action(**filter_opts) do
140
+ raise SerializersAlreadyFrozenError if defined? @serialization_frozen
141
+
142
+ @serialization_input_validation_failed_serializer = []
143
+ end
76
144
  end
77
145
 
78
146
  ##
79
- # Accept serialization using the passed in +serializer+ for the given +view+ as text/html
147
+ # Allow output serialization using the passed in +serializer+ for the given +view+
80
148
  #
81
- # Always overwrites the current acceptor of text/html. The last call to this, for the giben +filter_opts+ will win
82
- # the serialization.
149
+ # @see #freeze_io!
83
150
  #
84
- def accept_html(serializer, view: [nil], overwrite: true, **filter_opts)
85
- before_action(**filter_opts) do
86
- resolved_media_types(serializer, view: view) do |media_type, media_view, registered, register|
87
- break if registered.call(MEDIA_TYPE_HTML) && !overwrite
88
- register.call(MEDIA_TYPE_HTML, wrap_html(serializer, media_view: media_view, media_type: media_type))
151
+ # @param serializer the serializer to use for serialization.
152
+ # @param [(String | NilClass|)] view the view it should use the serializer for. Use nil for no view
153
+ # @param [(String | NilClass|)[]|NilClass] views the views it should use the serializer for. Use nil for no view
154
+ #
155
+ def allow_output_serializer(serializer, view: nil, views: nil, **filter_opts)
156
+ raise SerializersAlreadyFrozenError if defined? @serialization_frozen
157
+ raise ArrayInViewParameterError, :allow_output_serializer if view.is_a? Array
158
+
159
+ views = [view] if views.nil?
160
+ raise ViewsNotAnArrayError unless views.is_a? Array
161
+
162
+ before_action do
163
+ @serialization_available_serializers ||= {}
164
+ @serialization_available_serializers[:output] ||= {}
165
+ actions = filter_opts[:only] || :all_actions
166
+ actions = [actions] unless actions.is_a?(Array)
167
+ actions.each do |action|
168
+ @serialization_available_serializers[:output][action.to_s] ||= []
169
+ views.each do |v|
170
+ @serialization_available_serializers[:output][action.to_s].push({serializer: serializer, view: v})
171
+ end
89
172
  end
90
173
  end
174
+
175
+ before_action(**filter_opts) do
176
+ raise SerializersAlreadyFrozenError if defined? @serialization_frozen
177
+
178
+ @serialization_output_registrations ||= SerializationRegistration.new(:output)
179
+
180
+ mergeable_outputs = serializer.outputs_for(views: views)
181
+ raise AddedEmptyOutputSerializer, serializer.name if mergeable_outputs.registrations.empty?
182
+
183
+ @serialization_output_registrations = @serialization_output_registrations.merge(mergeable_outputs)
184
+ end
91
185
  end
92
186
 
93
- ##
94
- # Same as +accept_html+ but then for Api Viewer
95
- #
96
- def accept_api_viewer(serializer, view: [nil], overwrite: true, **filter_opts)
187
+ def allow_output_html(as: nil, layout: nil, **filter_opts)
97
188
  before_action(**filter_opts) do
98
- fixate_content_type = (params[:api_viewer_media_type] || '').gsub(' ', '+')
99
- resolved_media_types(serializer, view: view) do |media_type, media_view, registered, register|
100
- break if registered.call(MEDIA_TYPE_API_VIEWER) && !overwrite
101
- if fixate_content_type == '' || fixate_content_type == media_type.to_s
102
- wrapped_media_type = MediaTypeApiViewer.new(fixate_content_type.presence || media_type)
103
- register.call(MEDIA_TYPE_API_VIEWER, wrap_html(serializer, media_view: media_view, media_type: wrapped_media_type))
104
- break
189
+ raise SerializersAlreadyFrozenError if defined? @serialization_frozen
190
+
191
+ @serialization_output_registrations ||= SerializationRegistration.new(:output)
192
+
193
+ html_registration = SerializationRegistration.new(:output)
194
+ output_identifier = 'text/html'
195
+ output_identifier += "; variant=#{as}" unless as.nil?
196
+
197
+ validator = FakeValidator.new(as.nil? ? 'text/html' : as)
198
+
199
+ block = lambda { |_, _, controller|
200
+ if layout.nil?
201
+ controller.render_to_string
202
+ else
203
+ controller.render_to_string(layout: layout)
105
204
  end
205
+ }
206
+
207
+ html_registration.register_block(nil, validator, nil, block, true, wildcards: true)
208
+ html_registration.registrations[validator.identifier].display_identifier = output_identifier
209
+ html_registration.registrations["#{validator.identifier.split('/')[0]}/*"].display_identifier = output_identifier
210
+ html_registration.registrations['*/*'].display_identifier = output_identifier
211
+
212
+ @serialization_output_registrations = @serialization_output_registrations.merge(html_registration)
213
+ end
214
+ end
215
+
216
+ def allow_output_docs(description, **filter_opts)
217
+ before_action(**filter_opts) do
218
+ raise SerializersAlreadyFrozenError if defined? @serialization_frozen
219
+
220
+ @serialization_output_registrations ||= SerializationRegistration.new(:output)
221
+
222
+ docs_registration = SerializationRegistration.new(:output)
223
+ validator = FakeValidator.new('text/vnd.delftsolutions.docs')
224
+
225
+ block = lambda { |_, _, _|
226
+ description
227
+ }
228
+
229
+ docs_registration.register_block(nil, validator, nil, block, true, wildcards: true)
230
+ docs_registration.registrations['text/vnd.delftsolutions.docs'].display_identifier = 'text/plain; charset=utf-8'
231
+ docs_registration.registrations['text/*'].display_identifier = 'text/plain; charset=utf-8'
232
+ docs_registration.registrations['*/*'].display_identifier = 'text/plain; charset=utf-8'
233
+
234
+ @serialization_output_registrations = @serialization_output_registrations.merge(docs_registration)
235
+ end
236
+ end
237
+
238
+ def allow_api_viewer(serializer: MediaTypes::Serialization::Serializers::ApiViewer, **filter_opts)
239
+ before_action do
240
+ @serialization_api_viewer_enabled ||= {}
241
+ actions = filter_opts[:only] || :all_actions
242
+ actions = [actions] unless actions.kind_of?(Array)
243
+ actions.each do |action|
244
+ @serialization_api_viewer_enabled[action.to_s] = true
245
+ end
246
+ end
247
+
248
+ before_action(**filter_opts) do
249
+ if request.query_parameters['api_viewer']
250
+ @serialization_override_accept = request.query_parameters['api_viewer'].sub ' ', '+'
251
+ @serialization_wrapping_renderer = serializer
106
252
  end
107
253
  end
108
254
  end
109
255
 
110
256
  ##
111
- # Register a mime type, but explicitly notify that it can't be serialized.
112
- # This is done for file serving and redirects.
257
+ # Allow input serialization using the passed in +serializer+ for the given +view+
113
258
  #
114
- # @param [Symbol] mimes takes a list of symbols that should resolve through Mime::Type
259
+ # @see #freeze_io!
115
260
  #
116
- # @see #freeze_accepted_media!
261
+ # @param serializer the serializer to use for deserialization
262
+ # @param [(String | NilClass|)] view the view it should serializer for. Use nil for no view
263
+ # @param [(String | NilClass|)[]|NilClass] views the views it should serializer for. Use nil for no view
117
264
  #
118
- # @example fingerpint binary format
119
- #
120
- # no_serializer_for :fingerprint_bin, :fingerprint_deprecated_bin
121
- #
122
- def accept_without_serialization(*mimes, **filter_opts)
123
- before_action(**filter_opts) do
124
- self.serializers = Hash(serializers)
125
- mimes.each do |mime|
126
- media_type = Mime::Type.lookup_by_extension(mime) || mime
127
- serializers[String(media_type)] = nil
265
+ def allow_input_serializer(serializer, view: nil, views: nil, **filter_opts)
266
+ raise SerializersAlreadyFrozenError if defined? @serialization_frozen
267
+ raise ArrayInViewParameterError, :allow_input_serializer if view.is_a? Array
268
+ views = [view] if views.nil?
269
+ raise ViewsNotAnArrayError unless views.is_a? Array
270
+
271
+ before_action do
272
+ @serialization_available_serializers ||= {}
273
+ @serialization_available_serializers[:input] ||= {}
274
+ actions = filter_opts[:only] || :all_actions
275
+ actions = [actions] unless actions.is_a?(Array)
276
+ actions.each do |action|
277
+ @serialization_available_serializers[:input][action.to_s] ||= []
278
+ views.each do |v|
279
+ @serialization_available_serializers[:input][action.to_s].push({serializer: serializer, view: v})
280
+ end
128
281
  end
129
282
  end
283
+
284
+ before_action(**filter_opts) do
285
+ raise SerializersAlreadyFrozenError if defined? @serialization_frozen
286
+
287
+ @serialization_input_registrations ||= SerializationRegistration.new(:input)
288
+
289
+ mergeable_inputs = serializer.inputs_for(views: views)
290
+ raise AddedEmptyInputSerializer, serializer.name if mergeable_inputs.registrations.empty?
291
+
292
+ @serialization_input_registrations = @serialization_input_registrations.merge(mergeable_inputs)
293
+ end
294
+ end
295
+
296
+ def allow_all_output(**filter_opts)
297
+ before_action(**filter_opts) do
298
+ @serialization_output_registrations ||= SerializationRegistration.new(:output)
299
+ @serialization_output_allow_all ||= true
300
+ end
301
+ end
302
+
303
+ def allow_all_input(**filter_opts)
304
+ before_action(**filter_opts) do
305
+ @serialization_input_registrations ||= SerializationRegistration.new(:input)
306
+ @serialization_input_allow_all ||= true
307
+ end
130
308
  end
131
309
 
132
310
  ##
133
311
  # Freezes additions to the serializes and notifies the controller what it will be able to respond to.
134
312
  #
135
- def freeze_accepted_media!
136
- before_action do
137
- # If the responders gem is available, this freezes what a controller can respond to
138
- if self.class.respond_to?(:respond_to)
139
- self.class.respond_to(*Hash(serializers).keys.map { |type| Mime::Type.lookup(type) })
140
- end
313
+ def freeze_io!(**filter_opts)
314
+ before_action :serializer_freeze_io_internal, **filter_opts
315
+
316
+ output_error MediaTypes::Serialization::NoInputReceivedError do |p, error|
317
+ p.title 'Providing input is mandatory. Please set a Content-Type', lang: 'en'
318
+
319
+ p.status_code :bad_request
320
+ end
321
+ end
141
322
 
142
- serializers.freeze
323
+ def output_error(klazz, &block)
324
+ rescue_from klazz do |error|
325
+ problem = Problem.new(error)
326
+ block.call(problem, error) unless block.nil?
327
+
328
+ serializer = MediaTypes::Serialization::Serializers::ProblemSerializer
329
+ registrations = serializer.outputs_for(views: [:html, nil])
330
+
331
+ render_media(problem, serializers: [registrations], status: problem.response_status_code)
143
332
  end
144
333
  end
145
334
  end
@@ -148,139 +337,274 @@ module MediaTypes
148
337
  included do
149
338
  protected
150
339
 
151
- attr_accessor :serializers
152
340
  end
153
341
 
154
342
  protected
155
343
 
156
- def media_type_serializer
157
- @media_type_serializer ||= resolve_media_type_serializer
344
+ def serialize(victim, media_type, serializer: Object.new, links: [], vary: ['Accept'])
345
+ context = SerializationDSL.new(serializer, links, vary, context: self)
346
+ context.instance_exec { @serialization_output_registrations.call(victim, media_type, context) }
158
347
  end
159
348
 
160
- def serialize_media(media, serializer: media_type_serializer)
161
- @last_serialize_media = media
162
- @last_media_serializer = serializer.call(media, context: self)
163
- end
349
+ MEDIA_TYPES_SERIALIZATION_OBJ_IS_UNDEFINED = ::Object.new
164
350
 
165
- def media_type_json_root
166
- String(request.format.symbol).sub(/_json$/, '')
167
- end
351
+ def render_media(obj = MEDIA_TYPES_SERIALIZATION_OBJ_IS_UNDEFINED, serializers: nil, not_acceptable_serializer: nil, **options, &block)
352
+ if obj == MEDIA_TYPES_SERIALIZATION_OBJ_IS_UNDEFINED && options.keys.any? && !block
353
+ # options is too greedy :(
354
+ obj = options
355
+ options = {}
356
+ end
168
357
 
169
- def respond_to_matching(matcher, &block)
170
- respond_to do |format|
171
- serializers.each_key do |mime|
172
- next unless matcher.call(mime: mime, format: format)
173
- format.custom(mime, &block)
174
- end
358
+ if obj == MEDIA_TYPES_SERIALIZATION_OBJ_IS_UNDEFINED && block.nil?
359
+ raise 'render_media was called without an object. Please provide one or supply a block to match the serializer.'
175
360
  end
176
- end
361
+ obj = nil if obj == MEDIA_TYPES_SERIALIZATION_OBJ_IS_UNDEFINED
177
362
 
178
- def respond_to_accept(&block)
179
- respond_to do |format|
180
- serializers.each_key do |mime|
181
- result = yield mime: mime, format: format
182
- next unless result
183
- format.custom(mime) do
184
- result.call
185
- end
363
+ raise SerializersNotFrozenError unless defined? @serialization_frozen
364
+
365
+ not_acceptable_serializer ||= @serialization_not_acceptable_serializer if defined? @serialization_not_acceptable_serializer
366
+
367
+ @serialization_output_registrations ||= SerializationRegistration.new(:output)
368
+ registration = @serialization_output_registrations
369
+ unless serializers.nil?
370
+ registration = SerializationRegistration.new(:output)
371
+ serializers.each do |s|
372
+ registration = registration.merge(s)
186
373
  end
374
+ end
375
+
376
+ identifier = resolve_media_type(request, registration)
377
+
378
+ if identifier.nil?
379
+ serialization_render_not_acceptable(registration, not_acceptable_serializer)
380
+ return
381
+ end
382
+
383
+ serializer = resolve_serializer(request, identifier, registration)
187
384
 
188
- format.any { raise_no_accept_serializer }
385
+ unless block.nil?
386
+ selector = SerializationSelectorDsl.new(self, serializer)
387
+ selector.instance_exec(&block)
388
+
389
+ raise UnmatchedSerializerError, serializer unless selector.matched
390
+ obj = selector.value
189
391
  end
392
+
393
+ serialization_render_resolved(obj: obj, serializer: serializer, identifier: identifier, registrations: registration, options: options)
190
394
  end
191
395
 
192
- # def respond_to_viewer(&block)
193
- # TODO: special collector that matches on api_viewer_content_type matches too
194
- # end
396
+ def deserialize(request)
397
+ raise SerializersNotFrozenError unless defined?(@serialization_frozen)
195
398
 
196
- def request_accept
197
- @request_accept ||= HttpHeaders::Accept.new(request.get_header(HEADER_ACCEPT) || '')
399
+ result = nil
400
+ begin
401
+ result = deserialize!(request)
402
+ rescue NoInputReceivedError
403
+ return nil
404
+ end
405
+ result
198
406
  end
199
407
 
200
- def raise_no_accept_serializer
201
- raise NoSerializerForContentType.new(request_accept, serializers.keys)
408
+ def deserialize!(request)
409
+ raise SerializersNotFrozenError unless defined?(@serialization_frozen)
410
+ raise NoInputReceivedError if request.content_type.blank?
411
+ raise InputNotAcceptableError unless @serialization_input_registrations.has? request.content_type
412
+ @serialization_input_registrations.call(@serialization_decoded_input, request.content_type, self)
202
413
  end
203
414
 
204
- private
415
+ def resolve_serializer(request, identifier = nil, registration = @serialization_output_registrations)
416
+ identifier = resolve_media_type(request, registration) if identifier.nil?
417
+ return nil if identifier.nil?
418
+
419
+ registration = registration.registrations[identifier]
205
420
 
206
- def extract_synonym_version(synonym)
207
- synonym.rpartition('.').last[1..-1]
421
+ raise 'Assertion failed, inconsistent answer from resolve_media_type' if registration.nil?
422
+ registration.serializer
208
423
  end
209
424
 
210
- def resolve_media_type_serializer
211
- raise NoMediaTypeSerializers unless serializers
425
+ private
212
426
 
213
- # Rails negotiation
214
- #
215
- # The problem with rails negotiation is that it has its own logic for some of the handling that is not
216
- # spec compliant. If there is an exact match, that's fine and we leave it like this;
217
- #
218
- if serializers[request.format.to_s]
219
- return serializers[request.format.to_s]
427
+ def resolve_media_type(request, registration, allow_last: true)
428
+ if defined? @serialization_override_accept
429
+ @serialization_override_accept = registration.registrations.keys.last if allow_last && @serialization_override_accept == 'last'
430
+ return nil unless registration.has? @serialization_override_accept
431
+ return @serialization_override_accept
220
432
  end
221
433
 
222
434
  # Ruby negotiation
223
435
  #
224
436
  # This is similar to the respond_to logic. It sorts the accept values and tries to match against each option.
225
- # Currently does not allow for */* or type/*.
226
437
  #
227
- # respond_to_accept do ... end
228
438
  #
229
- request.accepts.each do |mime_type|
230
- next unless serializers.key?(mime_type.to_s)
231
- # Override Rails selected format
232
- request.set_header("action_dispatch.request.formats", [mime_type])
233
- return serializers[mime_type.to_s]
439
+
440
+ accept_header = Utils::AcceptHeader.new(request.get_header(HEADER_ACCEPT)) || ''
441
+ accept_header.each do |mime_type|
442
+ stripped = mime_type.to_s.split(';')[0]
443
+ next unless registration.has? stripped
444
+
445
+ return stripped
234
446
  end
235
447
 
236
- raise_no_accept_serializer
448
+ nil
237
449
  end
238
450
 
239
- def resolved_media_types(serializer, view:)
240
- self.serializers = Hash(serializers)
451
+ def serialization_render_not_acceptable(registrations, override = nil)
452
+ serializer = override
453
+ serializer ||= MediaTypes::Serialization::Serializers::FallbackNotAcceptableSerializer
454
+ identifier = serializer.validator.identifier
455
+ obj = { request: request, registrations: registrations }
456
+ new_registrations = serializer.outputs_for(views: [nil])
241
457
 
242
- registered = serializers.method(:key?)
243
- register = serializers.method(:[]=)
458
+ serialization_render_resolved(obj: obj, serializer: serializer, identifier: identifier, registrations: new_registrations, options: {})
459
+ response.status = :not_acceptable
460
+ end
461
+
462
+ def serializer_freeze_io_internal
463
+ raise UnableToRefreezeError if defined? @serialization_frozen
464
+
465
+ @serialization_frozen = true
466
+ @serialization_input_registrations ||= SerializationRegistration.new(:input)
467
+
468
+ raise NoOutputSerializersDefinedError unless defined? @serialization_output_registrations
469
+
470
+ # Input content-type negotiation and validation
471
+ all_allowed = false
472
+ all_allowed ||= @serialization_input_allow_all if defined?(@serialization_input_allow_all)
473
+
474
+ input_is_allowed = true
475
+ input_is_allowed = @serialization_input_registrations.has? request.content_type unless request.content_type.blank?
476
+
477
+ unless input_is_allowed || all_allowed
478
+ serializers = @serialization_unsupported_media_type_serializer || [MediaTypes::Serialization::Serializers::ProblemSerializer, MediaTypes::Serialization::Serializers::FallbackUnsupportedMediaTypeSerializer]
479
+ registrations = SerializationRegistration.new(:output)
480
+ serializers.each do |s|
481
+ registrations = registrations.merge(s.outputs_for(views: [nil, :html]))
482
+ end
483
+
484
+ input = {
485
+ registrations: @serialization_input_registrations
486
+ }
487
+
488
+ render_media nil, serializers: [registrations], status: :unsupported_media_type do
489
+ serializer MediaTypes::Serialization::Serializers::FallbackUnsupportedMediaTypeSerializer, input
490
+ serializer MediaTypes::Serialization::Serializers::ProblemSerializer do
491
+ error = UnsupportedMediaTypeError.new(input[:registrations].registrations.keys)
492
+ problem = Problem.new(error)
493
+ problem.title 'Unable to process your body Content-Type.', lang: 'en'
244
494
 
245
- Array(view).each do |media_view|
246
- media_view = String(media_view)
247
- Array(serializer.media_type(view: media_view)).each do |media_type|
248
- yield media_type, media_view, registered, register
495
+ problem
496
+ end
249
497
  end
498
+ return
250
499
  end
251
- end
252
500
 
253
- def wrap_media(serializer, media_view:, media_type:)
254
- lambda do |*args, **opts|
255
- serializer.wrap(
256
- serializer.new(*args, media_type: media_type, view: media_view, **opts),
257
- view: media_view
258
- )
501
+ if input_is_allowed && !request.content_type.blank?
502
+ begin
503
+ input_data = request.body.read
504
+ @serialization_decoded_input = @serialization_input_registrations.decode(input_data, request.content_type, self)
505
+ rescue InputValidationFailedError => e
506
+ serializers = @serialization_input_validation_failed_serializer || [MediaTypes::Serialization::Serializers::ProblemSerializer, MediaTypes::Serialization::Serializers::InputValidationErrorSerializer]
507
+ registrations = SerializationRegistration.new(:output)
508
+ serializers.each do |s|
509
+ registrations = registrations.merge(s.outputs_for(views: [nil, :html]))
510
+ end
511
+
512
+ input = {
513
+ identifier: request.content_type,
514
+ input: input_data,
515
+ error: e,
516
+ }
517
+
518
+ render_media nil, serializers: [registrations], status: :unprocessable_entity do
519
+ serializer MediaTypes::Serialization::Serializers::InputValidationErrorSerializer, input
520
+ serializer MediaTypes::Serialization::Serializers::ProblemSerializer do
521
+ problem = Problem.new(e)
522
+ problem.title 'Input failed to validate.', lang: 'en'
523
+
524
+ problem
525
+ end
526
+ end
527
+ return
528
+ end
259
529
  end
530
+
531
+ # Endpoint description media type
532
+
533
+ description_serializer = MediaTypes::Serialization::Serializers::EndpointDescriptionSerializer
534
+
535
+ # All endpoints have endpoint description.
536
+ # Placed in front of the list to make sure the api viewer doesn't pick it.
537
+ @serialization_output_registrations = description_serializer.outputs_for(views: [nil]).merge(@serialization_output_registrations)
538
+
539
+ endpoint_matched_identifier = resolve_media_type(request, description_serializer.serializer_output_registration, allow_last: false)
540
+ if endpoint_matched_identifier
541
+ # We picked an endpoint description media type
542
+ #
543
+ @serialization_available_serializers ||= {}
544
+ @serialization_available_serializers[:output] ||= {}
545
+ @serialization_api_viewer_enabled ||= {}
546
+
547
+ input = {
548
+ api_viewer: @serialization_api_viewer_enabled,
549
+ actions: @serialization_available_serializers,
550
+ }
551
+
552
+ serialization_render_resolved obj: input, serializer: description_serializer, identifier: endpoint_matched_identifier, registrations: @serialization_output_registrations, options: {}
553
+ return
554
+ end
555
+
556
+ # Output content negotiation
557
+ resolved_identifier = resolve_media_type(request, @serialization_output_registrations)
558
+
559
+ not_acceptable_serializer = nil
560
+ not_acceptable_serializer = @serialization_not_acceptable_serializer if defined? @serialization_not_acceptable_serializer
561
+ not_acceptable_serializer ||= MediaTypes::Serialization::Serializers::FallbackNotAcceptableSerializer
562
+
563
+ can_satisfy_allow = !resolved_identifier.nil?
564
+ can_satisfy_allow ||= @serialization_output_allow_all if defined?(@serialization_output_allow_all)
565
+
566
+ serialization_render_not_acceptable(@serialization_output_registrations, not_acceptable_serializer) unless can_satisfy_allow
260
567
  end
261
568
 
262
- def wrap_html(serializer, media_view:, media_type:)
263
- lambda do |*args, **opts|
264
- inner_media_type = media_type.try(:serialize_as) || media_type
265
-
266
- media_serializer = wrap_media(
267
- serializer,
268
- media_view: media_view,
269
- media_type: inner_media_type
270
- ).call(*args, **opts)
271
-
272
- override_mime_type = media_type.respond_to?(:serialize_as) ?
273
- "#{media_type.to_s} (#{media_type.serialize_as})" :
274
- media_type.to_s
275
-
276
- Wrapper::HtmlWrapper.new(
277
- media_serializer,
278
- view: media_view,
279
- mime_type: override_mime_type,
280
- representations: serializers.keys,
281
- url_context: request.original_fullpath.chomp(".#{request.format.symbol}")
282
- )
569
+ def serialization_render_resolved(obj:, identifier:, serializer:, registrations:, options:)
570
+ links = []
571
+ vary = []
572
+ context = SerializationDSL.new(serializer, links, vary, context: self)
573
+ result = registrations.call(obj, identifier, self, dsl: context)
574
+
575
+ if links.any?
576
+ items = links.map do |l|
577
+ href_part = "<#{l[:href]}>"
578
+ tags = l.to_a.select { |k,_| k != :href }.map { |k,v| "#{k}=#{v}" }
579
+ ([href_part] + tags).join('; ')
580
+ end
581
+ response.set_header('Link', items.join(', '))
582
+ end
583
+
584
+ if vary.any?
585
+ current_vary = (response.headers['Vary'] || "").split(',').map { |v| v.strip }.reject { |v| v.empty? }.sort
586
+ merged_vary = (vary.sort + current_vary).uniq
587
+
588
+ response.set_header('Vary', merged_vary.join(', '))
589
+ end
590
+
591
+ if defined? @serialization_wrapping_renderer
592
+ input = {
593
+ identifier: identifier,
594
+ registrations: registrations,
595
+ output: result,
596
+ links: links,
597
+ }
598
+ wrapped = @serialization_wrapping_renderer.serialize input, '*/*', self
599
+ render body: wrapped
600
+
601
+ response.content_type = 'text/html'
602
+ return
283
603
  end
604
+
605
+ render body: result, **options
606
+
607
+ response.content_type = registrations.identifier_for(identifier)
284
608
  end
285
609
  end
286
610
  end