Skip to content

Commit b7b49eb

Browse files
committed
Encapsulate serialization in ActiveModel::Serializer::Builder
Usage: ActiveModel::Serializer.build(resource, options)
1 parent 059409b commit b7b49eb

File tree

9 files changed

+231
-46
lines changed

9 files changed

+231
-46
lines changed

lib/action_controller/serialization.rb

Lines changed: 39 additions & 30 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::Serializer::Builder::ADAPTER_OPTION_KEYS
1011

1112
included do
1213
class_attribute :_serialization_scope
@@ -18,47 +19,55 @@ 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::Serializer.build"
26+
options[:adapter] = false
27+
end
28+
serializable_resource = ActiveModel::Serializer::build(resource, options) do |builder|
29+
if builder.serializer?
30+
builder.serialization_scope ||= serialization_scope
31+
builder.serialization_scope_name = _serialization_scope
32+
begin
33+
builder.adapter
34+
rescue ActiveModel::Serializer::ArraySerializer::NoSerializerError
35+
resource
36+
end
37+
else
38+
resource
39+
end
2740
end
28-
29-
@_serializer
3041
end
3142

43+
# Deprecated
3244
def use_adapter?
33-
!(@_adapter_opts.key?(:adapter) && !@_adapter_opts[:adapter])
45+
true
3446
end
3547

3648
[:_render_option_json, :_render_with_renderer_json].each do |renderer_method|
3749
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-
45-
begin
46-
serialized = serializer.new(resource, @_serializer_opts)
47-
rescue ActiveModel::Serializer::ArraySerializer::NoSerializerError
48-
else
49-
resource = ActiveModel::Serializer::Adapter.create(serialized, @_adapter_opts)
50-
end
51-
end
52-
53-
super(resource, options)
50+
serializable_resource = get_serializer(resource, options)
51+
super(serializable_resource, options)
5452
end
5553
end
5654

55+
# Tries to rescue the exception by looking up and calling a registered handler.
56+
#
57+
# Possibly Deprecated
58+
# TODO: Either Decorate 'exception' and define #handle_error where it is serialized
59+
# For example:
60+
# class ExceptionModel
61+
# include ActiveModel::Serialization
62+
# def initialize(exception)
63+
# # etc
64+
# end
65+
# def handle_error(exception)
66+
# exception_model = ActiveModel::Serializer.build_exception_model({ errors: ['Internal Server Error'] })
67+
# render json: exception_model, status: :internal_server_error
68+
# end
69+
# OR remove method as it doesn't do anything right now.
5770
def rescue_with_handler(exception)
58-
@_serializer = nil
59-
@_serializer_opts = nil
60-
@_adapter_opts = nil
61-
6271
super(exception)
6372
end
6473

lib/active_model/serializer.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ class Serializer
66
autoload :Configuration
77
autoload :ArraySerializer
88
autoload :Adapter
9+
autoload :Builder
910
include Configuration
1011

1112
class << self
@@ -31,6 +32,18 @@ def self.inherited(base)
3132
base._cache_digest = Digest::MD5.hexdigest(serializer_file.read)
3233
end
3334

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

test/action_controller/rescue_from_test.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ class RescueFromTestController < ActionController::Base
77
rescue_from Exception, with: :handle_error
88

99
def render_using_raise_error_serializer
10-
@profile = Profile.new({ name: 'Name 1', description: 'Description 1', comments: 'Comments 1' })
11-
render json: [@profile], serializer: RaiseErrorSerializer
10+
profile = Profile.new({ name: 'Name 1', description: 'Description 1', comments: 'Comments 1' })
11+
render json: [profile], serializer: RaiseErrorSerializer
1212
end
1313

1414
def handle_error(exception)
@@ -25,7 +25,7 @@ def test_rescue_from
2525
errors: ['Internal Server Error']
2626
}.to_json
2727

28-
assert_equal expected, @response.body
28+
assert_equal expected, response.body
2929
end
3030
end
3131
end

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' })
@@ -120,10 +121,7 @@ def render_fragment_changed_object_with_relationship
120121

121122
private
122123
def generate_cached_serializer(obj)
123-
serializer_class = ActiveModel::Serializer.serializer_for(obj)
124-
serializer = serializer_class.new(obj)
125-
adapter = ActiveModel::Serializer.adapter.new(serializer)
126-
adapter.to_json
124+
ActiveModel::Serializer.build(obj).to_json
127125
end
128126

129127
def with_adapter(adapter)
@@ -356,6 +354,28 @@ def test_cache_expiration_on_update
356354
assert_equal 'application/json', @response.content_type
357355
assert_equal expected.to_json, @response.body
358356
end
357+
358+
def test_warn_overridding_use_adapter_as_falsy_on_controller_instance
359+
controller = Class.new(ImplicitSerializationTestController) {
360+
def use_adapter?
361+
false
362+
end
363+
}.new
364+
assert_match /adapter: false/, (capture(:stderr) {
365+
controller.get_serializer(@profile)
366+
})
367+
end
368+
369+
def test_dont_warn_overridding_use_adapter_as_truthy_on_controller_instance
370+
controller = Class.new(ImplicitSerializationTestController) {
371+
def use_adapter?
372+
true
373+
end
374+
}.new
375+
assert_equal "", (capture(:stderr) {
376+
controller.get_serializer(@profile)
377+
})
378+
end
359379
end
360380
end
361381
end

test/serializer_builder_test.rb

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

test/serializers/cache_test.rb

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

128128
private
129129
def render_object_with_cache(obj)
130-
serializer_class = ActiveModel::Serializer.serializer_for(obj)
131-
serializer = serializer_class.new(obj)
132-
adapter = ActiveModel::Serializer.adapter.new(serializer)
133-
adapter.serializable_hash
130+
ActiveModel::Serializer.build(obj).serializable_hash
134131
end
135132
end
136133
end

test/serializers/meta_test.rb

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,8 @@ def test_meta_is_present_on_arrays_with_root
9999
private
100100

101101
def load_adapter(options)
102-
adapter_opts, serializer_opts =
103-
options.partition { |k, _| ActionController::Serialization::ADAPTER_OPTION_KEYS.include? k }.map { |h| Hash[h] }
104-
105-
serializer = AlternateBlogSerializer.new(@blog, serializer_opts)
106-
ActiveModel::Serializer::Adapter::FlattenJson.new(serializer, adapter_opts)
102+
options = options.merge(adapter: :flatten_json, serializer: AlternateBlogSerializer)
103+
ActiveModel::Serializer.build(@blog, options)
107104
end
108105
end
109106
end

test/test_helper.rb

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,55 @@
1313

1414
require 'active_model_serializers'
1515

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

0 commit comments

Comments
 (0)