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