Skip to content

Commit 5b0a4c3

Browse files
committed
Encapsulate serialization in ActiveModel::Serializer::Builder
Usage: ActiveModel::Serializer.build(resource, options)
1 parent 460150f commit 5b0a4c3

File tree

7 files changed

+125
-41
lines changed

7 files changed

+125
-41
lines changed

lib/action_controller/serialization.rb

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

77
include ActionController::Renderers
88

9-
ADAPTER_OPTION_KEYS = [:include, :fields, :root, :adapter]
9+
ADAPTER_OPTION_KEYS = ActiveModel::Serializer::Builder::ADAPTER_OPTION_KEYS
1010

1111
included do
1212
class_attribute :_serialization_scope
@@ -18,46 +18,46 @@ def serialization_scope
1818
respond_to?(_serialization_scope, true)
1919
end
2020

21+
# Deprecated
2122
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)
27-
end
28-
29-
@_serializer
23+
fail ActiveModel::Serializer::Error, "ActionController::Serialization##{__method__} has been removed. "\
24+
"Please use ActiveSupport::Serializer#build instead"
3025
end
3126

27+
# Deprecated
3228
def use_adapter?
33-
!(@_adapter_opts.key?(:adapter) && !@_adapter_opts[:adapter])
29+
fail ActiveModel::Serializer::Error, "ActionController::Serialization##{__method__} has been removed. "\
30+
"Please use ActiveSupport::Serializer#build instead"
3431
end
3532

3633
[:_render_option_json, :_render_with_renderer_json].each do |renderer_method|
3734
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-
43-
@_serializer_opts[:scope] ||= serialization_scope
44-
@_serializer_opts[:scope_name] = _serialization_scope
45-
46-
# omg hax
47-
object = serializer.new(resource, @_serializer_opts)
48-
adapter = ActiveModel::Serializer::Adapter.create(object, @_adapter_opts)
35+
builder = ActiveModel::Serializer::Builder.new(resource, options)
36+
if builder.serializer?
37+
builder.serialization_scope ||= serialization_scope
38+
builder.serialization_scope_name = _serialization_scope
39+
adapter = builder.adapter
4940
super(adapter, options)
5041
else
5142
super(resource, options)
5243
end
5344
end
5445
end
5546

47+
# Tries to rescue the exception by looking up and calling a registered handler.
48+
# TODO: Either Decorate 'exception' and define #handle_error where it is serialized
49+
# For example:
50+
# class ExceptionModel
51+
# include ActiveModel::Serialization
52+
# def initialize(exception)
53+
# # etc
54+
# end
55+
# def handle_error(exception)
56+
# exception_model = ActiveModel::Serializer.build_exception_model({ errors: ['Internal Server Error'] })
57+
# render json: exception_model, status: :internal_server_error
58+
# end
59+
# OR remove method as it doesn't do anything right now.
5660
def rescue_with_handler(exception)
57-
@_serializer = nil
58-
@_serializer_opts = nil
59-
@_adapter_opts = nil
60-
6161
super(exception)
6262
end
6363

lib/active_model/serializer.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
module ActiveModel
44
class Serializer
5+
Error = Class.new(StandardError)
56
extend ActiveSupport::Autoload
67
autoload :Configuration
78
autoload :ArraySerializer
89
autoload :Adapter
10+
autoload :Builder
911
include Configuration
1012

1113
class << self
@@ -28,6 +30,13 @@ def self.inherited(base)
2830
base._urls = []
2931
end
3032

33+
# Primary interface to building a serializer (with adapter)
34+
def self.build(resource, options = {})
35+
builder = Builder.new(resource, options)
36+
yield builder if block_given?
37+
builder.adapter
38+
end
39+
3140
def self.attributes(*attrs)
3241
attrs = attrs.first if attrs.first.class == Array
3342
@_attributes.concat attrs
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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, :root, :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+
def serialization_scope=(scope)
16+
serializer_opts[:scope] = scope
17+
end
18+
19+
def serialization_scope
20+
serializer_opts[:scope]
21+
end
22+
23+
def serialization_scope_name=(scope_name)
24+
serializer_opts[:scope_name] = scope_name
25+
end
26+
27+
def adapter
28+
@adapter ||= ActiveModel::Serializer::Adapter.create(serializer_instance, adapter_opts)
29+
end
30+
alias_method :adapter_instance, :adapter
31+
32+
def serializer_instance
33+
@serializer_instance ||= serializer.new(resource, serializer_opts)
34+
end
35+
36+
# Get serializer either explicitly :serializer or implicitly from resource
37+
# Remove :serializer key from serializer_opts
38+
# Replace :serializer key with :each_serializer if present
39+
def serializer
40+
@serializer ||=
41+
begin
42+
@serializer = serializer_opts.delete(:serializer)
43+
@serializer ||= ActiveModel::Serializer.serializer_for(resource)
44+
45+
if serializer_opts.key?(:each_serializer)
46+
serializer_opts[:serializer] = serializer_opts.delete(:each_serializer)
47+
end
48+
@serializer
49+
end
50+
end
51+
alias_method :serializer_class, :serializer
52+
53+
# True when no explicit adapter given, or explicit appear is truthy (non-nil)
54+
# False when explicit adapter is falsy (nil or false)
55+
def use_adapter?
56+
!(adapter_opts.key?(:adapter) && !adapter_opts[:adapter])
57+
end
58+
59+
def serializer?
60+
use_adapter? && !!(serializer)
61+
end
62+
63+
private
64+
65+
attr_reader :resource, :adapter_opts, :serializer_opts
66+
67+
end
68+
end
69+
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 MyController < 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: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,10 +136,7 @@ def render_fragment_changed_object_with_relationship
136136

137137
private
138138
def generate_cached_serializer(obj)
139-
serializer_class = ActiveModel::Serializer.serializer_for(obj)
140-
serializer = serializer_class.new(obj)
141-
adapter = ActiveModel::Serializer.adapter.new(serializer)
142-
adapter.to_json
139+
ActiveModel::Serializer.build(obj).to_json
143140
end
144141

145142
def with_adapter(adapter)
@@ -405,6 +402,20 @@ def test_cache_expiration_on_update
405402
assert_equal 'application/json', @response.content_type
406403
assert_equal expected.to_json, @response.body
407404
end
405+
406+
def test_fail_calling_use_adapter_on_controller_instance
407+
controller = MyController.new
408+
assert_raises(ActiveModel::Serializer::Error) {
409+
controller.use_adapter?
410+
}
411+
end
412+
413+
def test_fail_calling_get_serializer__on_controller_instance
414+
controller = MyController.new
415+
assert_raises(ActiveModel::Serializer::Error) {
416+
controller.get_serializer(Class.new)
417+
}
418+
end
408419
end
409420
end
410421
end

test/serializers/cache_test.rb

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,7 @@ def test_fragment_fetch_with_virtual_associations
117117

118118
private
119119
def render_object_with_cache(obj)
120-
serializer_class = ActiveModel::Serializer.serializer_for(obj)
121-
serializer = serializer_class.new(obj)
122-
adapter = ActiveModel::Serializer.adapter.new(serializer)
123-
adapter.serializable_hash
120+
ActiveModel::Serializer.build(obj).serializable_hash
124121
end
125122
end
126123
end

test/serializers/meta_test.rb

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,9 @@ def test_meta_is_present_on_arrays_with_root
9494
private
9595

9696
def load_adapter(options)
97-
adapter_opts, serializer_opts =
98-
options.partition { |k, _| ActionController::Serialization::ADAPTER_OPTION_KEYS.include? k }.map { |h| Hash[h] }
99-
100-
serializer = AlternateBlogSerializer.new(@blog, serializer_opts)
101-
ActiveModel::Serializer::Adapter::Json.new(serializer, adapter_opts)
97+
options = options.merge(adapter: :json, serializer: AlternateBlogSerializer)
98+
builder = ActiveModel::Serializer::Builder.new(@blog, options)
99+
builder.adapter
102100
end
103101
end
104102
end

0 commit comments

Comments
 (0)