Skip to content

Commit 4399c1a

Browse files
committed
Merge pull request #1017 from bf4/registerable_adapters
Make Adapters registerable so they are not namespace-constrained
2 parents 1388ae8 + 880f235 commit 4399c1a

20 files changed

+310
-107
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: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,39 @@ AllCops:
88
DisplayCopNames: true
99
DisplayStyleGuide: true
1010

11+
Style/IndentationConsistency:
12+
Exclude:
13+
- lib/active_model/serializer/adapter/flatten_json.rb
14+
- lib/active_model/serializer/adapter/fragment_cache.rb
15+
- lib/active_model/serializer/adapter/json.rb
16+
- lib/active_model/serializer/adapter/json/fragment_cache.rb
17+
- lib/active_model/serializer/adapter/json_api.rb
18+
- lib/active_model/serializer/adapter/json_api/fragment_cache.rb
19+
- lib/active_model/serializer/adapter/json_api/pagination_links.rb
20+
- lib/active_model/serializer/adapter/null.rb
21+
22+
Style/IndentationWidth:
23+
Exclude:
24+
- lib/active_model/serializer/adapter/flatten_json.rb
25+
- lib/active_model/serializer/adapter/fragment_cache.rb
26+
- lib/active_model/serializer/adapter/json.rb
27+
- lib/active_model/serializer/adapter/json/fragment_cache.rb
28+
- lib/active_model/serializer/adapter/json_api.rb
29+
- lib/active_model/serializer/adapter/json_api/fragment_cache.rb
30+
- lib/active_model/serializer/adapter/json_api/pagination_links.rb
31+
- lib/active_model/serializer/adapter/null.rb
32+
33+
Style/AccessModifierIndentation:
34+
Exclude:
35+
- lib/active_model/serializer/adapter/flatten_json.rb
36+
- lib/active_model/serializer/adapter/fragment_cache.rb
37+
- lib/active_model/serializer/adapter/json.rb
38+
- lib/active_model/serializer/adapter/json/fragment_cache.rb
39+
- lib/active_model/serializer/adapter/json_api.rb
40+
- lib/active_model/serializer/adapter/json_api/fragment_cache.rb
41+
- lib/active_model/serializer/adapter/json_api/pagination_links.rb
42+
- lib/active_model/serializer/adapter/null.rb
43+
1144
Lint/NestedMethodDefinition:
1245
Enabled: false
1346
Exclude:

.simplecov

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
when 'jruby', 'rbx'
99
96.0
1010
else
11-
98.3
11+
98.1
1212
end
1313
}.to_f.round(2)
1414
# rubocop:disable Style/DoubleNegation

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.lookup(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.lookup(adapter)` |
96+
| `ActiveModel::Serializer.adapter` | a convenience method for `ActiveModel::Serializer::Adapter.lookup(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/serializable_resource.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def serializer?
7676
private
7777

7878
ActiveModelSerializers.silence_warnings do
79-
attr_reader :resource, :adapter_opts, :serializer_opts
79+
attr_reader :resource, :adapter_opts, :serializer_opts
8080
end
8181
end
8282
end

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.lookup
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.lookup(config.adapter)
110100
end
111101

112102
def self.root_name

lib/active_model/serializer/adapter.rb

Lines changed: 67 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,83 @@
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
5-
require 'active_model/serializer/adapter/json'
6-
require 'active_model/serializer/adapter/json_api'
7-
autoload :FlattenJson
8-
autoload :Null
98
autoload :FragmentCache
9+
autoload :Json
10+
autoload :JsonApi
11+
autoload :Null
12+
autoload :FlattenJson
1013

1114
def self.create(resource, options = {})
1215
override = options.delete(:adapter)
1316
klass = override ? adapter_class(override) : ActiveModel::Serializer.adapter
1417
klass.new(resource, options)
1518
end
1619

20+
# @see ActiveModel::Serializer::Adapter.lookup
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.lookup(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 lookup(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 NameError, ArgumentError => e
64+
failure_message =
65+
"NameError: #{e.message}. Unknown adapter: #{adapter.inspect}. Valid adapters are: #{adapters}"
66+
raise UnknownAdapterError, failure_message, e.backtrace
67+
end
68+
69+
# @api private
70+
def find_by_name(adapter_name)
71+
adapter_name = adapter_name.to_s.classify.tr('API', 'Api')
72+
ActiveModel::Serializer::Adapter.const_get(adapter_name.to_sym) or # rubocop:disable Style/AndOr
73+
fail UnknownAdapterError
74+
end
75+
private :find_by_name
76+
end
77+
78+
# Automatically register adapters when subclassing
79+
def self.inherited(subclass)
80+
ActiveModel::Serializer::Adapter.register(subclass.to_s.demodulize, subclass)
2081
end
2182

2283
attr_reader :serializer
Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
module ActiveModel
2-
class Serializer
3-
class Adapter
4-
class FlattenJson < Json
1+
class ActiveModel::Serializer::Adapter::FlattenJson < ActiveModel::Serializer::Adapter::Json
52
def serializable_hash(options = {})
63
super
74
@result
@@ -13,7 +10,4 @@ def serializable_hash(options = {})
1310
def include_meta(json)
1411
json
1512
end
16-
end
17-
end
18-
end
1913
end

lib/active_model/serializer/adapter/fragment_cache.rb

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
module ActiveModel
2-
class Serializer
3-
class Adapter
4-
class FragmentCache
1+
class ActiveModel::Serializer::Adapter::FragmentCache
52
attr_reader :serializer
63

74
def initialize(adapter, serializer, options)
@@ -75,7 +72,4 @@ def fragment_serializer(name, klass)
7572
def to_valid_const_name(name)
7673
name.gsub('::', '_')
7774
end
78-
end
79-
end
80-
end
8175
end

0 commit comments

Comments
 (0)