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