Skip to content

Commit 18e5812

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 64168cb commit 18e5812

File tree

8 files changed

+182
-28
lines changed

8 files changed

+182
-28
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
@@ -105,19 +105,9 @@ def self.serializer_for(resource, options = {})
105105
end
106106
end
107107

108+
# @see ActiveModel::Serializer::Adapter.get
108109
def self.adapter
109-
adapter_class = case config.adapter
110-
when Symbol
111-
ActiveModel::Serializer::Adapter.adapter_class(config.adapter)
112-
when Class
113-
config.adapter
114-
end
115-
unless adapter_class
116-
valid_adapters = Adapter.constants.map { |klass| ":#{klass.to_s.downcase}" }
117-
raise ArgumentError, "Unknown adapter: #{config.adapter}. Valid adapters are: #{valid_adapters}"
118-
end
119-
120-
adapter_class
110+
ActiveModel::Serializer::Adapter.get(config.adapter)
121111
end
122112

123113
def self.root_name

lib/active_model/serializer/adapter.rb

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
module ActiveModel
22
class Serializer
33
class Adapter
4+
UnknownAdapterError = Class.new(ArgumentError)
5+
ADAPTER_MAP = {}
46
extend ActiveSupport::Autoload
57
require 'active_model/serializer/adapter/json'
68
require 'active_model/serializer/adapter/json_api'
@@ -14,13 +16,71 @@ def self.create(resource, options = {})
1416
klass.new(resource, options)
1517
end
1618

19+
# @see ActiveModel::Serializer::Adapter.get
1720
def self.adapter_class(adapter)
18-
adapter_name = adapter.to_s.classify.sub("API", "Api")
19-
"ActiveModel::Serializer::Adapter::#{adapter_name}".safe_constantize
21+
ActiveModel::Serializer::Adapter.get(adapter)
2022
end
2123

2224
attr_reader :serializer
2325

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

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 & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
# Ensure backward compatibility with Minitest 4
1414
Minitest::Test = MiniTest::Unit::TestCase unless defined?(Minitest::Test)
1515

16-
1716
require 'capture_warnings'
1817
@capture_warnings = CaptureWarnings.new(fail_build = true)
1918
@capture_warnings.before_tests
@@ -29,6 +28,12 @@
2928
end
3029

3130
require 'active_model_serializers'
31+
# eager load autoloaded adapters
32+
require 'active_model/serializer/adapter'
33+
ActiveModel::Serializer::Adapter::Null
34+
ActiveModel::Serializer::Adapter::Json
35+
ActiveModel::Serializer::Adapter::FlattenJson
36+
ActiveModel::Serializer::Adapter::JsonApi
3237

3338
require 'support/stream_capture'
3439

0 commit comments

Comments
 (0)