media_types-serialization 1.0.3 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.")
@@ -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
@@ -0,0 +1,82 @@
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 AcceptLanguageHeader < DelegateClass(Array)
31
+ def initialize(value)
32
+ __setobj__ HeaderList.new(value, entry_klazz: Entry)
33
+ end
34
+
35
+ class Entry
36
+
37
+ DELIMITER = '-'
38
+
39
+ attr_reader :locale, :region, :language
40
+
41
+ def initialize(locale, index:, parameters:)
42
+ self.locale = locale
43
+ # TODO: support extlang correctly, maybe we don't even need this
44
+ self.language, self.region = locale.split(DELIMITER)
45
+ self.parameters = parameters
46
+ self.index = index
47
+
48
+ freeze
49
+ end
50
+
51
+ # noinspection RubyInstanceMethodNamingConvention
52
+ def q
53
+ parameters.fetch(:q) { 1.0 }.to_f
54
+ end
55
+
56
+ def <=>(other)
57
+ quality = other.q <=> q
58
+ return quality unless quality.zero?
59
+ index <=> other.send(:index)
60
+ end
61
+
62
+ def [](parameter)
63
+ parameters.fetch(String(parameter).to_sym)
64
+ end
65
+
66
+ def to_header
67
+ to_s
68
+ end
69
+
70
+ def to_s
71
+ [locale].concat(parameters.map { |k, v| "#{k}=#{v}" }).compact.reject(&:empty?).join('; ')
72
+ end
73
+
74
+ private
75
+
76
+ attr_writer :locale, :region, :language
77
+ attr_accessor :parameters, :index
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,89 @@
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
+ module MediaTypes
26
+ module Serialization
27
+ module Utils
28
+ ##
29
+ # @example Accept values
30
+ #
31
+ # class AcceptHeader < DelegateClass(Array)
32
+ # def initialize(value)
33
+ # super MediaTypes::Serialization::Utils::HeaderList.new(value, entry_klazz: AcceptHeader::Entry)
34
+ # end
35
+ #
36
+ # class Entry
37
+ # def initialize(media_type, index: parameters:)
38
+ # ...
39
+ # end
40
+ #
41
+ # def q
42
+ # parameters.fetch(:q) { 1.0 }.to_f
43
+ # end
44
+ #
45
+ # def <=>(other)
46
+ # quality = other.q <=> q
47
+ # return quality unless quality.zero?
48
+ # index <=> other.index
49
+ # end
50
+ # end
51
+ # end
52
+ #
53
+ # Accept.new(['*/*; q=0.1', 'application/json, text/html; q=0.8'])
54
+ # # => List['application/json', 'text/html', '*/*']
55
+ #
56
+ module HeaderList
57
+ HEADER_DELIMITER = ','
58
+ PARAMETER_DELIMITER = ';'
59
+
60
+ module_function
61
+
62
+ def parse(combined, entry_klazz:)
63
+ Array(combined).map { |line| line.split(HEADER_DELIMITER) }.flatten.each_with_index.map do |entry, index|
64
+ value, *parameters = entry.strip.split(PARAMETER_DELIMITER)
65
+ indexed_parameters = ::Hash[Array(parameters).map { |p| p.strip.split('=') }].transform_keys!(&:to_sym)
66
+ entry_klazz.new(value, index: index, parameters: indexed_parameters)
67
+ end
68
+ end
69
+
70
+ def new(combined, entry_klazz:)
71
+ result = parse(combined, entry_klazz: entry_klazz)
72
+ entry_klazz.instance_methods(false).include?(:<=>) ? result.sort! : result
73
+ end
74
+
75
+ def to_header(list)
76
+ # noinspection RubyBlockToMethodReference
77
+ list.map { |entry| stringify_entry(entry) }
78
+ .join("#{HEADER_DELIMITER} ")
79
+ end
80
+
81
+ def stringify_entry(entry)
82
+ return entry.to_header if entry.respond_to?(:to_header)
83
+ return entry.to_s if entry.respond_to?(:to_s)
84
+ entry.inspect
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end