Skip to content

Commit 8da44d7

Browse files
committed
Make Adapters registerable so they are not namespace-constrained
Changes: - Introduce Adapter::get for use by Serializer.adapter - Move Adapter-finding logic from Adapter::adapter_class into Adapter::get Introduced interfaces: - non-inherited methods ```ruby ActiveModel::Serializer::Adapter.adapter_map # a Hash<adapter_name, adapter_class> ActiveModel::Serializer::Adapter.adapters # an Array<adapter_name> ActiveModel::Serializer::Adapter.register(name, klass) # adds an adapter to the adapter_map ActiveModel::Serializer::Adapter.get(name_or_klass) # raises Argument error when adapter not found ``` - Automatically register adapters when subclassing ```ruby def self.inherited(subclass) ActiveModel::Serializer::Adapter.register(subclass.to_s.demodulize, subclass) end ``` - Preserves subclass method `::adapter_class(adapter)` ```ruby def self.adapter_class(adapter) ActiveModel::Serializer::Adapter.get(adapter) end ``` - Serializer.adapter now uses `Adapter.get(config.adapter)` rather than have duplicate logic
1 parent fc7b9c3 commit 8da44d7

File tree

8 files changed

+182
-27
lines changed

8 files changed

+182
-27
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
.config
55
.yardoc
66
Gemfile.lock
7+
Gemfile.local
78
InstalledFiles
89
_yardoc
910
coverage

Gemfile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
source 'https://rubygems.org'
2+
#
3+
# Add a Gemfile.local to locally bundle gems outside of version control
4+
local_gemfile = File.join(File.expand_path("..", __FILE__), "Gemfile.local")
5+
if File.readable?(local_gemfile)
6+
eval_gemfile local_gemfile
7+
end
28

39
# Specify your gem's dependencies in active_model_serializers.gemspec
410
gemspec

docs/general/adapters.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,7 @@ If you want to have a root key in your responses you should use the Json adapter
4949
```ruby
5050
ActiveModel::Serializer.config.adapter = :json
5151
```
52+
53+
## Registering an adapter
54+
55+
ActiveModel::Serializer::Adapter.register(:my_adapter, MyAdapter)

lib/active_model/serializer.rb

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -89,19 +89,9 @@ def self.serializer_for(resource, options = {})
8989
end
9090
end
9191

92+
# @see ActiveModel::Serializer::Adapter.get
9293
def self.adapter
93-
adapter_class = case config.adapter
94-
when Symbol
95-
ActiveModel::Serializer::Adapter.adapter_class(config.adapter)
96-
when Class
97-
config.adapter
98-
end
99-
unless adapter_class
100-
valid_adapters = Adapter.constants.map { |klass| ":#{klass.to_s.downcase}" }
101-
raise ArgumentError, "Unknown adapter: #{config.adapter}. Valid adapters are: #{valid_adapters}"
102-
end
103-
104-
adapter_class
94+
ActiveModel::Serializer::Adapter.get(config.adapter)
10595
end
10696

10797
def self.root_name

lib/active_model/serializer/adapter.rb

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
module ActiveModel
44
class Serializer
55
class Adapter
6+
UnknownAdapterError = Class.new(ArgumentError)
7+
ADAPTER_MAP = {}
68
extend ActiveSupport::Autoload
79
autoload :Json
810
autoload :FlattenJson
@@ -11,6 +13,64 @@ class Adapter
1113

1214
attr_reader :serializer
1315

16+
# Only the Adapter class has these methods.
17+
# None of the sublasses have them.
18+
class << ActiveModel::Serializer::Adapter
19+
# @return Hash<adapter_name, adapter_class>
20+
def adapter_map
21+
ADAPTER_MAP
22+
end
23+
24+
# @return Array<adapter_name>
25+
def adapters
26+
adapter_map.keys.sort
27+
end
28+
29+
# Adds an adapter 'klass' with 'name' to the 'adapter_map'
30+
# Names are stringified and underscored
31+
def register(name, klass)
32+
adapter_map.update(name.to_s.underscore => klass)
33+
self
34+
end
35+
36+
# @param adapter [String, Symbol, Class] name to fetch adapter by
37+
# @return [ActiveModel::Serializer::Adapter] subclass of Adapter
38+
# @raise [UnknownAdapterError]
39+
def get(adapter)
40+
# 1. return if is a class
41+
return adapter if adapter.is_a?(Class)
42+
adapter_name = adapter.to_s.underscore
43+
# 2. return if registered
44+
adapter_map.fetch(adapter_name) {
45+
# 3. try to find adapter class from environment
46+
adapter_class = find_by_name(adapter_name)
47+
register(adapter_name, adapter_class)
48+
adapter_class
49+
}
50+
rescue ArgumentError
51+
failure_message =
52+
"Unknown adapter: #{adapter.inspect}. Valid adapters are: #{adapters}"
53+
raise UnknownAdapterError, failure_message, $!.backtrace
54+
rescue NameError
55+
failure_message =
56+
"NameError: #{$!.message}. Unknown adapter: #{adapter.inspect}. Valid adapters are: #{adapters}"
57+
raise UnknownAdapterError, failure_message, $!.backtrace
58+
end
59+
60+
# @api private
61+
def find_by_name(adapter_name)
62+
adapter_name = adapter_name.to_s.classify.tr("API", "Api")
63+
"ActiveModel::Serializer::Adapter::#{adapter_name}".safe_constantize or
64+
fail UnknownAdapterError
65+
end
66+
private :find_by_name
67+
end
68+
69+
# Automatically register adapters when subclassing
70+
def self.inherited(subclass)
71+
ActiveModel::Serializer::Adapter.register(subclass.to_s.demodulize, subclass)
72+
end
73+
1474
def initialize(serializer, options = {})
1575
@serializer = serializer
1676
@options = options
@@ -32,9 +92,9 @@ def self.create(resource, options = {})
3292
klass.new(resource, options)
3393
end
3494

95+
# @see ActiveModel::Serializer::Adapter.get
3596
def self.adapter_class(adapter)
36-
adapter_name = adapter.to_s.classify.sub("API", "Api")
37-
"ActiveModel::Serializer::Adapter::#{adapter_name}".safe_constantize
97+
ActiveModel::Serializer::Adapter.get(adapter)
3898
end
3999

40100
def fragment_cache(*args)

test/adapter_test.rb

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,6 @@ def test_serializer
1919
assert_equal @serializer, @adapter.serializer
2020
end
2121

22-
def test_adapter_class_for_known_adapter
23-
klass = ActiveModel::Serializer::Adapter.adapter_class(:json_api)
24-
assert_equal ActiveModel::Serializer::Adapter::JsonApi, klass
25-
end
26-
27-
def test_adapter_class_for_unknown_adapter
28-
klass = ActiveModel::Serializer::Adapter.adapter_class(:json_simple)
29-
assert_nil klass
30-
end
31-
3222
def test_create_adapter
3323
adapter = ActiveModel::Serializer::Adapter.create(@serializer)
3424
assert_equal ActiveModel::Serializer::Adapter::FlattenJson, adapter.class

test/serializers/adapter_for_test.rb

Lines changed: 101 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
module ActiveModel
22
class Serializer
33
class AdapterForTest < Minitest::Test
4+
UnknownAdapterError = ::ActiveModel::Serializer::Adapter::UnknownAdapterError
5+
46
def setup
57
@previous_adapter = ActiveModel::Serializer.config.adapter
68
end
@@ -20,7 +22,7 @@ def test_overwrite_adapter_with_symbol
2022
adapter = ActiveModel::Serializer.adapter
2123
assert_equal ActiveModel::Serializer::Adapter::Null, adapter
2224
ensure
23-
25+
ActiveModel::Serializer.config.adapter = @previous_adapter
2426
end
2527

2628
def test_overwrite_adapter_with_class
@@ -33,18 +35,114 @@ def test_overwrite_adapter_with_class
3335
def test_raises_exception_if_invalid_symbol_given
3436
ActiveModel::Serializer.config.adapter = :unknown
3537

36-
assert_raises ArgumentError do
38+
assert_raises UnknownAdapterError do
3739
ActiveModel::Serializer.adapter
3840
end
3941
end
4042

4143
def test_raises_exception_if_it_does_not_know_hot_to_infer_adapter
4244
ActiveModel::Serializer.config.adapter = 42
4345

44-
assert_raises ArgumentError do
46+
assert_raises UnknownAdapterError do
4547
ActiveModel::Serializer.adapter
4648
end
4749
end
50+
51+
def test_adapter_class_for_known_adapter
52+
klass = ActiveModel::Serializer::Adapter.adapter_class(:json_api)
53+
assert_equal ActiveModel::Serializer::Adapter::JsonApi, klass
54+
end
55+
56+
def test_adapter_class_for_unknown_adapter
57+
assert_raises UnknownAdapterError do
58+
ActiveModel::Serializer::Adapter.adapter_class(:json_simple)
59+
end
60+
end
61+
62+
def test_adapter_map
63+
assert_equal ActiveModel::Serializer::Adapter.adapter_map, {
64+
"json".freeze => ActiveModel::Serializer::Adapter::Json,
65+
"json_api".freeze => ActiveModel::Serializer::Adapter::JsonApi,
66+
"flatten_json".freeze => ActiveModel::Serializer::Adapter::FlattenJson,
67+
"null".freeze => ActiveModel::Serializer::Adapter::Null
68+
}
69+
end
70+
71+
def test_adapters
72+
assert_equal ActiveModel::Serializer::Adapter.adapters.sort, [
73+
"flatten_json".freeze,
74+
"json".freeze,
75+
"json_api".freeze,
76+
"null".freeze,
77+
]
78+
end
79+
80+
def test_get_adapter_by_string_name
81+
assert_equal ActiveModel::Serializer::Adapter.get("json".freeze), ActiveModel::Serializer::Adapter::Json
82+
end
83+
84+
def test_get_adapter_by_symbol_name
85+
assert_equal ActiveModel::Serializer::Adapter.get(:json), ActiveModel::Serializer::Adapter::Json
86+
end
87+
88+
def test_get_adapter_by_class
89+
klass = ActiveModel::Serializer::Adapter::Json
90+
assert_equal ActiveModel::Serializer::Adapter.get(klass), klass
91+
end
92+
93+
def test_get_adapter_from_environment_registers_adapter
94+
ActiveModel::Serializer::Adapter.const_set(:AdapterFromEnvironment, Class.new)
95+
klass = ::ActiveModel::Serializer::Adapter::AdapterFromEnvironment
96+
name = "adapter_from_environment".freeze
97+
assert_equal ActiveModel::Serializer::Adapter.get(name), klass
98+
assert ActiveModel::Serializer::Adapter.adapters.include?(name)
99+
ensure
100+
ActiveModel::Serializer::Adapter::ADAPTER_MAP.delete(name)
101+
ActiveModel::Serializer::Adapter.send(:remove_const, :AdapterFromEnvironment)
102+
end
103+
104+
def test_get_adapter_for_unknown_name
105+
assert_raises UnknownAdapterError do
106+
ActiveModel::Serializer::Adapter.get(:json_simple)
107+
end
108+
end
109+
110+
def test_adapter
111+
assert_equal ActiveModel::Serializer.config.adapter, :flatten_json
112+
assert_equal ActiveModel::Serializer.adapter, ActiveModel::Serializer::Adapter::FlattenJson
113+
end
114+
115+
def test_register_adapter
116+
new_adapter_name = :foo
117+
new_adapter_klass = Class.new
118+
ActiveModel::Serializer::Adapter.register(new_adapter_name, new_adapter_klass)
119+
assert ActiveModel::Serializer::Adapter.adapters.include?("foo".freeze)
120+
assert ActiveModel::Serializer::Adapter.get(:foo), new_adapter_klass
121+
ensure
122+
ActiveModel::Serializer::Adapter::ADAPTER_MAP.delete(new_adapter_name.to_s)
123+
end
124+
125+
def test_inherited_adapter_hooks_register_adapter
126+
Object.const_set(:MyAdapter, Class.new)
127+
my_adapter = MyAdapter
128+
ActiveModel::Serializer::Adapter.inherited(my_adapter)
129+
assert_equal ActiveModel::Serializer::Adapter.get(:my_adapter), my_adapter
130+
ensure
131+
ActiveModel::Serializer::Adapter::ADAPTER_MAP.delete("my_adapter".freeze)
132+
Object.send(:remove_const, :MyAdapter)
133+
end
134+
135+
def test_inherited_adapter_hooks_register_demodulized_adapter
136+
Object.const_set(:MyNamespace, Module.new)
137+
MyNamespace.const_set(:MyAdapter, Class.new)
138+
my_adapter = MyNamespace::MyAdapter
139+
ActiveModel::Serializer::Adapter.inherited(my_adapter)
140+
assert_equal ActiveModel::Serializer::Adapter.get(:my_adapter), my_adapter
141+
ensure
142+
ActiveModel::Serializer::Adapter::ADAPTER_MAP.delete("my_adapter".freeze)
143+
MyNamespace.send(:remove_const, :MyAdapter)
144+
Object.send(:remove_const, :MyNamespace)
145+
end
48146
end
49147
end
50148
end

test/test_helper.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@
1818
@capture_warnings.after_tests
1919
end
2020
require 'active_model_serializers'
21+
# eager load autoloaded adapters
22+
require 'active_model/serializer/adapter'
23+
ActiveModel::Serializer::Adapter::Null
24+
ActiveModel::Serializer::Adapter::Json
25+
ActiveModel::Serializer::Adapter::FlattenJson
26+
ActiveModel::Serializer::Adapter::JsonApi
2127

2228
# Use cleaner stream testing interface from Rails 5 if available
2329
# see https://github.com/rails/rails/blob/29959eb59d/activesupport/lib/active_support/testing/stream.rb

0 commit comments

Comments
 (0)