media_types-serialization 0.8.1 → 1.0.1

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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +10 -1
  3. data/.gitignore +12 -12
  4. data/.idea/.rakeTasks +5 -5
  5. data/.idea/inspectionProfiles/Project_Default.xml +5 -5
  6. data/.idea/runConfigurations/test.xml +19 -19
  7. data/CHANGELOG.md +18 -0
  8. data/CODE_OF_CONDUCT.md +74 -74
  9. data/Gemfile +4 -4
  10. data/Gemfile.lock +58 -61
  11. data/LICENSE.txt +21 -21
  12. data/README.md +640 -173
  13. data/Rakefile +10 -10
  14. data/bin/console +14 -14
  15. data/bin/setup +8 -8
  16. data/lib/media_types/problem.rb +64 -0
  17. data/lib/media_types/serialization.rb +431 -172
  18. data/lib/media_types/serialization/base.rb +111 -91
  19. data/lib/media_types/serialization/error.rb +178 -0
  20. data/lib/media_types/serialization/fake_validator.rb +52 -0
  21. data/lib/media_types/serialization/serialization_dsl.rb +117 -0
  22. data/lib/media_types/serialization/serialization_registration.rb +235 -0
  23. data/lib/media_types/serialization/serializers/api_viewer.rb +133 -0
  24. data/lib/media_types/serialization/serializers/common_css.rb +168 -0
  25. data/lib/media_types/serialization/serializers/endpoint_description_serializer.rb +80 -0
  26. data/lib/media_types/serialization/serializers/fallback_not_acceptable_serializer.rb +85 -0
  27. data/lib/media_types/serialization/serializers/fallback_unsupported_media_type_serializer.rb +58 -0
  28. data/lib/media_types/serialization/serializers/input_validation_error_serializer.rb +89 -0
  29. data/lib/media_types/serialization/serializers/problem_serializer.rb +87 -0
  30. data/lib/media_types/serialization/version.rb +1 -1
  31. data/media_types-serialization.gemspec +50 -50
  32. metadata +40 -43
  33. data/.travis.yml +0 -17
  34. data/lib/generators/media_types/serialization/api_viewer/api_viewer_generator.rb +0 -25
  35. data/lib/generators/media_types/serialization/api_viewer/templates/api_viewer.html.erb +0 -98
  36. data/lib/generators/media_types/serialization/api_viewer/templates/initializer.rb +0 -33
  37. data/lib/generators/media_types/serialization/api_viewer/templates/template_controller.rb +0 -23
  38. data/lib/media_types/serialization/media_type/register.rb +0 -4
  39. data/lib/media_types/serialization/migrations_command.rb +0 -38
  40. data/lib/media_types/serialization/migrations_support.rb +0 -50
  41. data/lib/media_types/serialization/mime_type_support.rb +0 -64
  42. data/lib/media_types/serialization/no_content_type_given.rb +0 -11
  43. data/lib/media_types/serialization/no_media_type_serializers.rb +0 -11
  44. data/lib/media_types/serialization/no_serializer_for_content_type.rb +0 -15
  45. data/lib/media_types/serialization/renderer.rb +0 -41
  46. data/lib/media_types/serialization/renderer/register.rb +0 -4
  47. data/lib/media_types/serialization/wrapper.rb +0 -13
  48. data/lib/media_types/serialization/wrapper/html_wrapper.rb +0 -45
  49. data/lib/media_types/serialization/wrapper/media_collection_wrapper.rb +0 -61
  50. data/lib/media_types/serialization/wrapper/media_index_wrapper.rb +0 -61
  51. data/lib/media_types/serialization/wrapper/media_object_wrapper.rb +0 -55
  52. data/lib/media_types/serialization/wrapper_support.rb +0 -38
data/Rakefile CHANGED
@@ -1,10 +1,10 @@
1
- require "bundler/gem_tasks"
2
- require "rake/testtask"
3
-
4
- Rake::TestTask.new(:test) do |t|
5
- t.libs << "test"
6
- t.libs << "lib"
7
- t.test_files = FileList["test/**/*_test.rb"]
8
- end
9
-
10
- task :default => :test
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
data/bin/console CHANGED
@@ -1,14 +1,14 @@
1
- #!/usr/bin/env ruby
2
-
3
- require "bundler/setup"
4
- require "media_types/serialization"
5
-
6
- # You can add fixtures and/or initialization code here to make experimenting
7
- # with your gem easier. You can also use a different console, if you like.
8
-
9
- # (If you use this, don't forget to add pry to your Gemfile!)
10
- # require "pry"
11
- # Pry.start
12
-
13
- require "irb"
14
- IRB.start(__FILE__)
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "media_types/serialization"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup CHANGED
@@ -1,8 +1,8 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
- IFS=$'\n\t'
4
- set -vx
5
-
6
- bundle install
7
-
8
- # Do any other automated setup that you need to do here
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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'
@@ -9,137 +17,253 @@ require 'active_support/core_ext/object/blank'
9
17
 
10
18
  require 'http_headers/accept'
11
19
 
12
- require 'media_types/serialization/no_media_type_serializers'
13
- require 'media_types/serialization/no_serializer_for_content_type'
14
20
  require 'media_types/serialization/base'
15
- require 'media_types/serialization/wrapper/html_wrapper'
16
-
17
- require 'awesome_print'
21
+ require 'media_types/serialization/error'
22
+ require 'media_types/serialization/serialization_dsl'
18
23
 
19
24
  require 'delegate'
20
25
 
21
- class MediaTypeApiViewer < SimpleDelegator
22
- def initialize(inner_media)
23
- super inner_media
26
+ class SerializationSelectorDsl < SimpleDelegator
27
+ def initialize(controller, selected_serializer)
28
+ @serializer = selected_serializer
29
+ self.value = nil
30
+ self.matched = false
31
+ super controller
24
32
  end
25
33
 
26
- def to_s
27
- 'application/vnd.xpbytes.api-viewer.v1'
28
- end
34
+ attr_accessor :value, :matched
35
+
36
+ def serializer(klazz, obj = nil, &block)
37
+ return if klazz != @serializer
29
38
 
30
- def serialize_as
31
- __getobj__
39
+ self.matched = true
40
+ self.value = block.nil? ? obj : yield
32
41
  end
33
42
  end
34
43
 
35
44
  module MediaTypes
36
45
  module Serialization
37
46
 
38
- mattr_accessor :common_suffix, :collect_links_for_collection, :collect_links_for_index,
39
- :html_wrapper_layout, :api_viewer_layout
47
+ HEADER_ACCEPT = 'HTTP_ACCEPT'
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 = Oj.method(:load)
71
+ else
72
+ require 'json'
73
+ self.json_encoder = JSON.method(:pretty_generate)
74
+ self.json_decoder = ->(txt) {
75
+ JSON.parse(txt, {
76
+ symbolize_names: true,
77
+ allow_nan: false,
78
+ create_additions: false,
79
+ object_class: ::Hash,
80
+ array_class: ::Array,
81
+ })
82
+ }
83
+ end
40
84
 
41
85
  extend ActiveSupport::Concern
42
86
 
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
87
  # rubocop:disable Metrics/BlockLength
49
88
  class_methods do
50
89
 
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)
90
+ def not_acceptable_serializer(serializer, **filter_opts)
67
91
  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
92
+ raise SerializersAlreadyFrozenError if defined? @serialization_frozen
93
+
94
+ @serialization_not_acceptable_serializer = serializer
72
95
  end
96
+ end
97
+
98
+ def unsupported_media_type_serializer(serializer, **filter_opts)
99
+ before_action(**filter_opts) do
100
+ raise SerializersAlreadyFrozenError if defined? @serialization_frozen
73
101
 
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
102
+ @serialization_unsupported_media_type_serializer ||= []
103
+ @serialization_unsupported_media_type_serializer.append(serializer)
104
+ end
105
+ end
106
+
107
+ def clear_unsupported_media_type_serializer!(**filter_opts)
108
+ before_action(**filter_opts) do
109
+ raise SerializersAlreadyFrozenError if defined? @serialization_frozen
110
+
111
+ @serialization_unsupported_media_type_serializer = []
112
+ end
113
+ end
114
+
115
+ def input_validation_failed_serializer(serializer, **filter_opts)
116
+ before_action(**filter_opts) do
117
+ raise SerializersAlreadyFrozenError if defined? @serialization_frozen
118
+
119
+ @serialization_input_validation_failed_serializer ||= []
120
+ @serialization_input_validation_failed_serializer.append(serializer)
121
+ end
122
+ end
123
+
124
+ def clear_input_validation_failed_serializers!(**filter_opts)
125
+ before_action(**filter_opts) do
126
+ raise SerializersAlreadyFrozenError if defined? @serialization_frozen
127
+
128
+ @serialization_input_validation_failed_serializer = []
129
+ end
76
130
  end
77
131
 
78
132
  ##
79
- # Accept serialization using the passed in +serializer+ for the given +view+ as text/html
133
+ # Allow output serialization using the passed in +serializer+ for the given +view+
80
134
  #
81
- # Always overwrites the current acceptor of text/html. The last call to this, for the giben +filter_opts+ will win
82
- # the serialization.
135
+ # @see #freeze_io!
83
136
  #
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))
137
+ # @param serializer the serializer to use for serialization.
138
+ # @param [(String | NilClass|)] view the view it should use the serializer for. Use nil for no view
139
+ # @param [(String | NilClass|)[]|NilClass] views the views it should use the serializer for. Use nil for no view
140
+ #
141
+ def allow_output_serializer(serializer, view: nil, views: nil, **filter_opts)
142
+ raise SerializersAlreadyFrozenError if defined? @serialization_frozen
143
+ raise ArrayInViewParameterError, :allow_output_serializer if view.is_a? Array
144
+
145
+ views = [view] if views.nil?
146
+ raise ViewsNotAnArrayError unless views.is_a? Array
147
+
148
+ before_action do
149
+ @serialization_available_serializers ||= {}
150
+ @serialization_available_serializers[:output] ||= {}
151
+ actions = filter_opts[:only] || :all_actions
152
+ actions = [actions] unless actions.is_a?(Array)
153
+ actions.each do |action|
154
+ @serialization_available_serializers[:output][action.to_s] ||= []
155
+ views.each do |v|
156
+ @serialization_available_serializers[:output][action.to_s].push({serializer: serializer, view: v})
157
+ end
89
158
  end
90
159
  end
160
+
161
+ before_action(**filter_opts) do
162
+ raise SerializersAlreadyFrozenError if defined? @serialization_frozen
163
+
164
+ @serialization_output_registrations ||= SerializationRegistration.new(:output)
165
+
166
+ mergeable_outputs = serializer.outputs_for(views: views)
167
+ raise AddedEmptyOutputSerializer, serializer.name if mergeable_outputs.registrations.empty?
168
+
169
+ @serialization_output_registrations = @serialization_output_registrations.merge(mergeable_outputs)
170
+ end
91
171
  end
172
+
173
+ def allow_api_viewer(serializer: MediaTypes::Serialization::Serializers::ApiViewer, **filter_opts)
174
+ before_action do
175
+ @serialization_api_viewer_enabled ||= {}
176
+ actions = filter_opts[:only] || :all_actions
177
+ actions = [actions] unless actions.kind_of?(Array)
178
+ actions.each do |action|
179
+ @serialization_api_viewer_enabled[action.to_s] = true
180
+ end
181
+ end
92
182
 
93
- ##
94
- # Same as +accept_html+ but then for Api Viewer
95
- #
96
- def accept_api_viewer(serializer, view: [nil], overwrite: true, **filter_opts)
97
183
  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
105
- end
184
+ if request.query_parameters['api_viewer']
185
+ @serialization_override_accept = request.query_parameters['api_viewer'].sub ' ', '+'
186
+ @serialization_wrapping_renderer = serializer
106
187
  end
107
188
  end
108
189
  end
109
190
 
110
191
  ##
111
- # Register a mime type, but explicitly notify that it can't be serialized.
112
- # This is done for file serving and redirects.
113
- #
114
- # @param [Symbol] mimes takes a list of symbols that should resolve through Mime::Type
115
- #
116
- # @see #freeze_accepted_media!
192
+ # Allow input serialization using the passed in +serializer+ for the given +view+
117
193
  #
118
- # @example fingerpint binary format
194
+ # @see #freeze_io!
119
195
  #
120
- # no_serializer_for :fingerprint_bin, :fingerprint_deprecated_bin
196
+ # @param serializer the serializer to use for deserialization
197
+ # @param [(String | NilClass|)] view the view it should serializer for. Use nil for no view
198
+ # @param [(String | NilClass|)[]|NilClass] views the views it should serializer for. Use nil for no view
121
199
  #
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
200
+ def allow_input_serializer(serializer, view: nil, views: nil, **filter_opts)
201
+ raise SerializersAlreadyFrozenError if defined? @serialization_frozen
202
+ raise ArrayInViewParameterError, :allow_input_serializer if view.is_a? Array
203
+ views = [view] if views.nil?
204
+ raise ViewsNotAnArrayError unless views.is_a? Array
205
+
206
+ before_action do
207
+ @serialization_available_serializers ||= {}
208
+ @serialization_available_serializers[:input] ||= {}
209
+ actions = filter_opts[:only] || :all_actions
210
+ actions = [actions] unless actions.is_a?(Array)
211
+ actions.each do |action|
212
+ @serialization_available_serializers[:input][action.to_s] ||= []
213
+ views.each do |v|
214
+ @serialization_available_serializers[:input][action.to_s].push({serializer: serializer, view: v})
215
+ end
128
216
  end
129
217
  end
218
+
219
+ before_action(**filter_opts) do
220
+ raise SerializersAlreadyFrozenError if defined? @serialization_frozen
221
+
222
+ @serialization_input_registrations ||= SerializationRegistration.new(:input)
223
+
224
+ mergeable_inputs = serializer.inputs_for(views: views)
225
+ raise AddedEmptyInputSerializer, serializer.name if mergeable_inputs.registrations.empty?
226
+
227
+ @serialization_input_registrations = @serialization_input_registrations.merge(mergeable_inputs)
228
+ end
229
+ end
230
+
231
+ def allow_all_output(**filter_opts)
232
+ before_action(**filter_opts) do
233
+ @serialization_output_registrations ||= SerializationRegistration.new(:output)
234
+ @serialization_output_allow_all ||= true
235
+ end
236
+ end
237
+
238
+ def allow_all_input(**filter_opts)
239
+ before_action(**filter_opts) do
240
+ @serialization_input_registrations ||= SerializationRegistration.new(:input)
241
+ @serialization_input_allow_all ||= true
242
+ end
130
243
  end
131
244
 
132
245
  ##
133
246
  # Freezes additions to the serializes and notifies the controller what it will be able to respond to.
134
247
  #
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
248
+ def freeze_io!
249
+ before_action :serializer_freeze_io_internal
250
+
251
+ output_error MediaTypes::Serialization::NoInputReceivedError do |p, error|
252
+ p.title 'Providing input is mandatory. Please set a Content-Type', lang: 'en'
141
253
 
142
- serializers.freeze
254
+ p.status_code :bad_request
255
+ end
256
+ end
257
+
258
+ def output_error(klazz, &block)
259
+ rescue_from klazz do |error|
260
+ problem = Problem.new(error)
261
+ block.call(problem, error) unless block.nil?
262
+
263
+ serializer = MediaTypes::Serialization::Serializers::ProblemSerializer
264
+ registrations = serializer.outputs_for(views: [:html, nil])
265
+
266
+ render_media(problem, serializers: [registrations], status: problem.response_status_code)
143
267
  end
144
268
  end
145
269
  end
@@ -148,139 +272,274 @@ module MediaTypes
148
272
  included do
149
273
  protected
150
274
 
151
- attr_accessor :serializers
152
275
  end
153
276
 
154
277
  protected
155
278
 
156
- def media_type_serializer
157
- @media_type_serializer ||= resolve_media_type_serializer
279
+ def serialize(victim, media_type, serializer: Object.new, links: [], vary: ['Accept'])
280
+ context = SerializationDSL.new(serializer, links, vary, context: self)
281
+ context.instance_exec { @serialization_output_registrations.call(victim, media_type, context) }
158
282
  end
159
283
 
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
284
+ MEDIA_TYPES_SERIALIZATION_OBJ_IS_UNDEFINED = ::Object.new
164
285
 
165
- def media_type_json_root
166
- String(request.format.symbol).sub(/_json$/, '')
167
- end
286
+ def render_media(obj = MEDIA_TYPES_SERIALIZATION_OBJ_IS_UNDEFINED, serializers: nil, not_acceptable_serializer: nil, **options, &block)
287
+ if obj == MEDIA_TYPES_SERIALIZATION_OBJ_IS_UNDEFINED && options.keys.any? && !block
288
+ # options is too greedy :(
289
+ obj = options
290
+ options = {}
291
+ end
168
292
 
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
293
+ if obj == MEDIA_TYPES_SERIALIZATION_OBJ_IS_UNDEFINED && block.nil?
294
+ raise 'render_media was called without an object. Please provide one or supply a block to match the serializer.'
175
295
  end
176
- end
296
+ obj = nil if obj == MEDIA_TYPES_SERIALIZATION_OBJ_IS_UNDEFINED
177
297
 
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
298
+ raise SerializersNotFrozenError unless defined? @serialization_frozen
299
+
300
+ not_acceptable_serializer ||= @serialization_not_acceptable_serializer if defined? @serialization_not_acceptable_serializer
301
+
302
+ @serialization_output_registrations ||= SerializationRegistration.new(:output)
303
+ registration = @serialization_output_registrations
304
+ unless serializers.nil?
305
+ registration = SerializationRegistration.new(:output)
306
+ serializers.each do |s|
307
+ registration = registration.merge(s)
186
308
  end
309
+ end
310
+
311
+ identifier = resolve_media_type(request, registration)
187
312
 
188
- format.any { raise_no_accept_serializer }
313
+ if identifier.nil?
314
+ serialization_render_not_acceptable(registration, not_acceptable_serializer)
315
+ return
189
316
  end
317
+
318
+ serializer = resolve_serializer(request, identifier, registration)
319
+
320
+ unless block.nil?
321
+ selector = SerializationSelectorDsl.new(self, serializer)
322
+ selector.instance_exec(&block)
323
+
324
+ raise UnmatchedSerializerError, serializer unless selector.matched
325
+ obj = selector.value
326
+ end
327
+
328
+ serialization_render_resolved(obj: obj, serializer: serializer, identifier: identifier, registrations: registration, options: options)
190
329
  end
191
330
 
192
- # def respond_to_viewer(&block)
193
- # TODO: special collector that matches on api_viewer_content_type matches too
194
- # end
331
+ def deserialize(request)
332
+ raise SerializersNotFrozenError unless defined?(@serialization_frozen)
195
333
 
196
- def request_accept
197
- @request_accept ||= HttpHeaders::Accept.new(request.get_header(HEADER_ACCEPT) || '')
334
+ result = nil
335
+ begin
336
+ result = deserialize!(request)
337
+ rescue NoInputReceivedError
338
+ return nil
339
+ end
340
+ result
198
341
  end
199
342
 
200
- def raise_no_accept_serializer
201
- raise NoSerializerForContentType.new(request_accept, serializers.keys)
343
+ def deserialize!(request)
344
+ raise SerializersNotFrozenError unless defined?(@serialization_frozen)
345
+ raise NoInputReceivedError if request.content_type.blank?
346
+ raise InputNotAcceptableError unless @serialization_input_registrations.has? request.content_type
347
+ @serialization_input_registrations.call(@serialization_decoded_input, request.content_type, self)
202
348
  end
203
349
 
204
- private
350
+ def resolve_serializer(request, identifier = nil, registration = @serialization_output_registrations)
351
+ identifier = resolve_media_type(request, registration) if identifier.nil?
352
+ return nil if identifier.nil?
205
353
 
206
- def extract_synonym_version(synonym)
207
- synonym.rpartition('.').last[1..-1]
354
+ registration = registration.registrations[identifier]
355
+
356
+ raise 'Assertion failed, inconsistent answer from resolve_media_type' if registration.nil?
357
+ registration.serializer
208
358
  end
209
359
 
210
- def resolve_media_type_serializer
211
- raise NoMediaTypeSerializers unless serializers
360
+ private
212
361
 
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]
362
+ def resolve_media_type(request, registration, allow_last: true)
363
+ if defined? @serialization_override_accept
364
+ @serialization_override_accept = registration.registrations.keys.last if allow_last && @serialization_override_accept == 'last'
365
+ return nil unless registration.has? @serialization_override_accept
366
+ return @serialization_override_accept
220
367
  end
221
368
 
222
369
  # Ruby negotiation
223
370
  #
224
371
  # 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
372
  #
227
- # respond_to_accept do ... end
228
373
  #
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]
374
+
375
+ accept_header = HttpHeaders::Accept.new(request.get_header(HEADER_ACCEPT)) || ''
376
+ accept_header.each do |mime_type|
377
+ stripped = mime_type.to_s.split(';')[0]
378
+ next unless registration.has? stripped
379
+
380
+ return stripped
234
381
  end
235
382
 
236
- raise_no_accept_serializer
383
+ nil
384
+ end
385
+
386
+ def serialization_render_not_acceptable(registrations, override = nil)
387
+ serializer = override
388
+ serializer ||= MediaTypes::Serialization::Serializers::FallbackNotAcceptableSerializer
389
+ identifier = serializer.validator.identifier
390
+ obj = { request: request, registrations: registrations }
391
+ new_registrations = serializer.outputs_for(views: [nil])
392
+
393
+ serialization_render_resolved(obj: obj, serializer: serializer, identifier: identifier, registrations: new_registrations, options: {})
394
+ response.status = :not_acceptable
237
395
  end
238
396
 
239
- def resolved_media_types(serializer, view:)
240
- self.serializers = Hash(serializers)
397
+ def serializer_freeze_io_internal
398
+ raise UnableToRefreezeError if defined? @serialization_frozen
399
+
400
+ @serialization_frozen = true
401
+ @serialization_input_registrations ||= SerializationRegistration.new(:input)
402
+
403
+ raise NoOutputSerializersDefinedError unless defined? @serialization_output_registrations
404
+
405
+ # Input content-type negotiation and validation
406
+ all_allowed = false
407
+ all_allowed ||= @serialization_input_allow_all if defined?(@serialization_input_allow_all)
241
408
 
242
- registered = serializers.method(:key?)
243
- register = serializers.method(:[]=)
409
+ input_is_allowed = true
410
+ input_is_allowed = @serialization_input_registrations.has? request.content_type unless request.content_type.blank?
244
411
 
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
412
+ unless input_is_allowed || all_allowed
413
+ serializers = @serialization_unsupported_media_type_serializer || [MediaTypes::Serialization::Serializers::ProblemSerializer, MediaTypes::Serialization::Serializers::FallbackUnsupportedMediaTypeSerializer]
414
+ registrations = SerializationRegistration.new(:output)
415
+ serializers.each do |s|
416
+ registrations = registrations.merge(s.outputs_for(views: [nil, :html]))
249
417
  end
418
+
419
+ input = {
420
+ registrations: @serialization_input_registrations
421
+ }
422
+
423
+ render_media nil, serializers: [registrations], status: :unsupported_media_type do
424
+ serializer MediaTypes::Serialization::Serializers::FallbackUnsupportedMediaTypeSerializer, input
425
+ serializer MediaTypes::Serialization::Serializers::ProblemSerializer do
426
+ error = UnsupportedMediaTypeError.new(input[:registrations].registrations.keys)
427
+ problem = Problem.new(error)
428
+ problem.title 'Unable to process your body Content-Type.', lang: 'en'
429
+
430
+ problem
431
+ end
432
+ end
433
+ return
250
434
  end
251
- end
252
435
 
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
- )
436
+ if input_is_allowed && !request.content_type.blank?
437
+ begin
438
+ input_data = request.body.read
439
+ @serialization_decoded_input = @serialization_input_registrations.decode(input_data, request.content_type, self)
440
+ rescue InputValidationFailedError => e
441
+ serializers = @serialization_input_validation_failed_serializer || [MediaTypes::Serialization::Serializers::ProblemSerializer, MediaTypes::Serialization::Serializers::InputValidationErrorSerializer]
442
+ registrations = SerializationRegistration.new(:output)
443
+ serializers.each do |s|
444
+ registrations = registrations.merge(s.outputs_for(views: [nil, :html]))
445
+ end
446
+
447
+ input = {
448
+ identifier: request.content_type,
449
+ input: input_data,
450
+ error: e,
451
+ }
452
+
453
+ render_media nil, serializers: [registrations], status: :unprocessable_entity do
454
+ serializer MediaTypes::Serialization::Serializers::InputValidationErrorSerializer, input
455
+ serializer MediaTypes::Serialization::Serializers::ProblemSerializer do
456
+ problem = Problem.new(e)
457
+ problem.title 'Input failed to validate.', lang: 'en'
458
+
459
+ problem
460
+ end
461
+ end
462
+ return
463
+ end
464
+ end
465
+
466
+ # Endpoint description media type
467
+
468
+ description_serializer = MediaTypes::Serialization::Serializers::EndpointDescriptionSerializer
469
+
470
+ # All endpoints have endpoint description.
471
+ # Placed in front of the list to make sure the api viewer doesn't pick it.
472
+ @serialization_output_registrations = description_serializer.outputs_for(views: [nil]).merge(@serialization_output_registrations)
473
+
474
+ endpoint_matched_identifier = resolve_media_type(request, description_serializer.serializer_output_registration, allow_last: false)
475
+ if endpoint_matched_identifier
476
+ # We picked an endpoint description media type
477
+ #
478
+ @serialization_available_serializers ||= {}
479
+ @serialization_available_serializers[:output] ||= {}
480
+ @serialization_api_viewer_enabled ||= {}
481
+
482
+ input = {
483
+ api_viewer: @serialization_api_viewer_enabled,
484
+ actions: @serialization_available_serializers,
485
+ }
486
+
487
+ serialization_render_resolved obj: input, serializer: description_serializer, identifier: endpoint_matched_identifier, registrations: @serialization_output_registrations, options: {}
488
+ return
259
489
  end
490
+
491
+ # Output content negotiation
492
+ resolved_identifier = resolve_media_type(request, @serialization_output_registrations)
493
+
494
+ not_acceptable_serializer = nil
495
+ not_acceptable_serializer = @serialization_not_acceptable_serializer if defined? @serialization_not_acceptable_serializer
496
+ not_acceptable_serializer ||= MediaTypes::Serialization::Serializers::FallbackNotAcceptableSerializer
497
+
498
+ can_satisfy_allow = !resolved_identifier.nil?
499
+ can_satisfy_allow ||= @serialization_output_allow_all if defined?(@serialization_output_allow_all)
500
+
501
+ serialization_render_not_acceptable(@serialization_output_registrations, not_acceptable_serializer) unless can_satisfy_allow
260
502
  end
261
503
 
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
- )
504
+ def serialization_render_resolved(obj:, identifier:, serializer:, registrations:, options:)
505
+ links = []
506
+ vary = []
507
+ context = SerializationDSL.new(serializer, links, vary, context: self)
508
+ result = registrations.call(obj, identifier, self, dsl: context)
509
+
510
+ if links.any?
511
+ items = links.map do |l|
512
+ href_part = "<#{l[:href]}>"
513
+ tags = l.to_a.select { |k,_| k != :href }.map { |k,v| "#{k}=#{v}" }
514
+ ([href_part] + tags).join('; ')
515
+ end
516
+ response.set_header('Link', items.join(', '))
517
+ end
518
+
519
+ if vary.any?
520
+ current_vary = (response.headers['Vary'] || "").split(',').map { |v| v.strip }.reject { |v| v.empty? }.sort
521
+ merged_vary = (vary.sort + current_vary).uniq
522
+
523
+ response.set_header('Vary', merged_vary.join(', '))
283
524
  end
525
+
526
+ if defined? @serialization_wrapping_renderer
527
+ input = {
528
+ identifier: identifier,
529
+ registrations: registrations,
530
+ output: result,
531
+ links: links,
532
+ }
533
+ wrapped = @serialization_wrapping_renderer.serialize input, '*/*', self
534
+ render body: wrapped
535
+
536
+ response.content_type = 'text/html'
537
+ return
538
+ end
539
+
540
+ render body: result, **options
541
+
542
+ response.content_type = registrations.identifier_for(identifier)
284
543
  end
285
544
  end
286
545
  end