media_types-serialization 1.0.1 → 1.2.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)
@@ -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