Skip to content

Commit b781265

Browse files
committed
Merge pull request #954 from bf4/encapsulate_serialization
Encapsulate serialization in ActiveModel::SerializableResource
2 parents 7bc1666 + df14029 commit b781265

File tree

9 files changed

+210
-79
lines changed

9 files changed

+210
-79
lines changed

lib/action_controller/serialization.rb

Lines changed: 24 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ module Serialization
66

77
include ActionController::Renderers
88

9-
ADAPTER_OPTION_KEYS = [:include, :fields, :adapter]
9+
# Deprecated
10+
ADAPTER_OPTION_KEYS = ActiveModel::SerializableResource::ADAPTER_OPTION_KEYS
1011

1112
included do
1213
class_attribute :_serialization_scope
@@ -18,48 +19,37 @@ def serialization_scope
1819
respond_to?(_serialization_scope, true)
1920
end
2021

21-
def get_serializer(resource)
22-
@_serializer ||= @_serializer_opts.delete(:serializer)
23-
@_serializer ||= ActiveModel::Serializer.serializer_for(resource)
24-
25-
if @_serializer_opts.key?(:each_serializer)
26-
@_serializer_opts[:serializer] = @_serializer_opts.delete(:each_serializer)
22+
def get_serializer(resource, options = {})
23+
if ! use_adapter?
24+
warn "ActionController::Serialization#use_adapter? has been removed. "\
25+
"Please pass 'adapter: false' or see ActiveSupport::SerializableResource#serialize"
26+
options[:adapter] = false
2727
end
28-
29-
@_serializer
30-
end
31-
32-
def use_adapter?
33-
!(@_adapter_opts.key?(:adapter) && !@_adapter_opts[:adapter])
34-
end
35-
36-
[:_render_option_json, :_render_with_renderer_json].each do |renderer_method|
37-
define_method renderer_method do |resource, options|
38-
@_adapter_opts, @_serializer_opts =
39-
options.partition { |k, _| ADAPTER_OPTION_KEYS.include? k }.map { |h| Hash[h] }
40-
41-
if use_adapter? && (serializer = get_serializer(resource))
42-
@_serializer_opts[:scope] ||= serialization_scope
43-
@_serializer_opts[:scope_name] = _serialization_scope
44-
28+
ActiveModel::SerializableResource.serialize(resource, options) do |serializable_resource|
29+
if serializable_resource.serializer?
30+
serializable_resource.serialization_scope ||= serialization_scope
31+
serializable_resource.serialization_scope_name = _serialization_scope
4532
begin
46-
serialized = serializer.new(resource, @_serializer_opts)
33+
serializable_resource.adapter
4734
rescue ActiveModel::Serializer::ArraySerializer::NoSerializerError
48-
else
49-
resource = ActiveModel::Serializer::Adapter.create(serialized, @_adapter_opts)
35+
resource
5036
end
37+
else
38+
resource
5139
end
52-
53-
super(resource, options)
5440
end
5541
end
5642

57-
def rescue_with_handler(exception)
58-
@_serializer = nil
59-
@_serializer_opts = nil
60-
@_adapter_opts = nil
43+
# Deprecated
44+
def use_adapter?
45+
true
46+
end
6147

62-
super(exception)
48+
[:_render_option_json, :_render_with_renderer_json].each do |renderer_method|
49+
define_method renderer_method do |resource, options|
50+
serializable_resource = get_serializer(resource, options)
51+
super(serializable_resource, options)
52+
end
6353
end
6454

6555
module ClassMethods
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
require "set"
2+
module ActiveModel
3+
class SerializableResource
4+
5+
ADAPTER_OPTION_KEYS = Set.new([:include, :fields, :adapter])
6+
7+
def initialize(resource, options = {})
8+
@resource = resource
9+
@adapter_opts, @serializer_opts =
10+
options.partition { |k, _| ADAPTER_OPTION_KEYS.include? k }.map { |h| Hash[h] }
11+
end
12+
13+
delegate :serializable_hash, :as_json, :to_json, to: :adapter
14+
15+
# Primary interface to building a serializer (with adapter)
16+
# If no block is given,
17+
# returns the serializable_resource, ready for #as_json/#to_json/#serializable_hash.
18+
# Otherwise, yields the serializable_resource and
19+
# returns the contents of the block
20+
def self.serialize(resource, options = {})
21+
serializable_resource = SerializableResource.new(resource, options)
22+
if block_given?
23+
yield serializable_resource
24+
else
25+
serializable_resource
26+
end
27+
end
28+
29+
def serialization_scope=(scope)
30+
serializer_opts[:scope] = scope
31+
end
32+
33+
def serialization_scope
34+
serializer_opts[:scope]
35+
end
36+
37+
def serialization_scope_name=(scope_name)
38+
serializer_opts[:scope_name] = scope_name
39+
end
40+
41+
def adapter
42+
@adapter ||= ActiveModel::Serializer::Adapter.create(serializer_instance, adapter_opts)
43+
end
44+
alias_method :adapter_instance, :adapter
45+
46+
def serializer_instance
47+
@serializer_instance ||= serializer.new(resource, serializer_opts)
48+
end
49+
50+
# Get serializer either explicitly :serializer or implicitly from resource
51+
# Remove :serializer key from serializer_opts
52+
# Replace :serializer key with :each_serializer if present
53+
def serializer
54+
@serializer ||=
55+
begin
56+
@serializer = serializer_opts.delete(:serializer)
57+
@serializer ||= ActiveModel::Serializer.serializer_for(resource)
58+
59+
if serializer_opts.key?(:each_serializer)
60+
serializer_opts[:serializer] = serializer_opts.delete(:each_serializer)
61+
end
62+
@serializer
63+
end
64+
end
65+
alias_method :serializer_class, :serializer
66+
67+
# True when no explicit adapter given, or explicit appear is truthy (non-nil)
68+
# False when explicit adapter is falsy (nil or false)
69+
def use_adapter?
70+
!(adapter_opts.key?(:adapter) && !adapter_opts[:adapter])
71+
end
72+
73+
def serializer?
74+
use_adapter? && !!(serializer)
75+
end
76+
77+
private
78+
79+
attr_reader :resource, :adapter_opts, :serializer_opts
80+
81+
end
82+
end

lib/active_model_serializers.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
require 'active_model/serializer'
44
require 'active_model/serializer/fieldset'
55
require 'active_model/serializer/railtie'
6+
require 'active_model/serializable_resource'
67

78
begin
89
require 'action_controller'

test/action_controller/rescue_from_test.rb

Lines changed: 0 additions & 32 deletions
This file was deleted.

test/action_controller/serialization_test.rb

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
module ActionController
55
module Serialization
66
class ImplicitSerializerTest < ActionController::TestCase
7+
include ActiveSupport::Testing::Stream
78
class ImplicitSerializationTestController < ActionController::Base
89
def render_using_implicit_serializer
910
@profile = Profile.new({ name: 'Name 1', description: 'Description 1', comments: 'Comments 1' })
@@ -140,10 +141,7 @@ def render_fragment_changed_object_with_relationship
140141

141142
private
142143
def generate_cached_serializer(obj)
143-
serializer_class = ActiveModel::Serializer.serializer_for(obj)
144-
serializer = serializer_class.new(obj)
145-
adapter = ActiveModel::Serializer.adapter.new(serializer)
146-
adapter.to_json
144+
ActiveModel::SerializableResource.new(obj).to_json
147145
end
148146

149147
def with_adapter(adapter)
@@ -400,6 +398,28 @@ def test_cache_expiration_on_update
400398
assert_equal 'application/json', @response.content_type
401399
assert_equal expected.to_json, @response.body
402400
end
401+
402+
def test_warn_overridding_use_adapter_as_falsy_on_controller_instance
403+
controller = Class.new(ImplicitSerializationTestController) {
404+
def use_adapter?
405+
false
406+
end
407+
}.new
408+
assert_match /adapter: false/, (capture(:stderr) {
409+
controller.get_serializer(@profile)
410+
})
411+
end
412+
413+
def test_dont_warn_overridding_use_adapter_as_truthy_on_controller_instance
414+
controller = Class.new(ImplicitSerializationTestController) {
415+
def use_adapter?
416+
true
417+
end
418+
}.new
419+
assert_equal "", (capture(:stderr) {
420+
controller.get_serializer(@profile)
421+
})
422+
end
403423
end
404424
end
405425
end

test/serializable_resource_test.rb

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
require 'test_helper'
2+
3+
module ActiveModel
4+
class SerializableResourceTest < Minitest::Test
5+
def setup
6+
@resource = Profile.new({ name: 'Name 1', description: 'Description 1', comments: 'Comments 1' })
7+
@serializer = ProfileSerializer.new(@resource)
8+
@adapter = ActiveModel::Serializer::Adapter.create(@serializer)
9+
@serializable_resource = ActiveModel::SerializableResource.new(@resource)
10+
end
11+
12+
def test_serializable_resource_delegates_serializable_hash_to_the_adapter
13+
options = nil
14+
assert_equal @adapter.serializable_hash(options), @serializable_resource.serializable_hash(options)
15+
end
16+
17+
def test_serializable_resource_delegates_to_json_to_the_adapter
18+
options = nil
19+
assert_equal @adapter.to_json(options), @serializable_resource.to_json(options)
20+
end
21+
22+
def test_serializable_resource_delegates_as_json_to_the_adapter
23+
options = nil
24+
assert_equal @adapter.as_json(options), @serializable_resource.as_json(options)
25+
end
26+
end
27+
end

test/serializers/cache_test.rb

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,7 @@ def _cache_digest_definition
131131

132132
private
133133
def render_object_with_cache(obj)
134-
serializer_class = ActiveModel::Serializer.serializer_for(obj)
135-
serializer = serializer_class.new(obj)
136-
adapter = ActiveModel::Serializer.adapter.new(serializer)
137-
adapter.serializable_hash
134+
ActiveModel::SerializableResource.new(obj).serializable_hash
138135
end
139136
end
140137
end

test/serializers/meta_test.rb

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,11 +113,8 @@ def test_meta_is_present_on_arrays_with_root
113113
private
114114

115115
def load_adapter(options)
116-
adapter_opts, serializer_opts =
117-
options.partition { |k, _| ActionController::Serialization::ADAPTER_OPTION_KEYS.include? k }.map { |h| Hash[h] }
118-
119-
serializer = AlternateBlogSerializer.new(@blog, serializer_opts)
120-
ActiveModel::Serializer::Adapter::FlattenJson.new(serializer, adapter_opts)
116+
options = options.merge(adapter: :flatten_json, serializer: AlternateBlogSerializer)
117+
ActiveModel::SerializableResource.new(@blog, options)
121118
end
122119
end
123120
end

test/test_helper.rb

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,55 @@
1919
end
2020
require 'active_model_serializers'
2121

22+
# Use cleaner stream testing interface from Rails 5 if available
23+
# see https://github.com/rails/rails/blob/29959eb59d/activesupport/lib/active_support/testing/stream.rb
24+
begin
25+
require "active_support/testing/stream"
26+
rescue LoadError
27+
module ActiveSupport
28+
module Testing
29+
module Stream #:nodoc:
30+
private
31+
32+
def silence_stream(stream)
33+
old_stream = stream.dup
34+
stream.reopen(IO::NULL)
35+
stream.sync = true
36+
yield
37+
ensure
38+
stream.reopen(old_stream)
39+
old_stream.close
40+
end
41+
42+
def quietly
43+
silence_stream(STDOUT) do
44+
silence_stream(STDERR) do
45+
yield
46+
end
47+
end
48+
end
49+
50+
def capture(stream)
51+
stream = stream.to_s
52+
captured_stream = Tempfile.new(stream)
53+
stream_io = eval("$#{stream}")
54+
origin_stream = stream_io.dup
55+
stream_io.reopen(captured_stream)
56+
57+
yield
58+
59+
stream_io.rewind
60+
return captured_stream.read
61+
ensure
62+
captured_stream.close
63+
captured_stream.unlink
64+
stream_io.reopen(origin_stream)
65+
end
66+
end
67+
end
68+
end
69+
end
70+
2271
class Foo < Rails::Application
2372
if Rails::VERSION::MAJOR >= 4
2473
config.eager_load = false

0 commit comments

Comments
 (0)