Skip to content

Commit d9e76c2

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 1388ae8 commit d9e76c2

File tree

9 files changed

+257
-28
lines changed

9 files changed

+257
-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

.rubocop.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ Lint/NestedMethodDefinition:
1616
Style/StringLiterals:
1717
EnforcedStyle: single_quotes
1818

19+
Style/SpecialGlobalVars:
20+
Enabled: false
21+
1922
Metrics/AbcSize:
2023
Max: 35 # TODO: Lower to 15
2124

Gemfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
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+
eval_gemfile local_gemfile if File.readable?(local_gemfile)
26

37
# Specify your gem's dependencies in active_model_serializers.gemspec
48
gemspec

docs/general/adapters.md

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ resources in the `"included"` member when the resource names are included in the
3232

3333
## Choosing an adapter
3434

35-
If you want to use a different adapter, such as JsonApi, you can change this in an initializer:
35+
If you want to use a specify a default adapter, such as JsonApi, you can change this in an initializer:
3636

3737
```ruby
3838
ActiveModel::Serializer.config.adapter = ActiveModel::Serializer::Adapter::JsonApi
@@ -44,8 +44,59 @@ or
4444
ActiveModel::Serializer.config.adapter = :json_api
4545
```
4646

47-
If you want to have a root key in your responses you should use the Json adapter, instead of the default FlattenJson:
47+
If you want to have a root key for each resource in your responses, you should use the Json or
48+
JsonApi adapters instead of the default FlattenJson:
4849

4950
```ruby
5051
ActiveModel::Serializer.config.adapter = :json
5152
```
53+
54+
## Advanced adapter configuration
55+
56+
### Registering an adapter
57+
58+
The default adapter can be configured, as above, to use any class given to it.
59+
60+
An adapter may also be specified, e.g. when rendering, as a class or as a symbol.
61+
If a symbol, then the adapter must be, e.g. `:great_example`,
62+
`ActiveModel::Serializer::Adapter::GreatExample`, or registered.
63+
64+
There are two ways to register an adapter:
65+
66+
1) The simplest, is to subclass `ActiveModel::Serializer::Adapter`, e.g. the below will
67+
register the `Example::UsefulAdapter` as `:useful_adapter`.
68+
69+
```ruby
70+
module Example
71+
class UsefulAdapter < ActiveModel::Serializer::Adapter
72+
end
73+
end
74+
```
75+
76+
You'll notice that the name it registers is the class name underscored, not the full namespace.
77+
78+
Under the covers, when the `ActiveModel::Serializer::Adapter` is subclassed, it registers
79+
the subclass as `register(:useful_adapter, Example::UsefulAdapter)`
80+
81+
2) Any class can be registered as an adapter by calling `register` directly on the
82+
`ActiveModel::Serializer::Adapter` class. e.g., the below registers `MyAdapter` as
83+
`:special_adapter`.
84+
85+
```ruby
86+
class MyAdapter; end
87+
ActiveModel::Serializer::Adapter.register(:special_adapter, MyAdapter)
88+
```
89+
90+
### Looking up an adapter
91+
92+
| `ActiveModel::Serializer::Adapter.adapter_map` | A Hash of all known adapters { adapter_name => adapter_class } |
93+
| `ActiveModel::Serializer::Adapter.adapters` | A (sorted) Array of all known adapter_names |
94+
| `ActiveModel::Serializer::Adapter.get(name_or_klass)` | The adapter_class, else raises an `ActiveModel::Serializer::Adapter::UnknownAdapter` error |
95+
| `ActiveModel::Serializer::Adapter.adapter_class(adapter)` | delegates to `ActiveModel::Serializer::Adapter.get(adapter)` |
96+
| `ActiveModel::Serializer.adapter` | a convenience method for `ActiveModel::Serializer::Adapter.get(config.adapter)` |
97+
98+
The registered adapter name is always a String, but may be looked up as a Symbol or String.
99+
Helpfully, the Symbol or String is underscored, so that `get(:my_adapter)` and `get("MyAdapter")`
100+
may both be used.
101+
102+
For more information, see [the Adapter class on GitHub](https://github.com/rails-api/active_model_serializers/blob/master/lib/active_model/serializer/adapter.rb)

lib/active_model/serializer.rb

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -94,19 +94,9 @@ def self.serializer_for(resource, options = {})
9494
end
9595
end
9696

97+
# @see ActiveModel::Serializer::Adapter.get
9798
def self.adapter
98-
adapter_class = case config.adapter
99-
when Symbol
100-
ActiveModel::Serializer::Adapter.adapter_class(config.adapter)
101-
when Class
102-
config.adapter
103-
end
104-
unless adapter_class
105-
valid_adapters = Adapter.constants.map { |klass| ":#{klass.to_s.downcase}" }
106-
raise ArgumentError, "Unknown adapter: #{config.adapter}. Valid adapters are: #{valid_adapters}"
107-
end
108-
109-
adapter_class
99+
ActiveModel::Serializer::Adapter.get(config.adapter)
110100
end
111101

112102
def self.root_name

lib/active_model/serializer/adapter.rb

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
module ActiveModel
22
class Serializer
33
class Adapter
4+
UnknownAdapterError = Class.new(ArgumentError)
5+
ADAPTER_MAP = {}
6+
private_constant :ADAPTER_MAP if defined?(private_constant)
47
extend ActiveSupport::Autoload
58
require 'active_model/serializer/adapter/json'
69
require 'active_model/serializer/adapter/json_api'
@@ -14,9 +17,71 @@ def self.create(resource, options = {})
1417
klass.new(resource, options)
1518
end
1619

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

2287
attr_reader :serializer

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: 118 additions & 2 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,6 +22,7 @@ def test_overwrite_adapter_with_symbol
2022
adapter = ActiveModel::Serializer.adapter
2123
assert_equal ActiveModel::Serializer::Adapter::Null, adapter
2224
ensure
25+
ActiveModel::Serializer.config.adapter = @previous_adapter
2326
end
2427

2528
def test_overwrite_adapter_with_class
@@ -32,18 +35,131 @@ def test_overwrite_adapter_with_class
3235
def test_raises_exception_if_invalid_symbol_given
3336
ActiveModel::Serializer.config.adapter = :unknown
3437

35-
assert_raises ArgumentError do
38+
assert_raises UnknownAdapterError do
3639
ActiveModel::Serializer.adapter
3740
end
3841
end
3942

4043
def test_raises_exception_if_it_does_not_know_hot_to_infer_adapter
4144
ActiveModel::Serializer.config.adapter = 42
4245

43-
assert_raises ArgumentError do
46+
assert_raises UnknownAdapterError do
4447
ActiveModel::Serializer.adapter
4548
end
4649
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+
expected_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+
assert_equal ActiveModel::Serializer::Adapter.adapter_map, expected_adapter_map
70+
end
71+
72+
def test_adapters
73+
assert_equal ActiveModel::Serializer::Adapter.adapters.sort, [
74+
'flatten_json'.freeze,
75+
'json'.freeze,
76+
'json_api'.freeze,
77+
'null'.freeze
78+
]
79+
end
80+
81+
def test_get_adapter_by_string_name
82+
assert_equal ActiveModel::Serializer::Adapter.get('json'.freeze), ActiveModel::Serializer::Adapter::Json
83+
end
84+
85+
def test_get_adapter_by_symbol_name
86+
assert_equal ActiveModel::Serializer::Adapter.get(:json), ActiveModel::Serializer::Adapter::Json
87+
end
88+
89+
def test_get_adapter_by_class
90+
klass = ActiveModel::Serializer::Adapter::Json
91+
assert_equal ActiveModel::Serializer::Adapter.get(klass), klass
92+
end
93+
94+
def test_get_adapter_from_environment_registers_adapter
95+
ActiveModel::Serializer::Adapter.const_set(:AdapterFromEnvironment, Class.new)
96+
klass = ::ActiveModel::Serializer::Adapter::AdapterFromEnvironment
97+
name = 'adapter_from_environment'.freeze
98+
assert_equal ActiveModel::Serializer::Adapter.get(name), klass
99+
assert ActiveModel::Serializer::Adapter.adapters.include?(name)
100+
ensure
101+
ActiveModel::Serializer::Adapter.adapter_map.delete(name)
102+
ActiveModel::Serializer::Adapter.send(:remove_const, :AdapterFromEnvironment)
103+
end
104+
105+
def test_get_adapter_for_unknown_name
106+
assert_raises UnknownAdapterError do
107+
ActiveModel::Serializer::Adapter.get(:json_simple)
108+
end
109+
end
110+
111+
def test_adapter
112+
assert_equal ActiveModel::Serializer.config.adapter, :flatten_json
113+
assert_equal ActiveModel::Serializer.adapter, ActiveModel::Serializer::Adapter::FlattenJson
114+
end
115+
116+
def test_register_adapter
117+
new_adapter_name = :foo
118+
new_adapter_klass = Class.new
119+
ActiveModel::Serializer::Adapter.register(new_adapter_name, new_adapter_klass)
120+
assert ActiveModel::Serializer::Adapter.adapters.include?('foo'.freeze)
121+
assert ActiveModel::Serializer::Adapter.get(:foo), new_adapter_klass
122+
ensure
123+
ActiveModel::Serializer::Adapter.adapter_map.delete(new_adapter_name.to_s)
124+
end
125+
126+
def test_inherited_adapter_hooks_register_adapter
127+
Object.const_set(:MyAdapter, Class.new)
128+
my_adapter = MyAdapter
129+
ActiveModel::Serializer::Adapter.inherited(my_adapter)
130+
assert_equal ActiveModel::Serializer::Adapter.get(:my_adapter), my_adapter
131+
ensure
132+
ActiveModel::Serializer::Adapter.adapter_map.delete('my_adapter'.freeze)
133+
Object.send(:remove_const, :MyAdapter)
134+
end
135+
136+
def test_inherited_adapter_hooks_register_demodulized_adapter
137+
Object.const_set(:MyNamespace, Module.new)
138+
MyNamespace.const_set(:MyAdapter, Class.new)
139+
my_adapter = MyNamespace::MyAdapter
140+
ActiveModel::Serializer::Adapter.inherited(my_adapter)
141+
assert_equal ActiveModel::Serializer::Adapter.get(:my_adapter), my_adapter
142+
ensure
143+
ActiveModel::Serializer::Adapter.adapter_map.delete('my_adapter'.freeze)
144+
MyNamespace.send(:remove_const, :MyAdapter)
145+
Object.send(:remove_const, :MyNamespace)
146+
end
147+
148+
def test_inherited_adapter_hooks_register_subclass_of_registered_adapter
149+
Object.const_set(:MyAdapter, Class.new)
150+
my_adapter = MyAdapter
151+
Object.const_set(:MySubclassedAdapter, Class.new(MyAdapter))
152+
my_subclassed_adapter = MySubclassedAdapter
153+
ActiveModel::Serializer::Adapter.inherited(my_adapter)
154+
ActiveModel::Serializer::Adapter.inherited(my_subclassed_adapter)
155+
assert_equal ActiveModel::Serializer::Adapter.get(:my_adapter), my_adapter
156+
assert_equal ActiveModel::Serializer::Adapter.get(:my_subclassed_adapter), my_subclassed_adapter
157+
ensure
158+
ActiveModel::Serializer::Adapter.adapter_map.delete('my_adapter'.freeze)
159+
ActiveModel::Serializer::Adapter.adapter_map.delete('my_subclassed_adapter'.freeze)
160+
Object.send(:remove_const, :MyAdapter)
161+
Object.send(:remove_const, :MySubclassedAdapter)
162+
end
47163
end
48164
end
49165
end

test/test_helper.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,15 @@
3939
end
4040

4141
require 'active_model_serializers'
42+
# eager load autoloaded adapters
43+
# rubocop:disable Lint/Void
44+
require 'active_model/serializer/adapter'
45+
ActiveModel::Serializer::Adapter::Null
46+
ActiveModel::Serializer::Adapter::Json
47+
ActiveModel::Serializer::Adapter::FlattenJson
48+
ActiveModel::Serializer::Adapter::JsonApi
49+
# rubocop:enable Lint/Void
50+
require 'active_model/serializer/adapter'
4251

4352
require 'support/stream_capture'
4453

0 commit comments

Comments
 (0)