media_types-serialization 0.8.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|