media_types-serialization 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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)
@@ -67,7 +67,21 @@ module MediaTypes
67
67
  quirks_mode: false
68
68
  )
69
69
  }
70
- self.json_decoder = Oj.method(:load)
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
+ }
71
85
  else
72
86
  require 'json'
73
87
  self.json_encoder = JSON.method(:pretty_generate)
@@ -169,7 +183,66 @@ module MediaTypes
169
183
  @serialization_output_registrations = @serialization_output_registrations.merge(mergeable_outputs)
170
184
  end
171
185
  end
172
-
186
+
187
+ def allow_output_html(as: nil, view: nil, layout: 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
+ if layout.nil?
201
+ if view.nil?
202
+ controller.render_to_string
203
+ else
204
+ controller.render_to_string(template: view)
205
+ end
206
+ else
207
+ if view.nil?
208
+ controller.render_to_string(layout: layout)
209
+ else
210
+ controller.render_to_string(template: view, layout: layout)
211
+ end
212
+ end
213
+ }
214
+
215
+ html_registration.register_block(nil, validator, nil, block, true, wildcards: true)
216
+ html_registration.registrations[validator.identifier].display_identifier = output_identifier
217
+ html_registration.registrations["#{validator.identifier.split('/')[0]}/*"].display_identifier = output_identifier
218
+ html_registration.registrations['*/*'].display_identifier = output_identifier
219
+
220
+ @serialization_output_registrations = @serialization_output_registrations.merge(html_registration)
221
+ end
222
+ end
223
+
224
+ def allow_output_docs(description, **filter_opts)
225
+ before_action(**filter_opts) do
226
+ raise SerializersAlreadyFrozenError if defined? @serialization_frozen
227
+
228
+ @serialization_output_registrations ||= SerializationRegistration.new(:output)
229
+
230
+ docs_registration = SerializationRegistration.new(:output)
231
+ validator = FakeValidator.new('text/vnd.delftsolutions.docs')
232
+
233
+ block = lambda { |_, _, _|
234
+ description
235
+ }
236
+
237
+ docs_registration.register_block(nil, validator, nil, block, true, wildcards: true)
238
+ docs_registration.registrations['text/vnd.delftsolutions.docs'].display_identifier = 'text/plain; charset=utf-8'
239
+ docs_registration.registrations['text/*'].display_identifier = 'text/plain; charset=utf-8'
240
+ docs_registration.registrations['*/*'].display_identifier = 'text/plain; charset=utf-8'
241
+
242
+ @serialization_output_registrations = @serialization_output_registrations.merge(docs_registration)
243
+ end
244
+ end
245
+
173
246
  def allow_api_viewer(serializer: MediaTypes::Serialization::Serializers::ApiViewer, **filter_opts)
174
247
  before_action do
175
248
  @serialization_api_viewer_enabled ||= {}
@@ -202,7 +275,7 @@ module MediaTypes
202
275
  raise ArrayInViewParameterError, :allow_input_serializer if view.is_a? Array
203
276
  views = [view] if views.nil?
204
277
  raise ViewsNotAnArrayError unless views.is_a? Array
205
-
278
+
206
279
  before_action do
207
280
  @serialization_available_serializers ||= {}
208
281
  @serialization_available_serializers[:input] ||= {}
@@ -245,8 +318,8 @@ module MediaTypes
245
318
  ##
246
319
  # Freezes additions to the serializes and notifies the controller what it will be able to respond to.
247
320
  #
248
- def freeze_io!
249
- before_action :serializer_freeze_io_internal
321
+ def freeze_io!(**filter_opts)
322
+ before_action :serializer_freeze_io_internal, **filter_opts
250
323
 
251
324
  output_error MediaTypes::Serialization::NoInputReceivedError do |p, error|
252
325
  p.title 'Providing input is mandatory. Please set a Content-Type', lang: 'en'
@@ -352,7 +425,7 @@ module MediaTypes
352
425
  return nil if identifier.nil?
353
426
 
354
427
  registration = registration.registrations[identifier]
355
-
428
+
356
429
  raise 'Assertion failed, inconsistent answer from resolve_media_type' if registration.nil?
357
430
  registration.serializer
358
431
  end
@@ -372,7 +445,7 @@ module MediaTypes
372
445
  #
373
446
  #
374
447
 
375
- accept_header = HttpHeaders::Accept.new(request.get_header(HEADER_ACCEPT)) || ''
448
+ accept_header = Utils::AcceptHeader.new(request.get_header(HEADER_ACCEPT)) || ''
376
449
  accept_header.each do |mime_type|
377
450
  stripped = mime_type.to_s.split(';')[0]
378
451
  next unless registration.has? stripped
@@ -389,7 +462,7 @@ module MediaTypes
389
462
  identifier = serializer.validator.identifier
390
463
  obj = { request: request, registrations: registrations }
391
464
  new_registrations = serializer.outputs_for(views: [nil])
392
-
465
+
393
466
  serialization_render_resolved(obj: obj, serializer: serializer, identifier: identifier, registrations: new_registrations, options: {})
394
467
  response.status = :not_acceptable
395
468
  end
@@ -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)
@@ -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
@@ -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
 
@@ -0,0 +1,77 @@
1
+ =begin
2
+ The MIT License (MIT)
3
+
4
+ Copyright (c) 2019 Derk-Jan Karrenbeld
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in
14
+ all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
+ THE SOFTWARE.
23
+ =end
24
+
25
+ require 'media_types/serialization/utils/header_list'
26
+
27
+ module MediaTypes
28
+ module Serialization
29
+ module Utils
30
+ class AcceptHeader < DelegateClass(Array)
31
+ def initialize(value)
32
+ __setobj__ HeaderList.new(value, entry_klazz: AcceptHeader::Entry)
33
+ end
34
+
35
+ class Entry
36
+ def initialize(media_type, index:, parameters:)
37
+ self.media_type = media_type
38
+ self.parameters = parameters
39
+ self.index = index
40
+
41
+ freeze
42
+ end
43
+
44
+ attr_reader :media_type
45
+
46
+ # noinspection RubyInstanceMethodNamingConvention
47
+ def q
48
+ parameters.fetch(:q) { 1.0 }.to_f
49
+ end
50
+
51
+ def <=>(other)
52
+ quality = other.q <=> q
53
+ return quality unless quality.zero?
54
+ index <=> other.send(:index)
55
+ end
56
+
57
+ def [](parameter)
58
+ parameters.fetch(String(parameter).to_sym)
59
+ end
60
+
61
+ def to_header
62
+ to_s
63
+ end
64
+
65
+ def to_s
66
+ [media_type].concat(parameters.map { |k, v| "#{k}=#{v}" }).compact.reject(&:empty?).join('; ')
67
+ end
68
+
69
+ private
70
+
71
+ attr_writer :media_type
72
+ attr_accessor :parameters, :index
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end