media_types-serialization 1.0.2 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -15,8 +15,7 @@ require 'active_support/concern'
15
15
  require 'active_support/core_ext/module/attribute_accessors'
16
16
  require 'active_support/core_ext/object/blank'
17
17
 
18
- require 'http_headers/accept'
19
-
18
+ require 'media_types/serialization/utils/accept_header'
20
19
  require 'media_types/serialization/base'
21
20
  require 'media_types/serialization/error'
22
21
  require 'media_types/serialization/serialization_dsl'
@@ -44,7 +43,8 @@ end
44
43
  module MediaTypes
45
44
  module Serialization
46
45
 
47
- HEADER_ACCEPT = 'HTTP_ACCEPT'
46
+ HEADER_ACCEPT = 'HTTP_ACCEPT'.freeze
47
+ HEADER_ACCEPT_LANGUAGE = 'HTTP_ACCEPT_LANGUAGE'.freeze
48
48
 
49
49
  mattr_accessor :json_encoder, :json_decoder
50
50
  if defined?(::Oj)
@@ -183,7 +183,60 @@ module MediaTypes
183
183
  @serialization_output_registrations = @serialization_output_registrations.merge(mergeable_outputs)
184
184
  end
185
185
  end
186
-
186
+
187
+ def allow_output_html(as: nil, view: nil, layout: nil, formats: [:html], variants: nil, **filter_opts)
188
+ before_action(**filter_opts) do
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
+ options = {}
201
+ options[:layout] = layout unless layout.nil?
202
+ options[:template] = view unless view.nil?
203
+ options[:formats] = formats unless formats.nil?
204
+ options[:variants] = variants unless variants.nil?
205
+
206
+ controller.render_to_string(**options)
207
+ }
208
+
209
+ html_registration.register_block(nil, validator, nil, block, true, wildcards: true)
210
+ html_registration.registrations[validator.identifier].display_identifier = output_identifier
211
+ html_registration.registrations["#{validator.identifier.split('/')[0]}/*"].display_identifier = output_identifier
212
+ html_registration.registrations['*/*'].display_identifier = output_identifier
213
+
214
+ @serialization_output_registrations = @serialization_output_registrations.merge(html_registration)
215
+ end
216
+ end
217
+
218
+ def allow_output_docs(description, **filter_opts)
219
+ before_action(**filter_opts) do
220
+ raise SerializersAlreadyFrozenError if defined? @serialization_frozen
221
+
222
+ @serialization_output_registrations ||= SerializationRegistration.new(:output)
223
+
224
+ docs_registration = SerializationRegistration.new(:output)
225
+ validator = FakeValidator.new('text/vnd.delftsolutions.docs')
226
+
227
+ block = lambda { |_, _, _|
228
+ description
229
+ }
230
+
231
+ docs_registration.register_block(nil, validator, nil, block, true, wildcards: true)
232
+ docs_registration.registrations['text/vnd.delftsolutions.docs'].display_identifier = 'text/plain; charset=utf-8'
233
+ docs_registration.registrations['text/*'].display_identifier = 'text/plain; charset=utf-8'
234
+ docs_registration.registrations['*/*'].display_identifier = 'text/plain; charset=utf-8'
235
+
236
+ @serialization_output_registrations = @serialization_output_registrations.merge(docs_registration)
237
+ end
238
+ end
239
+
187
240
  def allow_api_viewer(serializer: MediaTypes::Serialization::Serializers::ApiViewer, **filter_opts)
188
241
  before_action do
189
242
  @serialization_api_viewer_enabled ||= {}
@@ -216,7 +269,7 @@ module MediaTypes
216
269
  raise ArrayInViewParameterError, :allow_input_serializer if view.is_a? Array
217
270
  views = [view] if views.nil?
218
271
  raise ViewsNotAnArrayError unless views.is_a? Array
219
-
272
+
220
273
  before_action do
221
274
  @serialization_available_serializers ||= {}
222
275
  @serialization_available_serializers[:input] ||= {}
@@ -259,8 +312,8 @@ module MediaTypes
259
312
  ##
260
313
  # Freezes additions to the serializes and notifies the controller what it will be able to respond to.
261
314
  #
262
- def freeze_io!
263
- before_action :serializer_freeze_io_internal
315
+ def freeze_io!(**filter_opts)
316
+ before_action :serializer_freeze_io_internal, **filter_opts
264
317
 
265
318
  output_error MediaTypes::Serialization::NoInputReceivedError do |p, error|
266
319
  p.title 'Providing input is mandatory. Please set a Content-Type', lang: 'en'
@@ -283,11 +336,6 @@ module MediaTypes
283
336
  end
284
337
  # rubocop:enable Metrics/BlockLength
285
338
 
286
- included do
287
- protected
288
-
289
- end
290
-
291
339
  protected
292
340
 
293
341
  def serialize(victim, media_type, serializer: Object.new, links: [], vary: ['Accept'])
@@ -307,6 +355,7 @@ module MediaTypes
307
355
  if obj == MEDIA_TYPES_SERIALIZATION_OBJ_IS_UNDEFINED && block.nil?
308
356
  raise 'render_media was called without an object. Please provide one or supply a block to match the serializer.'
309
357
  end
358
+
310
359
  obj = nil if obj == MEDIA_TYPES_SERIALIZATION_OBJ_IS_UNDEFINED
311
360
 
312
361
  raise SerializersNotFrozenError unless defined? @serialization_frozen
@@ -336,10 +385,17 @@ module MediaTypes
336
385
  selector.instance_exec(&block)
337
386
 
338
387
  raise UnmatchedSerializerError, serializer unless selector.matched
388
+
339
389
  obj = selector.value
340
390
  end
341
391
 
342
- serialization_render_resolved(obj: obj, serializer: serializer, identifier: identifier, registrations: registration, options: options)
392
+ serialization_render_resolved(
393
+ obj: obj,
394
+ serializer: serializer,
395
+ identifier: identifier,
396
+ registrations: registration,
397
+ options: options
398
+ )
343
399
  end
344
400
 
345
401
  def deserialize(request)
@@ -358,6 +414,7 @@ module MediaTypes
358
414
  raise SerializersNotFrozenError unless defined?(@serialization_frozen)
359
415
  raise NoInputReceivedError if request.content_type.blank?
360
416
  raise InputNotAcceptableError unless @serialization_input_registrations.has? request.content_type
417
+
361
418
  @serialization_input_registrations.call(@serialization_decoded_input, request.content_type, self)
362
419
  end
363
420
 
@@ -366,8 +423,9 @@ module MediaTypes
366
423
  return nil if identifier.nil?
367
424
 
368
425
  registration = registration.registrations[identifier]
369
-
426
+
370
427
  raise 'Assertion failed, inconsistent answer from resolve_media_type' if registration.nil?
428
+
371
429
  registration.serializer
372
430
  end
373
431
 
@@ -375,8 +433,12 @@ module MediaTypes
375
433
 
376
434
  def resolve_media_type(request, registration, allow_last: true)
377
435
  if defined? @serialization_override_accept
378
- @serialization_override_accept = registration.registrations.keys.last if allow_last && @serialization_override_accept == 'last'
436
+ if allow_last && @serialization_override_accept == 'last'
437
+ @serialization_override_accept = registration.registrations.keys.last
438
+ end
439
+
379
440
  return nil unless registration.has? @serialization_override_accept
441
+
380
442
  return @serialization_override_accept
381
443
  end
382
444
 
@@ -386,7 +448,7 @@ module MediaTypes
386
448
  #
387
449
  #
388
450
 
389
- accept_header = HttpHeaders::Accept.new(request.get_header(HEADER_ACCEPT)) || ''
451
+ accept_header = Utils::AcceptHeader.new(request.get_header(HEADER_ACCEPT)) || ''
390
452
  accept_header.each do |mime_type|
391
453
  stripped = mime_type.to_s.split(';')[0]
392
454
  next unless registration.has? stripped
@@ -403,7 +465,7 @@ module MediaTypes
403
465
  identifier = serializer.validator.identifier
404
466
  obj = { request: request, registrations: registrations }
405
467
  new_registrations = serializer.outputs_for(views: [nil])
406
-
468
+
407
469
  serialization_render_resolved(obj: obj, serializer: serializer, identifier: identifier, registrations: new_registrations, options: {})
408
470
  response.status = :not_acceptable
409
471
  end
@@ -424,7 +486,10 @@ module MediaTypes
424
486
  input_is_allowed = @serialization_input_registrations.has? request.content_type unless request.content_type.blank?
425
487
 
426
488
  unless input_is_allowed || all_allowed
427
- serializers = @serialization_unsupported_media_type_serializer || [MediaTypes::Serialization::Serializers::ProblemSerializer, MediaTypes::Serialization::Serializers::FallbackUnsupportedMediaTypeSerializer]
489
+ serializers = @serialization_unsupported_media_type_serializer || [
490
+ MediaTypes::Serialization::Serializers::ProblemSerializer,
491
+ MediaTypes::Serialization::Serializers::FallbackUnsupportedMediaTypeSerializer
492
+ ]
428
493
  registrations = SerializationRegistration.new(:output)
429
494
  serializers.each do |s|
430
495
  registrations = registrations.merge(s.outputs_for(views: [nil, :html]))
@@ -452,7 +517,10 @@ module MediaTypes
452
517
  input_data = request.body.read
453
518
  @serialization_decoded_input = @serialization_input_registrations.decode(input_data, request.content_type, self)
454
519
  rescue InputValidationFailedError => e
455
- serializers = @serialization_input_validation_failed_serializer || [MediaTypes::Serialization::Serializers::ProblemSerializer, MediaTypes::Serialization::Serializers::InputValidationErrorSerializer]
520
+ serializers = @serialization_input_validation_failed_serializer || [
521
+ MediaTypes::Serialization::Serializers::ProblemSerializer,
522
+ MediaTypes::Serialization::Serializers::InputValidationErrorSerializer
523
+ ]
456
524
  registrations = SerializationRegistration.new(:output)
457
525
  serializers.each do |s|
458
526
  registrations = registrations.merge(s.outputs_for(views: [nil, :html]))
@@ -498,7 +566,13 @@ module MediaTypes
498
566
  actions: @serialization_available_serializers,
499
567
  }
500
568
 
501
- serialization_render_resolved obj: input, serializer: description_serializer, identifier: endpoint_matched_identifier, registrations: @serialization_output_registrations, options: {}
569
+ serialization_render_resolved(
570
+ obj: input,
571
+ serializer: description_serializer,
572
+ identifier: endpoint_matched_identifier,
573
+ registrations: @serialization_output_registrations,
574
+ options: {}
575
+ )
502
576
  return
503
577
  end
504
578
 
@@ -30,6 +30,10 @@ module MediaTypes
30
30
  self.serializer_disable_wildcards = true
31
31
  end
32
32
 
33
+ def enable_wildcards
34
+ self.serializer_disable_wildcards = false
35
+ end
36
+
33
37
  def output(view: nil, version: nil, versions: nil, &block)
34
38
  versions = [version] if versions.nil?
35
39
  raise VersionsNotAnArrayError unless versions.is_a? Array
@@ -57,18 +61,18 @@ module MediaTypes
57
61
  end
58
62
  end
59
63
 
60
- def output_alias(media_type_identifier, view: nil)
64
+ def output_alias(media_type_identifier, view: nil, hide_variant: false)
61
65
  validator = serializer_validator.view(view)
62
66
  victim_identifier = validator.identifier
63
67
 
64
- serializer_output_registration.register_alias(self, media_type_identifier, victim_identifier, false, wildcards: !self.serializer_disable_wildcards)
68
+ serializer_output_registration.register_alias(self, media_type_identifier, victim_identifier, false, hide_variant, wildcards: !self.serializer_disable_wildcards)
65
69
  end
66
70
 
67
- def output_alias_optional(media_type_identifier, view: nil)
71
+ def output_alias_optional(media_type_identifier, view: nil, hide_variant: false)
68
72
  validator = serializer_validator.view(view)
69
73
  victim_identifier = validator.identifier
70
74
 
71
- serializer_output_registration.register_alias(self, media_type_identifier, victim_identifier, true, wildcards: !self.serializer_disable_wildcards)
75
+ serializer_output_registration.register_alias(self, media_type_identifier, victim_identifier, true, hide_variant, wildcards: !self.serializer_disable_wildcards)
72
76
  end
73
77
 
74
78
  def input(view: nil, version: nil, versions: nil, &block)
@@ -102,22 +106,22 @@ module MediaTypes
102
106
  validator = serializer_validator.view(view)
103
107
  victim_identifier = validator.identifier
104
108
 
105
- serializer_input_registration.register_alias(self, media_type_identifier, victim_identifier, false)
109
+ serializer_input_registration.register_alias(self, media_type_identifier, victim_identifier, false, true, wildcards: false)
106
110
  end
107
111
 
108
112
  def input_alias_optional(media_type_identifier, view: nil)
109
113
  validator = serializer_validator.view(view)
110
114
  victim_identifier = validator.identifier
111
115
 
112
- serializer_input_registration.register_alias(self, media_type_identifier, victim_identifier, true)
116
+ serializer_input_registration.register_alias(self, media_type_identifier, victim_identifier, true, true, wildcards: false)
113
117
  end
114
118
 
115
- def serialize(victim, media_type_identifier, context, dsl: nil, raw: nil)
119
+ def serialize(victim, media_type_identifier, context:, dsl: nil, raw: nil)
116
120
  dsl ||= SerializationDSL.new(self, context: context)
117
121
  serializer_output_registration.call(victim, media_type_identifier.to_s, context, dsl: dsl, raw: raw)
118
122
  end
119
123
 
120
- def deserialize(victim, media_type_identifier, context)
124
+ def deserialize(victim, media_type_identifier, context:)
121
125
  serializer_input_registration.call(victim, media_type_identifier, context)
122
126
  end
123
127
 
@@ -73,21 +73,29 @@ module MediaTypes
73
73
  def initialize(identifier, inout)
74
74
  super(
75
75
  "Serializer tried to define an #{inout}_alias that points to the media type identifier #{identifier} but no such #{inout} has been defined yet. Please move the #{inout} definition above the alias.\n\n" \
76
- "Move the output definition above the alias:\n" \
76
+ "Move the #{inout} definition above the alias:\n" \
77
77
  "\n" \
78
78
  "class MySerializer < MediaTypes::Serialization::Base\n" \
79
79
  "#...\n" \
80
- "output do\n" \
80
+ "#{inout} do\n" \
81
81
  " # ...\n" \
82
82
  "end\n" \
83
83
  "\n" \
84
- "output_alias 'text/html'\n" \
84
+ "#{inout}_alias 'text/html'\n" \
85
85
  "# ^----- move here\n" \
86
86
  'end'
87
87
  )
88
88
  end
89
89
  end
90
90
 
91
+ class VersionedAliasDefinitionError < ConfigurationError
92
+ def initialize(identifier, inout, prefix_match)
93
+ super(
94
+ "Serializer tried to define an #{inout}_alias that points to the media type identifier #{identifier} but no such #{inout} has been defined yet. An #{inout} named #{prefix_match} was found. Often this can be fixed by providing an #{inout} with a nil version."
95
+ )
96
+ end
97
+ end
98
+
91
99
  class DuplicateDefinitionError < ConfigurationError
92
100
  def initialize(identifier, inout)
93
101
  super("Serializer tried to define an #{inout} using the media type identifier #{identifier}, but another #{inout} was already defined with that identifier. Please remove one of the two.")
@@ -27,8 +27,8 @@ class FakeValidator
27
27
  def identifier
28
28
  suffix = suffixes[[internal_view, internal_version]] || ''
29
29
  result = prefix
30
- result += '.' + internal_view.to_s unless internal_view.nil?
31
30
  result += '.v' + internal_version.to_s unless internal_version.nil?
31
+ result += '.' + internal_view.to_s unless internal_view.nil?
32
32
  result += '+' + suffix.to_s unless suffix.empty?
33
33
 
34
34
  result
@@ -56,7 +56,7 @@ module MediaTypes
56
56
  array.each do |e|
57
57
  child_links = []
58
58
  context = SerializationDSL.new(__getobj__, child_links, context: @serialization_context)
59
- serializer.serialize(e, identifier, @serialization_context, dsl: context)
59
+ serializer.serialize(e, identifier, context: @serialization_context, dsl: context)
60
60
 
61
61
  self_links = child_links.select { |l| l[:rel] == :self }
62
62
  raise NoSelfLinkProvidedError, identifier unless self_links.any?
@@ -79,7 +79,7 @@ module MediaTypes
79
79
 
80
80
  array.each do |e|
81
81
  context = SerializationDSL.new(__getobj__, [], @serialization_vary, context: @serialization_context)
82
- result = serializer.serialize(e, identifier, @serialization_context, dsl: context, raw: true)
82
+ result = serializer.serialize(e, identifier, context: @serialization_context, dsl: context, raw: true)
83
83
 
84
84
  result = block.call(result) unless block.nil?
85
85
 
@@ -105,7 +105,7 @@ module MediaTypes
105
105
  def emit
106
106
  serialization_dsl_result
107
107
  end
108
-
108
+
109
109
  def object(&block)
110
110
  context = SerializationDSL.new(__getobj__, @serialization_links, @serialization_vary, context: @serialization_context)
111
111
  context.instance_exec(&block)
@@ -31,14 +31,23 @@ module MediaTypes
31
31
  register_wildcards(identifier, registration) if wildcards && inout == :output
32
32
  end
33
33
 
34
- def register_alias(serializer, alias_identifier, target_identifier, optional, wildcards: true)
35
- raise DuplicateDefinitionError.new(identifier, inout) if registrations.key? alias_identifier
34
+ def register_alias(serializer, alias_identifier, target_identifier, optional, hide_variant, wildcards: true)
35
+ raise DuplicateDefinitionError.new(alias_identifier, inout) if registrations.key? alias_identifier
36
36
 
37
- raise UnbackedAliasDefinitionError.new(target_identifier, inout) unless registrations.key? target_identifier
37
+ unless registrations.key? target_identifier
38
+ potential_match = registrations.keys.find do |k|
39
+ k.starts_with? target_identifier
40
+ end
41
+ raise VersionedAliasDefinitionError.new(target_identifier, inout, potential_match) unless potential_match.nil?
42
+ raise UnbackedAliasDefinitionError.new(target_identifier, inout)
43
+ end
38
44
 
39
45
  target = registrations[target_identifier]
40
46
 
41
- registration = SerializationAliasRegistration.new serializer, inout, target.validator, alias_identifier, target, optional
47
+ result_content_type = alias_identifier
48
+ result_content_type += "; variant=#{target_identifier}" unless hide_variant
49
+
50
+ registration = SerializationAliasRegistration.new serializer, inout, target.validator, result_content_type, target, optional, hide_variant
42
51
  registrations[alias_identifier] = registration
43
52
 
44
53
  register_wildcards(alias_identifier, registration) if wildcards && inout == :output
@@ -103,7 +112,7 @@ module MediaTypes
103
112
  private
104
113
 
105
114
  def register_wildcards(identifier, registration)
106
- new_alias = SerializationAliasRegistration.new registration.serializer, registration.inout, registration.validator, identifier, registration, true
115
+ new_alias = SerializationAliasRegistration.new registration.serializer, registration.inout, registration.validator, identifier, registration, true, true
107
116
 
108
117
  registrations['*/*'] = new_alias unless has? '*/*'
109
118
 
@@ -160,14 +169,14 @@ module MediaTypes
160
169
  begin
161
170
  victim = MediaTypes::Serialization.json_decoder.call(victim)
162
171
  validator.validate!(victim)
163
- rescue MediaTypes::Scheme::ValidationError, Oj::ParseError, JSON::ParserError => inner
172
+ rescue MediaTypes::Scheme::ValidationError, Oj::ParseError, JSON::ParserError, EncodingError => inner
164
173
  raise InputValidationFailedError, inner
165
174
  end
166
175
  else
167
176
  begin
168
177
  victim = MediaTypes::Serialization.json_decoder.call(victim)
169
178
  validator.validate!(victim)
170
- rescue MediaTypes::Scheme::ValidationError, JSON::ParserError => inner
179
+ rescue MediaTypes::Scheme::ValidationError, JSON::ParserError, EncodingError => inner
171
180
  raise InputValidationFailedError, inner
172
181
  end
173
182
  end
@@ -204,9 +213,10 @@ module MediaTypes
204
213
 
205
214
  # A registration that calls another registration when called.
206
215
  class SerializationAliasRegistration < SerializationBaseRegistration
207
- def initialize(serializer, inout, validator, display_identifier, target, optional)
216
+ def initialize(serializer, inout, validator, display_identifier, target, optional, hide_variant)
208
217
  self.target = target
209
218
  self.optional = optional
219
+ self.hide_variant = hide_variant
210
220
  super(serializer, inout, validator, display_identifier)
211
221
  end
212
222
 
@@ -229,7 +239,7 @@ module MediaTypes
229
239
  target.call(victim, context, dsl: dsl, raw: raw)
230
240
  end
231
241
 
232
- attr_accessor :target, :optional
242
+ attr_accessor :target, :optional, :hide_variant
233
243
  end
234
244
  end
235
245
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'erb'
4
4
  require 'media_types/serialization/base'
5
+ require 'media_types/serialization/utils/accept_language_header'
5
6
 
6
7
  module MediaTypes
7
8
  module Serialization
@@ -9,13 +10,19 @@ module MediaTypes
9
10
  class ProblemSerializer < MediaTypes::Serialization::Base
10
11
 
11
12
  unvalidated 'application/vnd.delftsolutions.problem'
13
+ disable_wildcards
12
14
 
13
15
  output do |problem, _, context|
14
16
  raise 'No translations defined, add at least one title' unless problem.translations.keys.any?
15
17
 
16
- # TODO: content-language selection
17
-
18
- translation = problem.translations[problem.translations.keys.first]
18
+ accept_language_header = Utils::AcceptLanguageHeader.new(context.request.get_header(HEADER_ACCEPT_LANGUAGE) || '')
19
+ translation_entry = accept_language_header.map do |locale|
20
+ problem.translations.keys.find do |l|
21
+ l.start_with? locale.locale
22
+ end
23
+ end.compact.first || problem.translations.keys.first
24
+ translation = problem.translations[translation_entry]
25
+
19
26
  title = translation[:title]
20
27
  detail = translation[:detail] || problem.error.message
21
28
 
@@ -33,12 +40,19 @@ module MediaTypes
33
40
  output_alias 'application/problem+json'
34
41
 
35
42
  output_raw view: :html do |problem, _, context|
36
- # TODO: content-language selection
37
-
38
- translation = problem.translations[problem.translations.keys.first]
43
+ accept_language_header = Utils::AcceptLanguageHeader.new(context.request.get_header(HEADER_ACCEPT_LANGUAGE) || '')
44
+ translation_entry = accept_language_header.map do |locale|
45
+ problem.translations.keys.find do |l|
46
+ l.starts_with? locale.locale
47
+ end
48
+ end.compact.first || problem.translations.keys.first
49
+ translation = problem.translations[translation_entry]
50
+
39
51
  title = translation[:title]
40
52
  detail = translation[:detail] || problem.error.message
41
53
 
54
+ detail_lang = translation[:detail].nil? ? 'en' : translation_entry
55
+
42
56
  input = OpenStruct.new(
43
57
  title: title,
44
58
  detail: detail,
@@ -61,11 +75,11 @@ module MediaTypes
61
75
  </header>
62
76
  <section id="content">
63
77
  <nav>
64
- <section id="description">
78
+ <section id="description" lang="#{translation_entry}">
65
79
  <h2><a href="<%= help_url %>"><%= CGI::escapeHTML(title) %></a></h2>
66
80
  </section>
67
81
  </nav>
68
- <main>
82
+ <main lang="#{detail_lang}">
69
83
  <p><%= detail %>
70
84
  </main>
71
85
  </section>
@@ -76,8 +90,7 @@ module MediaTypes
76
90
  template.result(input.instance_eval { binding })
77
91
  end
78
92
 
79
- # Hack: results in the alias being registered as */* wildcard
80
- self.serializer_output_registration.registrations.delete('*/*')
93
+ enable_wildcards
81
94
 
82
95
  output_alias_optional 'text/html', view: :html
83
96