media_types-serialization 0.8.0 → 1.1.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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +16 -3
- data/.prettierrc +1 -0
- data/CHANGELOG.md +42 -0
- data/CODE_OF_CONDUCT.md +74 -74
- data/Gemfile.lock +74 -83
- data/README.md +691 -179
- data/lib/media_types/problem.rb +64 -0
- data/lib/media_types/serialization.rb +497 -173
- data/lib/media_types/serialization/base.rb +115 -91
- data/lib/media_types/serialization/error.rb +186 -0
- data/lib/media_types/serialization/fake_validator.rb +52 -0
- data/lib/media_types/serialization/serialization_dsl.rb +117 -0
- data/lib/media_types/serialization/serialization_registration.rb +245 -0
- data/lib/media_types/serialization/serializers/api_viewer.rb +133 -0
- data/lib/media_types/serialization/serializers/common_css.rb +168 -0
- data/lib/media_types/serialization/serializers/endpoint_description_serializer.rb +80 -0
- data/lib/media_types/serialization/serializers/fallback_not_acceptable_serializer.rb +85 -0
- data/lib/media_types/serialization/serializers/fallback_unsupported_media_type_serializer.rb +58 -0
- data/lib/media_types/serialization/serializers/input_validation_error_serializer.rb +89 -0
- data/lib/media_types/serialization/serializers/problem_serializer.rb +100 -0
- data/lib/media_types/serialization/utils/accept_header.rb +77 -0
- data/lib/media_types/serialization/utils/accept_language_header.rb +82 -0
- data/lib/media_types/serialization/utils/header_list.rb +89 -0
- data/lib/media_types/serialization/version.rb +1 -1
- data/media_types-serialization.gemspec +48 -50
- metadata +48 -79
- data/.travis.yml +0 -17
- data/lib/generators/media_types/serialization/api_viewer/api_viewer_generator.rb +0 -25
- data/lib/generators/media_types/serialization/api_viewer/templates/api_viewer.html.erb +0 -98
- data/lib/generators/media_types/serialization/api_viewer/templates/initializer.rb +0 -33
- data/lib/generators/media_types/serialization/api_viewer/templates/template_controller.rb +0 -23
- data/lib/media_types/serialization/media_type/register.rb +0 -4
- data/lib/media_types/serialization/migrations_command.rb +0 -38
- data/lib/media_types/serialization/migrations_support.rb +0 -50
- data/lib/media_types/serialization/mime_type_support.rb +0 -64
- data/lib/media_types/serialization/no_content_type_given.rb +0 -11
- data/lib/media_types/serialization/no_media_type_serializers.rb +0 -11
- data/lib/media_types/serialization/no_serializer_for_content_type.rb +0 -15
- data/lib/media_types/serialization/renderer.rb +0 -41
- data/lib/media_types/serialization/renderer/register.rb +0 -4
- data/lib/media_types/serialization/wrapper.rb +0 -13
- data/lib/media_types/serialization/wrapper/html_wrapper.rb +0 -45
- data/lib/media_types/serialization/wrapper/media_collection_wrapper.rb +0 -59
- data/lib/media_types/serialization/wrapper/media_index_wrapper.rb +0 -59
- data/lib/media_types/serialization/wrapper/media_object_wrapper.rb +0 -55
- data/lib/media_types/serialization/wrapper_support.rb +0 -38
@@ -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'
|
@@ -7,139 +15,320 @@ require 'active_support/concern'
|
|
7
15
|
require 'active_support/core_ext/module/attribute_accessors'
|
8
16
|
require 'active_support/core_ext/object/blank'
|
9
17
|
|
10
|
-
require '
|
11
|
-
|
12
|
-
require 'media_types/serialization/no_media_type_serializers'
|
13
|
-
require 'media_types/serialization/no_serializer_for_content_type'
|
18
|
+
require 'media_types/serialization/utils/accept_header'
|
14
19
|
require 'media_types/serialization/base'
|
15
|
-
require 'media_types/serialization/
|
16
|
-
|
17
|
-
require 'awesome_print'
|
20
|
+
require 'media_types/serialization/error'
|
21
|
+
require 'media_types/serialization/serialization_dsl'
|
18
22
|
|
19
23
|
require 'delegate'
|
20
24
|
|
21
|
-
class
|
22
|
-
def initialize(
|
23
|
-
|
25
|
+
class SerializationSelectorDsl < SimpleDelegator
|
26
|
+
def initialize(controller, selected_serializer)
|
27
|
+
@serializer = selected_serializer
|
28
|
+
self.value = nil
|
29
|
+
self.matched = false
|
30
|
+
super controller
|
24
31
|
end
|
25
32
|
|
26
|
-
|
27
|
-
'application/vnd.xpbytes.api-viewer.v1'
|
28
|
-
end
|
33
|
+
attr_accessor :value, :matched
|
29
34
|
|
30
|
-
def
|
31
|
-
|
35
|
+
def serializer(klazz, obj = nil, &block)
|
36
|
+
return if klazz != @serializer
|
37
|
+
|
38
|
+
self.matched = true
|
39
|
+
self.value = block.nil? ? obj : yield
|
32
40
|
end
|
33
41
|
end
|
34
42
|
|
35
43
|
module MediaTypes
|
36
44
|
module Serialization
|
37
45
|
|
38
|
-
|
39
|
-
|
46
|
+
HEADER_ACCEPT = 'HTTP_ACCEPT'.freeze
|
47
|
+
HEADER_ACCEPT_LANGUAGE = 'HTTP_ACCEPT_LANGUAGE'.freeze
|
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 = ->(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
|
+
}
|
85
|
+
else
|
86
|
+
require 'json'
|
87
|
+
self.json_encoder = JSON.method(:pretty_generate)
|
88
|
+
self.json_decoder = ->(txt) {
|
89
|
+
JSON.parse(txt, {
|
90
|
+
symbolize_names: true,
|
91
|
+
allow_nan: false,
|
92
|
+
create_additions: false,
|
93
|
+
object_class: ::Hash,
|
94
|
+
array_class: ::Array,
|
95
|
+
})
|
96
|
+
}
|
97
|
+
end
|
40
98
|
|
41
99
|
extend ActiveSupport::Concern
|
42
100
|
|
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
101
|
# rubocop:disable Metrics/BlockLength
|
49
102
|
class_methods do
|
50
103
|
|
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)
|
104
|
+
def not_acceptable_serializer(serializer, **filter_opts)
|
67
105
|
before_action(**filter_opts) do
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
end
|
106
|
+
raise SerializersAlreadyFrozenError if defined? @serialization_frozen
|
107
|
+
|
108
|
+
@serialization_not_acceptable_serializer = serializer
|
72
109
|
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def unsupported_media_type_serializer(serializer, **filter_opts)
|
113
|
+
before_action(**filter_opts) do
|
114
|
+
raise SerializersAlreadyFrozenError if defined? @serialization_frozen
|
73
115
|
|
74
|
-
|
75
|
-
|
116
|
+
@serialization_unsupported_media_type_serializer ||= []
|
117
|
+
@serialization_unsupported_media_type_serializer.append(serializer)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def clear_unsupported_media_type_serializer!(**filter_opts)
|
122
|
+
before_action(**filter_opts) do
|
123
|
+
raise SerializersAlreadyFrozenError if defined? @serialization_frozen
|
124
|
+
|
125
|
+
@serialization_unsupported_media_type_serializer = []
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def input_validation_failed_serializer(serializer, **filter_opts)
|
130
|
+
before_action(**filter_opts) do
|
131
|
+
raise SerializersAlreadyFrozenError if defined? @serialization_frozen
|
132
|
+
|
133
|
+
@serialization_input_validation_failed_serializer ||= []
|
134
|
+
@serialization_input_validation_failed_serializer.append(serializer)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def clear_input_validation_failed_serializers!(**filter_opts)
|
139
|
+
before_action(**filter_opts) do
|
140
|
+
raise SerializersAlreadyFrozenError if defined? @serialization_frozen
|
141
|
+
|
142
|
+
@serialization_input_validation_failed_serializer = []
|
143
|
+
end
|
76
144
|
end
|
77
145
|
|
78
146
|
##
|
79
|
-
#
|
147
|
+
# Allow output serialization using the passed in +serializer+ for the given +view+
|
80
148
|
#
|
81
|
-
#
|
82
|
-
# the serialization.
|
149
|
+
# @see #freeze_io!
|
83
150
|
#
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
151
|
+
# @param serializer the serializer to use for serialization.
|
152
|
+
# @param [(String | NilClass|)] view the view it should use the serializer for. Use nil for no view
|
153
|
+
# @param [(String | NilClass|)[]|NilClass] views the views it should use the serializer for. Use nil for no view
|
154
|
+
#
|
155
|
+
def allow_output_serializer(serializer, view: nil, views: nil, **filter_opts)
|
156
|
+
raise SerializersAlreadyFrozenError if defined? @serialization_frozen
|
157
|
+
raise ArrayInViewParameterError, :allow_output_serializer if view.is_a? Array
|
158
|
+
|
159
|
+
views = [view] if views.nil?
|
160
|
+
raise ViewsNotAnArrayError unless views.is_a? Array
|
161
|
+
|
162
|
+
before_action do
|
163
|
+
@serialization_available_serializers ||= {}
|
164
|
+
@serialization_available_serializers[:output] ||= {}
|
165
|
+
actions = filter_opts[:only] || :all_actions
|
166
|
+
actions = [actions] unless actions.is_a?(Array)
|
167
|
+
actions.each do |action|
|
168
|
+
@serialization_available_serializers[:output][action.to_s] ||= []
|
169
|
+
views.each do |v|
|
170
|
+
@serialization_available_serializers[:output][action.to_s].push({serializer: serializer, view: v})
|
171
|
+
end
|
89
172
|
end
|
90
173
|
end
|
174
|
+
|
175
|
+
before_action(**filter_opts) do
|
176
|
+
raise SerializersAlreadyFrozenError if defined? @serialization_frozen
|
177
|
+
|
178
|
+
@serialization_output_registrations ||= SerializationRegistration.new(:output)
|
179
|
+
|
180
|
+
mergeable_outputs = serializer.outputs_for(views: views)
|
181
|
+
raise AddedEmptyOutputSerializer, serializer.name if mergeable_outputs.registrations.empty?
|
182
|
+
|
183
|
+
@serialization_output_registrations = @serialization_output_registrations.merge(mergeable_outputs)
|
184
|
+
end
|
91
185
|
end
|
92
186
|
|
93
|
-
|
94
|
-
# Same as +accept_html+ but then for Api Viewer
|
95
|
-
#
|
96
|
-
def accept_api_viewer(serializer, view: [nil], overwrite: true, **filter_opts)
|
187
|
+
def allow_output_html(as: nil, layout: nil, **filter_opts)
|
97
188
|
before_action(**filter_opts) do
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
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
|
+
controller.render_to_string
|
202
|
+
else
|
203
|
+
controller.render_to_string(layout: layout)
|
105
204
|
end
|
205
|
+
}
|
206
|
+
|
207
|
+
html_registration.register_block(nil, validator, nil, block, true, wildcards: true)
|
208
|
+
html_registration.registrations[validator.identifier].display_identifier = output_identifier
|
209
|
+
html_registration.registrations["#{validator.identifier.split('/')[0]}/*"].display_identifier = output_identifier
|
210
|
+
html_registration.registrations['*/*'].display_identifier = output_identifier
|
211
|
+
|
212
|
+
@serialization_output_registrations = @serialization_output_registrations.merge(html_registration)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def allow_output_docs(description, **filter_opts)
|
217
|
+
before_action(**filter_opts) do
|
218
|
+
raise SerializersAlreadyFrozenError if defined? @serialization_frozen
|
219
|
+
|
220
|
+
@serialization_output_registrations ||= SerializationRegistration.new(:output)
|
221
|
+
|
222
|
+
docs_registration = SerializationRegistration.new(:output)
|
223
|
+
validator = FakeValidator.new('text/vnd.delftsolutions.docs')
|
224
|
+
|
225
|
+
block = lambda { |_, _, _|
|
226
|
+
description
|
227
|
+
}
|
228
|
+
|
229
|
+
docs_registration.register_block(nil, validator, nil, block, true, wildcards: true)
|
230
|
+
docs_registration.registrations['text/vnd.delftsolutions.docs'].display_identifier = 'text/plain; charset=utf-8'
|
231
|
+
docs_registration.registrations['text/*'].display_identifier = 'text/plain; charset=utf-8'
|
232
|
+
docs_registration.registrations['*/*'].display_identifier = 'text/plain; charset=utf-8'
|
233
|
+
|
234
|
+
@serialization_output_registrations = @serialization_output_registrations.merge(docs_registration)
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
def allow_api_viewer(serializer: MediaTypes::Serialization::Serializers::ApiViewer, **filter_opts)
|
239
|
+
before_action do
|
240
|
+
@serialization_api_viewer_enabled ||= {}
|
241
|
+
actions = filter_opts[:only] || :all_actions
|
242
|
+
actions = [actions] unless actions.kind_of?(Array)
|
243
|
+
actions.each do |action|
|
244
|
+
@serialization_api_viewer_enabled[action.to_s] = true
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
before_action(**filter_opts) do
|
249
|
+
if request.query_parameters['api_viewer']
|
250
|
+
@serialization_override_accept = request.query_parameters['api_viewer'].sub ' ', '+'
|
251
|
+
@serialization_wrapping_renderer = serializer
|
106
252
|
end
|
107
253
|
end
|
108
254
|
end
|
109
255
|
|
110
256
|
##
|
111
|
-
#
|
112
|
-
# This is done for file serving and redirects.
|
257
|
+
# Allow input serialization using the passed in +serializer+ for the given +view+
|
113
258
|
#
|
114
|
-
# @
|
259
|
+
# @see #freeze_io!
|
115
260
|
#
|
116
|
-
# @
|
261
|
+
# @param serializer the serializer to use for deserialization
|
262
|
+
# @param [(String | NilClass|)] view the view it should serializer for. Use nil for no view
|
263
|
+
# @param [(String | NilClass|)[]|NilClass] views the views it should serializer for. Use nil for no view
|
117
264
|
#
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
265
|
+
def allow_input_serializer(serializer, view: nil, views: nil, **filter_opts)
|
266
|
+
raise SerializersAlreadyFrozenError if defined? @serialization_frozen
|
267
|
+
raise ArrayInViewParameterError, :allow_input_serializer if view.is_a? Array
|
268
|
+
views = [view] if views.nil?
|
269
|
+
raise ViewsNotAnArrayError unless views.is_a? Array
|
270
|
+
|
271
|
+
before_action do
|
272
|
+
@serialization_available_serializers ||= {}
|
273
|
+
@serialization_available_serializers[:input] ||= {}
|
274
|
+
actions = filter_opts[:only] || :all_actions
|
275
|
+
actions = [actions] unless actions.is_a?(Array)
|
276
|
+
actions.each do |action|
|
277
|
+
@serialization_available_serializers[:input][action.to_s] ||= []
|
278
|
+
views.each do |v|
|
279
|
+
@serialization_available_serializers[:input][action.to_s].push({serializer: serializer, view: v})
|
280
|
+
end
|
128
281
|
end
|
129
282
|
end
|
283
|
+
|
284
|
+
before_action(**filter_opts) do
|
285
|
+
raise SerializersAlreadyFrozenError if defined? @serialization_frozen
|
286
|
+
|
287
|
+
@serialization_input_registrations ||= SerializationRegistration.new(:input)
|
288
|
+
|
289
|
+
mergeable_inputs = serializer.inputs_for(views: views)
|
290
|
+
raise AddedEmptyInputSerializer, serializer.name if mergeable_inputs.registrations.empty?
|
291
|
+
|
292
|
+
@serialization_input_registrations = @serialization_input_registrations.merge(mergeable_inputs)
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
def allow_all_output(**filter_opts)
|
297
|
+
before_action(**filter_opts) do
|
298
|
+
@serialization_output_registrations ||= SerializationRegistration.new(:output)
|
299
|
+
@serialization_output_allow_all ||= true
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
def allow_all_input(**filter_opts)
|
304
|
+
before_action(**filter_opts) do
|
305
|
+
@serialization_input_registrations ||= SerializationRegistration.new(:input)
|
306
|
+
@serialization_input_allow_all ||= true
|
307
|
+
end
|
130
308
|
end
|
131
309
|
|
132
310
|
##
|
133
311
|
# Freezes additions to the serializes and notifies the controller what it will be able to respond to.
|
134
312
|
#
|
135
|
-
def
|
136
|
-
before_action
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
313
|
+
def freeze_io!(**filter_opts)
|
314
|
+
before_action :serializer_freeze_io_internal, **filter_opts
|
315
|
+
|
316
|
+
output_error MediaTypes::Serialization::NoInputReceivedError do |p, error|
|
317
|
+
p.title 'Providing input is mandatory. Please set a Content-Type', lang: 'en'
|
318
|
+
|
319
|
+
p.status_code :bad_request
|
320
|
+
end
|
321
|
+
end
|
141
322
|
|
142
|
-
|
323
|
+
def output_error(klazz, &block)
|
324
|
+
rescue_from klazz do |error|
|
325
|
+
problem = Problem.new(error)
|
326
|
+
block.call(problem, error) unless block.nil?
|
327
|
+
|
328
|
+
serializer = MediaTypes::Serialization::Serializers::ProblemSerializer
|
329
|
+
registrations = serializer.outputs_for(views: [:html, nil])
|
330
|
+
|
331
|
+
render_media(problem, serializers: [registrations], status: problem.response_status_code)
|
143
332
|
end
|
144
333
|
end
|
145
334
|
end
|
@@ -148,139 +337,274 @@ module MediaTypes
|
|
148
337
|
included do
|
149
338
|
protected
|
150
339
|
|
151
|
-
attr_accessor :serializers
|
152
340
|
end
|
153
341
|
|
154
342
|
protected
|
155
343
|
|
156
|
-
def
|
157
|
-
|
344
|
+
def serialize(victim, media_type, serializer: Object.new, links: [], vary: ['Accept'])
|
345
|
+
context = SerializationDSL.new(serializer, links, vary, context: self)
|
346
|
+
context.instance_exec { @serialization_output_registrations.call(victim, media_type, context) }
|
158
347
|
end
|
159
348
|
|
160
|
-
|
161
|
-
@last_serialize_media = media
|
162
|
-
@last_media_serializer = serializer.call(media, context: self)
|
163
|
-
end
|
349
|
+
MEDIA_TYPES_SERIALIZATION_OBJ_IS_UNDEFINED = ::Object.new
|
164
350
|
|
165
|
-
def
|
166
|
-
|
167
|
-
|
351
|
+
def render_media(obj = MEDIA_TYPES_SERIALIZATION_OBJ_IS_UNDEFINED, serializers: nil, not_acceptable_serializer: nil, **options, &block)
|
352
|
+
if obj == MEDIA_TYPES_SERIALIZATION_OBJ_IS_UNDEFINED && options.keys.any? && !block
|
353
|
+
# options is too greedy :(
|
354
|
+
obj = options
|
355
|
+
options = {}
|
356
|
+
end
|
168
357
|
|
169
|
-
|
170
|
-
|
171
|
-
serializers.each_key do |mime|
|
172
|
-
next unless matcher.call(mime: mime, format: format)
|
173
|
-
format.custom(mime, &block)
|
174
|
-
end
|
358
|
+
if obj == MEDIA_TYPES_SERIALIZATION_OBJ_IS_UNDEFINED && block.nil?
|
359
|
+
raise 'render_media was called without an object. Please provide one or supply a block to match the serializer.'
|
175
360
|
end
|
176
|
-
|
361
|
+
obj = nil if obj == MEDIA_TYPES_SERIALIZATION_OBJ_IS_UNDEFINED
|
177
362
|
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
363
|
+
raise SerializersNotFrozenError unless defined? @serialization_frozen
|
364
|
+
|
365
|
+
not_acceptable_serializer ||= @serialization_not_acceptable_serializer if defined? @serialization_not_acceptable_serializer
|
366
|
+
|
367
|
+
@serialization_output_registrations ||= SerializationRegistration.new(:output)
|
368
|
+
registration = @serialization_output_registrations
|
369
|
+
unless serializers.nil?
|
370
|
+
registration = SerializationRegistration.new(:output)
|
371
|
+
serializers.each do |s|
|
372
|
+
registration = registration.merge(s)
|
186
373
|
end
|
374
|
+
end
|
375
|
+
|
376
|
+
identifier = resolve_media_type(request, registration)
|
377
|
+
|
378
|
+
if identifier.nil?
|
379
|
+
serialization_render_not_acceptable(registration, not_acceptable_serializer)
|
380
|
+
return
|
381
|
+
end
|
382
|
+
|
383
|
+
serializer = resolve_serializer(request, identifier, registration)
|
187
384
|
|
188
|
-
|
385
|
+
unless block.nil?
|
386
|
+
selector = SerializationSelectorDsl.new(self, serializer)
|
387
|
+
selector.instance_exec(&block)
|
388
|
+
|
389
|
+
raise UnmatchedSerializerError, serializer unless selector.matched
|
390
|
+
obj = selector.value
|
189
391
|
end
|
392
|
+
|
393
|
+
serialization_render_resolved(obj: obj, serializer: serializer, identifier: identifier, registrations: registration, options: options)
|
190
394
|
end
|
191
395
|
|
192
|
-
|
193
|
-
|
194
|
-
# end
|
396
|
+
def deserialize(request)
|
397
|
+
raise SerializersNotFrozenError unless defined?(@serialization_frozen)
|
195
398
|
|
196
|
-
|
197
|
-
|
399
|
+
result = nil
|
400
|
+
begin
|
401
|
+
result = deserialize!(request)
|
402
|
+
rescue NoInputReceivedError
|
403
|
+
return nil
|
404
|
+
end
|
405
|
+
result
|
198
406
|
end
|
199
407
|
|
200
|
-
def
|
201
|
-
raise
|
408
|
+
def deserialize!(request)
|
409
|
+
raise SerializersNotFrozenError unless defined?(@serialization_frozen)
|
410
|
+
raise NoInputReceivedError if request.content_type.blank?
|
411
|
+
raise InputNotAcceptableError unless @serialization_input_registrations.has? request.content_type
|
412
|
+
@serialization_input_registrations.call(@serialization_decoded_input, request.content_type, self)
|
202
413
|
end
|
203
414
|
|
204
|
-
|
415
|
+
def resolve_serializer(request, identifier = nil, registration = @serialization_output_registrations)
|
416
|
+
identifier = resolve_media_type(request, registration) if identifier.nil?
|
417
|
+
return nil if identifier.nil?
|
418
|
+
|
419
|
+
registration = registration.registrations[identifier]
|
205
420
|
|
206
|
-
|
207
|
-
|
421
|
+
raise 'Assertion failed, inconsistent answer from resolve_media_type' if registration.nil?
|
422
|
+
registration.serializer
|
208
423
|
end
|
209
424
|
|
210
|
-
|
211
|
-
raise NoMediaTypeSerializers unless serializers
|
425
|
+
private
|
212
426
|
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
if serializers[request.format.to_s]
|
219
|
-
return serializers[request.format.to_s]
|
427
|
+
def resolve_media_type(request, registration, allow_last: true)
|
428
|
+
if defined? @serialization_override_accept
|
429
|
+
@serialization_override_accept = registration.registrations.keys.last if allow_last && @serialization_override_accept == 'last'
|
430
|
+
return nil unless registration.has? @serialization_override_accept
|
431
|
+
return @serialization_override_accept
|
220
432
|
end
|
221
433
|
|
222
434
|
# Ruby negotiation
|
223
435
|
#
|
224
436
|
# 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
437
|
#
|
227
|
-
# respond_to_accept do ... end
|
228
438
|
#
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
439
|
+
|
440
|
+
accept_header = Utils::AcceptHeader.new(request.get_header(HEADER_ACCEPT)) || ''
|
441
|
+
accept_header.each do |mime_type|
|
442
|
+
stripped = mime_type.to_s.split(';')[0]
|
443
|
+
next unless registration.has? stripped
|
444
|
+
|
445
|
+
return stripped
|
234
446
|
end
|
235
447
|
|
236
|
-
|
448
|
+
nil
|
237
449
|
end
|
238
450
|
|
239
|
-
def
|
240
|
-
|
451
|
+
def serialization_render_not_acceptable(registrations, override = nil)
|
452
|
+
serializer = override
|
453
|
+
serializer ||= MediaTypes::Serialization::Serializers::FallbackNotAcceptableSerializer
|
454
|
+
identifier = serializer.validator.identifier
|
455
|
+
obj = { request: request, registrations: registrations }
|
456
|
+
new_registrations = serializer.outputs_for(views: [nil])
|
241
457
|
|
242
|
-
|
243
|
-
|
458
|
+
serialization_render_resolved(obj: obj, serializer: serializer, identifier: identifier, registrations: new_registrations, options: {})
|
459
|
+
response.status = :not_acceptable
|
460
|
+
end
|
461
|
+
|
462
|
+
def serializer_freeze_io_internal
|
463
|
+
raise UnableToRefreezeError if defined? @serialization_frozen
|
464
|
+
|
465
|
+
@serialization_frozen = true
|
466
|
+
@serialization_input_registrations ||= SerializationRegistration.new(:input)
|
467
|
+
|
468
|
+
raise NoOutputSerializersDefinedError unless defined? @serialization_output_registrations
|
469
|
+
|
470
|
+
# Input content-type negotiation and validation
|
471
|
+
all_allowed = false
|
472
|
+
all_allowed ||= @serialization_input_allow_all if defined?(@serialization_input_allow_all)
|
473
|
+
|
474
|
+
input_is_allowed = true
|
475
|
+
input_is_allowed = @serialization_input_registrations.has? request.content_type unless request.content_type.blank?
|
476
|
+
|
477
|
+
unless input_is_allowed || all_allowed
|
478
|
+
serializers = @serialization_unsupported_media_type_serializer || [MediaTypes::Serialization::Serializers::ProblemSerializer, MediaTypes::Serialization::Serializers::FallbackUnsupportedMediaTypeSerializer]
|
479
|
+
registrations = SerializationRegistration.new(:output)
|
480
|
+
serializers.each do |s|
|
481
|
+
registrations = registrations.merge(s.outputs_for(views: [nil, :html]))
|
482
|
+
end
|
483
|
+
|
484
|
+
input = {
|
485
|
+
registrations: @serialization_input_registrations
|
486
|
+
}
|
487
|
+
|
488
|
+
render_media nil, serializers: [registrations], status: :unsupported_media_type do
|
489
|
+
serializer MediaTypes::Serialization::Serializers::FallbackUnsupportedMediaTypeSerializer, input
|
490
|
+
serializer MediaTypes::Serialization::Serializers::ProblemSerializer do
|
491
|
+
error = UnsupportedMediaTypeError.new(input[:registrations].registrations.keys)
|
492
|
+
problem = Problem.new(error)
|
493
|
+
problem.title 'Unable to process your body Content-Type.', lang: 'en'
|
244
494
|
|
245
|
-
|
246
|
-
|
247
|
-
Array(serializer.media_type(view: media_view)).each do |media_type|
|
248
|
-
yield media_type, media_view, registered, register
|
495
|
+
problem
|
496
|
+
end
|
249
497
|
end
|
498
|
+
return
|
250
499
|
end
|
251
|
-
end
|
252
500
|
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
501
|
+
if input_is_allowed && !request.content_type.blank?
|
502
|
+
begin
|
503
|
+
input_data = request.body.read
|
504
|
+
@serialization_decoded_input = @serialization_input_registrations.decode(input_data, request.content_type, self)
|
505
|
+
rescue InputValidationFailedError => e
|
506
|
+
serializers = @serialization_input_validation_failed_serializer || [MediaTypes::Serialization::Serializers::ProblemSerializer, MediaTypes::Serialization::Serializers::InputValidationErrorSerializer]
|
507
|
+
registrations = SerializationRegistration.new(:output)
|
508
|
+
serializers.each do |s|
|
509
|
+
registrations = registrations.merge(s.outputs_for(views: [nil, :html]))
|
510
|
+
end
|
511
|
+
|
512
|
+
input = {
|
513
|
+
identifier: request.content_type,
|
514
|
+
input: input_data,
|
515
|
+
error: e,
|
516
|
+
}
|
517
|
+
|
518
|
+
render_media nil, serializers: [registrations], status: :unprocessable_entity do
|
519
|
+
serializer MediaTypes::Serialization::Serializers::InputValidationErrorSerializer, input
|
520
|
+
serializer MediaTypes::Serialization::Serializers::ProblemSerializer do
|
521
|
+
problem = Problem.new(e)
|
522
|
+
problem.title 'Input failed to validate.', lang: 'en'
|
523
|
+
|
524
|
+
problem
|
525
|
+
end
|
526
|
+
end
|
527
|
+
return
|
528
|
+
end
|
259
529
|
end
|
530
|
+
|
531
|
+
# Endpoint description media type
|
532
|
+
|
533
|
+
description_serializer = MediaTypes::Serialization::Serializers::EndpointDescriptionSerializer
|
534
|
+
|
535
|
+
# All endpoints have endpoint description.
|
536
|
+
# Placed in front of the list to make sure the api viewer doesn't pick it.
|
537
|
+
@serialization_output_registrations = description_serializer.outputs_for(views: [nil]).merge(@serialization_output_registrations)
|
538
|
+
|
539
|
+
endpoint_matched_identifier = resolve_media_type(request, description_serializer.serializer_output_registration, allow_last: false)
|
540
|
+
if endpoint_matched_identifier
|
541
|
+
# We picked an endpoint description media type
|
542
|
+
#
|
543
|
+
@serialization_available_serializers ||= {}
|
544
|
+
@serialization_available_serializers[:output] ||= {}
|
545
|
+
@serialization_api_viewer_enabled ||= {}
|
546
|
+
|
547
|
+
input = {
|
548
|
+
api_viewer: @serialization_api_viewer_enabled,
|
549
|
+
actions: @serialization_available_serializers,
|
550
|
+
}
|
551
|
+
|
552
|
+
serialization_render_resolved obj: input, serializer: description_serializer, identifier: endpoint_matched_identifier, registrations: @serialization_output_registrations, options: {}
|
553
|
+
return
|
554
|
+
end
|
555
|
+
|
556
|
+
# Output content negotiation
|
557
|
+
resolved_identifier = resolve_media_type(request, @serialization_output_registrations)
|
558
|
+
|
559
|
+
not_acceptable_serializer = nil
|
560
|
+
not_acceptable_serializer = @serialization_not_acceptable_serializer if defined? @serialization_not_acceptable_serializer
|
561
|
+
not_acceptable_serializer ||= MediaTypes::Serialization::Serializers::FallbackNotAcceptableSerializer
|
562
|
+
|
563
|
+
can_satisfy_allow = !resolved_identifier.nil?
|
564
|
+
can_satisfy_allow ||= @serialization_output_allow_all if defined?(@serialization_output_allow_all)
|
565
|
+
|
566
|
+
serialization_render_not_acceptable(@serialization_output_registrations, not_acceptable_serializer) unless can_satisfy_allow
|
260
567
|
end
|
261
568
|
|
262
|
-
def
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
569
|
+
def serialization_render_resolved(obj:, identifier:, serializer:, registrations:, options:)
|
570
|
+
links = []
|
571
|
+
vary = []
|
572
|
+
context = SerializationDSL.new(serializer, links, vary, context: self)
|
573
|
+
result = registrations.call(obj, identifier, self, dsl: context)
|
574
|
+
|
575
|
+
if links.any?
|
576
|
+
items = links.map do |l|
|
577
|
+
href_part = "<#{l[:href]}>"
|
578
|
+
tags = l.to_a.select { |k,_| k != :href }.map { |k,v| "#{k}=#{v}" }
|
579
|
+
([href_part] + tags).join('; ')
|
580
|
+
end
|
581
|
+
response.set_header('Link', items.join(', '))
|
582
|
+
end
|
583
|
+
|
584
|
+
if vary.any?
|
585
|
+
current_vary = (response.headers['Vary'] || "").split(',').map { |v| v.strip }.reject { |v| v.empty? }.sort
|
586
|
+
merged_vary = (vary.sort + current_vary).uniq
|
587
|
+
|
588
|
+
response.set_header('Vary', merged_vary.join(', '))
|
589
|
+
end
|
590
|
+
|
591
|
+
if defined? @serialization_wrapping_renderer
|
592
|
+
input = {
|
593
|
+
identifier: identifier,
|
594
|
+
registrations: registrations,
|
595
|
+
output: result,
|
596
|
+
links: links,
|
597
|
+
}
|
598
|
+
wrapped = @serialization_wrapping_renderer.serialize input, '*/*', self
|
599
|
+
render body: wrapped
|
600
|
+
|
601
|
+
response.content_type = 'text/html'
|
602
|
+
return
|
283
603
|
end
|
604
|
+
|
605
|
+
render body: result, **options
|
606
|
+
|
607
|
+
response.content_type = registrations.identifier_for(identifier)
|
284
608
|
end
|
285
609
|
end
|
286
610
|
end
|