media_types-serialization 0.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 +7 -0
- data/.gitignore +12 -0
- data/.idea/.rakeTasks +7 -0
- data/.idea/dictionaries/Derk_Jan.xml +7 -0
- data/.idea/inspectionProfiles/Project_Default.xml +6 -0
- data/.idea/media_types-serialization.iml +55 -0
- data/.idea/misc.xml +7 -0
- data/.idea/modules.xml +8 -0
- data/.idea/runConfigurations/test.xml +20 -0
- data/.idea/vcs.xml +6 -0
- data/.travis.yml +17 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +76 -0
- data/LICENSE.txt +21 -0
- data/README.md +209 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/media_types/serialization.rb +194 -0
- data/lib/media_types/serialization/base.rb +138 -0
- data/lib/media_types/serialization/error.rb +7 -0
- data/lib/media_types/serialization/migrations_command.rb +38 -0
- data/lib/media_types/serialization/migrations_support.rb +41 -0
- data/lib/media_types/serialization/mime_type_support.rb +55 -0
- data/lib/media_types/serialization/no_content_type_given.rb +11 -0
- data/lib/media_types/serialization/no_media_type_serializers.rb +11 -0
- data/lib/media_types/serialization/no_serializer_for_content_type.rb +15 -0
- data/lib/media_types/serialization/renderer.rb +31 -0
- data/lib/media_types/serialization/renderer/register.rb +4 -0
- data/lib/media_types/serialization/version.rb +5 -0
- data/lib/media_types/serialization/wrapper.rb +15 -0
- data/lib/media_types/serialization/wrapper/html_wrapper.rb +42 -0
- data/lib/media_types/serialization/wrapper/media_collection_wrapper.rb +48 -0
- data/lib/media_types/serialization/wrapper/media_index_wrapper.rb +48 -0
- data/lib/media_types/serialization/wrapper/media_object_wrapper.rb +45 -0
- data/lib/media_types/serialization/wrapper/media_wrapper.rb +32 -0
- data/lib/media_types/serialization/wrapper/root_key.rb +20 -0
- data/media_types-serialization.gemspec +48 -0
- metadata +212 -0
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +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__)
|
data/bin/setup
ADDED
@@ -0,0 +1,194 @@
|
|
1
|
+
require 'media_types/serialization/version'
|
2
|
+
|
3
|
+
require 'abstract_controller'
|
4
|
+
require 'action_controller/metal/mime_responds'
|
5
|
+
require 'action_dispatch/http/mime_type'
|
6
|
+
require 'active_support/concern'
|
7
|
+
require 'active_support/core_ext/module/attribute_accessors'
|
8
|
+
|
9
|
+
require 'http_headers/accept'
|
10
|
+
|
11
|
+
require 'media_types/serialization/no_media_type_serializers'
|
12
|
+
require 'media_types/serialization/no_serializer_for_content_type'
|
13
|
+
require 'media_types/serialization/wrapper/html_wrapper'
|
14
|
+
require 'media_types/serialization/wrapper/media_wrapper'
|
15
|
+
|
16
|
+
module MediaTypes
|
17
|
+
module Serialization
|
18
|
+
|
19
|
+
mattr_accessor :common_suffix
|
20
|
+
|
21
|
+
extend ActiveSupport::Concern
|
22
|
+
|
23
|
+
HEADER_ACCEPT = 'HTTP_ACCEPT'
|
24
|
+
MEDIA_TYPE_HTML = 'text/html'
|
25
|
+
|
26
|
+
# rubocop:disable Metrics/BlockLength
|
27
|
+
class_methods do
|
28
|
+
# @see #freeze_accepted_media!
|
29
|
+
#
|
30
|
+
def accept_serialization(serializer, view: [nil], accept_html: true, **filter_opts)
|
31
|
+
before_action(**filter_opts) do
|
32
|
+
self.serializers = resolved_media_types(serializer, view: view) do |media_type, media_view, res|
|
33
|
+
opts = { media_type: media_type, media_view: media_view }
|
34
|
+
|
35
|
+
res[MEDIA_TYPE_HTML] = wrap_html(serializer, **opts) if accept_html && !res[MEDIA_TYPE_HTML]
|
36
|
+
res[String(media_type)] = wrap_media(serializer, **opts) if media_type != MEDIA_TYPE_HTML
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def accept_html(serializer, **filter_opts)
|
42
|
+
before_action(**filter_opts) do
|
43
|
+
self.serializers = resolved_media_types(serializer, view: nil) do |_, media_view, res|
|
44
|
+
res[MEDIA_TYPE_HTML] = wrap_html(serializer, media_view: media_view, media_type: MEDIA_TYPE_HTML)
|
45
|
+
break
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
##
|
51
|
+
# Register a mime type, but explicitly notify that it can't be serialized.
|
52
|
+
# This is done for file serving and redirects.
|
53
|
+
#
|
54
|
+
# @param [Symbol] mimes takes a list of symbols that should resolve through Mime::Type
|
55
|
+
#
|
56
|
+
# @see #freeze_accepted_media!
|
57
|
+
#
|
58
|
+
# @example fingerpint binary format
|
59
|
+
#
|
60
|
+
# no_serializer_for :fingerprint_bin, :fingerprint_deprecated_bin
|
61
|
+
#
|
62
|
+
def accept_without_serialization(*mimes, **filter_opts)
|
63
|
+
before_action(**filter_opts) do
|
64
|
+
self.serializers = Array(mimes).each_with_object(Hash(serializers)) do |mime, res|
|
65
|
+
res[(Mime::Type.lookup_by_extension(mime) || mime).to_s] = nil
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
##
|
71
|
+
# Freezes additions to the serializes and notifies the controller what it will be able to respond to.
|
72
|
+
#
|
73
|
+
def freeze_accepted_media!
|
74
|
+
before_action do
|
75
|
+
# If the responders gem is available, this freezes what a controller can respond to
|
76
|
+
if self.class.respond_to?(:respond_to)
|
77
|
+
self.class.respond_to(*Hash(serializers).keys.map { |type| Mime::Type.lookup(type) })
|
78
|
+
end
|
79
|
+
serializers.freeze
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
# rubocop:enable Metrics/BlockLength
|
84
|
+
|
85
|
+
included do
|
86
|
+
protected
|
87
|
+
|
88
|
+
attr_accessor :serializers
|
89
|
+
end
|
90
|
+
|
91
|
+
protected
|
92
|
+
|
93
|
+
def media_type_serializer
|
94
|
+
@media_type_serializer ||= resolve_media_type_serializer
|
95
|
+
end
|
96
|
+
|
97
|
+
def serialize_media(media, serializer: media_type_serializer)
|
98
|
+
@last_serialize_media = media
|
99
|
+
@last_media_serializer = serializer.call(media, context: self)
|
100
|
+
end
|
101
|
+
|
102
|
+
def media_type_json_root
|
103
|
+
String(request.format.symbol).sub(/_json$/, '')
|
104
|
+
end
|
105
|
+
|
106
|
+
def respond_to_matching(matcher, &block)
|
107
|
+
respond_to do |format|
|
108
|
+
serializers.each_key do |mime|
|
109
|
+
next unless matcher.call(mime: mime, format: format)
|
110
|
+
format.custom(mime, &block)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def respond_to_accept(&block)
|
116
|
+
respond_to do |format|
|
117
|
+
serializers.each_key do |mime|
|
118
|
+
format.custom(mime, &block)
|
119
|
+
end
|
120
|
+
|
121
|
+
format.any { raise_no_accept_serializer }
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def request_accept
|
126
|
+
@request_accept ||= HttpHeaders::Accept.new(request.get_header(HEADER_ACCEPT) || '')
|
127
|
+
end
|
128
|
+
|
129
|
+
def raise_no_accept_serializer
|
130
|
+
raise NoSerializerForContentType.new(request_accept, serializers.keys)
|
131
|
+
end
|
132
|
+
|
133
|
+
private
|
134
|
+
|
135
|
+
def extract_synonym_version(synonym)
|
136
|
+
synonym.rpartition('.').last[1..-1]
|
137
|
+
end
|
138
|
+
|
139
|
+
def resolve_media_type_serializer
|
140
|
+
raise NoMediaTypeSerializers unless serializers
|
141
|
+
|
142
|
+
# Rails negotiation
|
143
|
+
if serializers[request.format.to_s]
|
144
|
+
return serializers[request.format.to_s]
|
145
|
+
end
|
146
|
+
|
147
|
+
# Ruby negotiation
|
148
|
+
request.accepts.each do |mime_type|
|
149
|
+
next unless serializers.key?(mime_type.to_s)
|
150
|
+
# Override Rails selected format
|
151
|
+
request.set_header("action_dispatch.request.formats", [mime_type])
|
152
|
+
return serializers[mime_type.to_s]
|
153
|
+
end
|
154
|
+
|
155
|
+
raise_no_accept_serializer
|
156
|
+
end
|
157
|
+
|
158
|
+
def resolved_media_types(serializer, view:)
|
159
|
+
Array(view).each_with_object(Hash(serializers)) do |media_view, res|
|
160
|
+
media_view = String(media_view)
|
161
|
+
Array(serializer.media_type(view: media_view)).each do |media_type|
|
162
|
+
yield media_type, media_view, res
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def wrap_media(serializer, media_view:, media_type:)
|
168
|
+
lambda do |*args, **opts|
|
169
|
+
Wrapper::MediaWrapper.new(
|
170
|
+
serializer.new(*args, media_type: media_type, view: media_view, **opts),
|
171
|
+
view: media_view
|
172
|
+
)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def wrap_html(serializer, media_view:, media_type:)
|
177
|
+
lambda do |*args, **opts|
|
178
|
+
media_serializer = wrap_media(
|
179
|
+
serializer,
|
180
|
+
media_view: media_view,
|
181
|
+
media_type: media_type
|
182
|
+
).call(*args, **opts)
|
183
|
+
|
184
|
+
Wrapper::HtmlWrapper.new(
|
185
|
+
media_serializer,
|
186
|
+
view: media_view,
|
187
|
+
mime_type: media_type.to_s,
|
188
|
+
representations: serializers.keys,
|
189
|
+
url_context: request.original_fullpath.chomp(".#{request.format.symbol}")
|
190
|
+
)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
require 'media_types/serialization/mime_type_support'
|
6
|
+
require 'media_types/serialization/migrations_support'
|
7
|
+
|
8
|
+
module MediaTypes
|
9
|
+
module Serialization
|
10
|
+
class Base
|
11
|
+
include MimeTypeSupport
|
12
|
+
include MigrationsSupport
|
13
|
+
|
14
|
+
attr_accessor :serializable
|
15
|
+
|
16
|
+
def initialize(serializable, media_type:, view: nil, context:)
|
17
|
+
self.context = context
|
18
|
+
self.current_media_type = media_type
|
19
|
+
self.current_view = view
|
20
|
+
|
21
|
+
set(serializable)
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_link_header
|
25
|
+
{}
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_html
|
29
|
+
raise NotImplementedError, format(
|
30
|
+
'In %<class>s, to_html is not implemented.',
|
31
|
+
class: self.class.name
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_xml
|
36
|
+
raise NotImplementedError, format(
|
37
|
+
'In %<class>s, to_xml is not implemented.',
|
38
|
+
class: self.class.name
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_hash
|
43
|
+
raise NotImplementedError, format(
|
44
|
+
'In %<class>s, to_hash is not implemented.',
|
45
|
+
class: self.class.name
|
46
|
+
)
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_text
|
50
|
+
raise NotImplementedError, format(
|
51
|
+
'In %<class>s, to_text is not implemented/',
|
52
|
+
class: self.class.name
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
56
|
+
def to_json
|
57
|
+
raise NotImplementedError, format(
|
58
|
+
'In %<class>s, to_json is not implemented/',
|
59
|
+
class: self.class.name
|
60
|
+
)
|
61
|
+
end
|
62
|
+
|
63
|
+
def to_h
|
64
|
+
raise NotImplementedError, format(
|
65
|
+
'In %<class>s, to_h is not implemented. Missing alias to_h to_hash.',
|
66
|
+
class: self.class.name
|
67
|
+
)
|
68
|
+
end
|
69
|
+
|
70
|
+
def to_body
|
71
|
+
raise NotImplementedError, format(
|
72
|
+
'In %<class>s, to_body is not implemented. This is a general purpose catch all renderer',
|
73
|
+
class: self.class.name
|
74
|
+
)
|
75
|
+
end
|
76
|
+
|
77
|
+
def respond_to?(sym, include_all = false)
|
78
|
+
return false if [:to_h, :to_hash, :to_json, :to_text, :to_xml, :to_html, :to_body, :extract_self].include?(sym)
|
79
|
+
return true if sym == :to_link_header
|
80
|
+
|
81
|
+
super
|
82
|
+
end
|
83
|
+
|
84
|
+
protected
|
85
|
+
|
86
|
+
attr_accessor :context, :current_media_type, :current_view
|
87
|
+
|
88
|
+
def extract_self
|
89
|
+
raise NotImplementedError, format(
|
90
|
+
'In %<class>s, extract_self is not implemented, thus a self link for %<model>s can not be generated. ' \
|
91
|
+
'Implement extract_self on %<class>s or deny the MediaType[s] %<media_types>s for this request.',
|
92
|
+
class: self.class.name,
|
93
|
+
model: serializable.class.name,
|
94
|
+
media_types: self.class.media_types(view: '[view]').to_s
|
95
|
+
)
|
96
|
+
end
|
97
|
+
|
98
|
+
def extract_links
|
99
|
+
{}
|
100
|
+
end
|
101
|
+
|
102
|
+
def header_links
|
103
|
+
extract_links
|
104
|
+
end
|
105
|
+
|
106
|
+
def set(serializable)
|
107
|
+
self.serializable = serializable
|
108
|
+
self
|
109
|
+
end
|
110
|
+
|
111
|
+
def extract(extractable, *keys)
|
112
|
+
return {} unless keys.present?
|
113
|
+
extractable.slice(*keys)
|
114
|
+
rescue TypeError => err
|
115
|
+
raise TypeError, format(
|
116
|
+
'[serializer] failed to slice keys to extract. Given keys: %<keys>s. Extractable: %<extractable>s' \
|
117
|
+
'Error: %<error>s',
|
118
|
+
keys: keys,
|
119
|
+
extractable: extractable,
|
120
|
+
error: err
|
121
|
+
)
|
122
|
+
end
|
123
|
+
|
124
|
+
def resolve_file_url(url)
|
125
|
+
return url if !url || URI(url).absolute?
|
126
|
+
|
127
|
+
format(
|
128
|
+
'https://%<host>s:%<port>s%<path>s',
|
129
|
+
host: context.default_url_options[:host],
|
130
|
+
port: context.default_url_options[:port],
|
131
|
+
path: url
|
132
|
+
)
|
133
|
+
rescue URI::InvalidURIError
|
134
|
+
url
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
#
|
3
|
+
module MediaTypes
|
4
|
+
module Serialization
|
5
|
+
class MigrationsCommand
|
6
|
+
def initialize(serializer)
|
7
|
+
self.serializer = serializer
|
8
|
+
self.migrations = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(result, mime_type, view)
|
12
|
+
return result if matches_current_mime_type?(view: view, mime_type: mime_type)
|
13
|
+
|
14
|
+
migrations.reduce(result) do |migrated, (version, migration)|
|
15
|
+
migrated = migration.call(migrated)
|
16
|
+
next migrated unless matches_mime_type?(mime_type.version(version), mime_type)
|
17
|
+
break migrated
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
attr_accessor :serializer, :migrations
|
24
|
+
|
25
|
+
def version(version, &block)
|
26
|
+
migrations[version] = ->(result) { serializer.instance_exec(result, &block) }
|
27
|
+
end
|
28
|
+
|
29
|
+
def matches_current_mime_type?(view:, mime_type:)
|
30
|
+
serializer.class.current_mime_type(view: view) == mime_type.to_s
|
31
|
+
end
|
32
|
+
|
33
|
+
def matches_mime_type?(left, right)
|
34
|
+
left.to_s == right.to_s
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/concern'
|
4
|
+
require 'media_types/serialization/migrations_command'
|
5
|
+
|
6
|
+
module MediaTypes
|
7
|
+
module Serialization
|
8
|
+
module MigrationsSupport
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
|
11
|
+
included do
|
12
|
+
mattr_accessor :migrations
|
13
|
+
end
|
14
|
+
|
15
|
+
class_methods do
|
16
|
+
def migrator(serializer)
|
17
|
+
return nil unless migrations
|
18
|
+
migrations.call(serializer)
|
19
|
+
end
|
20
|
+
|
21
|
+
protected
|
22
|
+
|
23
|
+
def backward_migrations(&block)
|
24
|
+
self.migrations = lambda do |serializer|
|
25
|
+
MigrationsCommand.new(serializer).tap do |callable|
|
26
|
+
callable.instance_exec(&block)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def migrate(result = nil, media_type = current_media_type, view = current_view)
|
33
|
+
result ||= yield
|
34
|
+
|
35
|
+
migrator = self.class.migrator(self)
|
36
|
+
return result unless migrator
|
37
|
+
migrator.call(result, media_type, view)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|