media_types-serialization 0.8.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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