Skip to content

Commit

Permalink
Make Adapters registerable so they are not namespace-constrained
Browse files Browse the repository at this point in the history
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
  • Loading branch information
bf4 committed Aug 21, 2015
1 parent fc7b9c3 commit 8da44d7
Show file tree
Hide file tree
Showing 8 changed files with 182 additions and 27 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
.config
.yardoc
Gemfile.lock
Gemfile.local
InstalledFiles
_yardoc
coverage
Expand Down
6 changes: 6 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
source 'https://rubygems.org'
#
# Add a Gemfile.local to locally bundle gems outside of version control
local_gemfile = File.join(File.expand_path("..", __FILE__), "Gemfile.local")
if File.readable?(local_gemfile)
eval_gemfile local_gemfile
end

# Specify your gem's dependencies in active_model_serializers.gemspec
gemspec
Expand Down
4 changes: 4 additions & 0 deletions docs/general/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,7 @@ If you want to have a root key in your responses you should use the Json adapter
```ruby
ActiveModel::Serializer.config.adapter = :json
```

## Registering an adapter

ActiveModel::Serializer::Adapter.register(:my_adapter, MyAdapter)
14 changes: 2 additions & 12 deletions lib/active_model/serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,19 +89,9 @@ def self.serializer_for(resource, options = {})
end
end

# @see ActiveModel::Serializer::Adapter.get
def self.adapter
adapter_class = case config.adapter
when Symbol
ActiveModel::Serializer::Adapter.adapter_class(config.adapter)
when Class
config.adapter
end
unless adapter_class
valid_adapters = Adapter.constants.map { |klass| ":#{klass.to_s.downcase}" }
raise ArgumentError, "Unknown adapter: #{config.adapter}. Valid adapters are: #{valid_adapters}"
end

adapter_class
ActiveModel::Serializer::Adapter.get(config.adapter)
end

def self.root_name
Expand Down
64 changes: 62 additions & 2 deletions lib/active_model/serializer/adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
module ActiveModel
class Serializer
class Adapter
UnknownAdapterError = Class.new(ArgumentError)
ADAPTER_MAP = {}
extend ActiveSupport::Autoload
autoload :Json
autoload :FlattenJson
Expand All @@ -11,6 +13,64 @@ class Adapter

attr_reader :serializer

# Only the Adapter class has these methods.
# None of the sublasses have them.
class << ActiveModel::Serializer::Adapter
# @return Hash<adapter_name, adapter_class>
def adapter_map
ADAPTER_MAP
end

# @return Array<adapter_name>
def adapters
adapter_map.keys.sort
end

# Adds an adapter 'klass' with 'name' to the 'adapter_map'
# Names are stringified and underscored
def register(name, klass)
adapter_map.update(name.to_s.underscore => klass)
self
end

# @param adapter [String, Symbol, Class] name to fetch adapter by
# @return [ActiveModel::Serializer::Adapter] subclass of Adapter
# @raise [UnknownAdapterError]
def get(adapter)
# 1. return if is a class
return adapter if adapter.is_a?(Class)
adapter_name = adapter.to_s.underscore
# 2. return if registered
adapter_map.fetch(adapter_name) {
# 3. try to find adapter class from environment
adapter_class = find_by_name(adapter_name)
register(adapter_name, adapter_class)
adapter_class
}
rescue ArgumentError
failure_message =
"Unknown adapter: #{adapter.inspect}. Valid adapters are: #{adapters}"
raise UnknownAdapterError, failure_message, $!.backtrace
rescue NameError
failure_message =
"NameError: #{$!.message}. Unknown adapter: #{adapter.inspect}. Valid adapters are: #{adapters}"
raise UnknownAdapterError, failure_message, $!.backtrace
end

# @api private
def find_by_name(adapter_name)
adapter_name = adapter_name.to_s.classify.tr("API", "Api")
"ActiveModel::Serializer::Adapter::#{adapter_name}".safe_constantize or
fail UnknownAdapterError
end
private :find_by_name
end

# Automatically register adapters when subclassing
def self.inherited(subclass)
ActiveModel::Serializer::Adapter.register(subclass.to_s.demodulize, subclass)
end

def initialize(serializer, options = {})
@serializer = serializer
@options = options
Expand All @@ -32,9 +92,9 @@ def self.create(resource, options = {})
klass.new(resource, options)
end

# @see ActiveModel::Serializer::Adapter.get
def self.adapter_class(adapter)
adapter_name = adapter.to_s.classify.sub("API", "Api")
"ActiveModel::Serializer::Adapter::#{adapter_name}".safe_constantize
ActiveModel::Serializer::Adapter.get(adapter)
end

def fragment_cache(*args)
Expand Down
10 changes: 0 additions & 10 deletions test/adapter_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,6 @@ def test_serializer
assert_equal @serializer, @adapter.serializer
end

def test_adapter_class_for_known_adapter
klass = ActiveModel::Serializer::Adapter.adapter_class(:json_api)
assert_equal ActiveModel::Serializer::Adapter::JsonApi, klass
end

def test_adapter_class_for_unknown_adapter
klass = ActiveModel::Serializer::Adapter.adapter_class(:json_simple)
assert_nil klass
end

def test_create_adapter
adapter = ActiveModel::Serializer::Adapter.create(@serializer)
assert_equal ActiveModel::Serializer::Adapter::FlattenJson, adapter.class
Expand Down
104 changes: 101 additions & 3 deletions test/serializers/adapter_for_test.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
module ActiveModel
class Serializer
class AdapterForTest < Minitest::Test
UnknownAdapterError = ::ActiveModel::Serializer::Adapter::UnknownAdapterError

def setup
@previous_adapter = ActiveModel::Serializer.config.adapter
end
Expand All @@ -20,7 +22,7 @@ def test_overwrite_adapter_with_symbol
adapter = ActiveModel::Serializer.adapter
assert_equal ActiveModel::Serializer::Adapter::Null, adapter
ensure

ActiveModel::Serializer.config.adapter = @previous_adapter
end

def test_overwrite_adapter_with_class
Expand All @@ -33,18 +35,114 @@ def test_overwrite_adapter_with_class
def test_raises_exception_if_invalid_symbol_given
ActiveModel::Serializer.config.adapter = :unknown

assert_raises ArgumentError do
assert_raises UnknownAdapterError do
ActiveModel::Serializer.adapter
end
end

def test_raises_exception_if_it_does_not_know_hot_to_infer_adapter
ActiveModel::Serializer.config.adapter = 42

assert_raises ArgumentError do
assert_raises UnknownAdapterError do
ActiveModel::Serializer.adapter
end
end

def test_adapter_class_for_known_adapter
klass = ActiveModel::Serializer::Adapter.adapter_class(:json_api)
assert_equal ActiveModel::Serializer::Adapter::JsonApi, klass
end

def test_adapter_class_for_unknown_adapter
assert_raises UnknownAdapterError do
ActiveModel::Serializer::Adapter.adapter_class(:json_simple)
end
end

def test_adapter_map
assert_equal ActiveModel::Serializer::Adapter.adapter_map, {
"json".freeze => ActiveModel::Serializer::Adapter::Json,
"json_api".freeze => ActiveModel::Serializer::Adapter::JsonApi,
"flatten_json".freeze => ActiveModel::Serializer::Adapter::FlattenJson,
"null".freeze => ActiveModel::Serializer::Adapter::Null
}
end

def test_adapters
assert_equal ActiveModel::Serializer::Adapter.adapters.sort, [
"flatten_json".freeze,
"json".freeze,
"json_api".freeze,
"null".freeze,
]
end

def test_get_adapter_by_string_name
assert_equal ActiveModel::Serializer::Adapter.get("json".freeze), ActiveModel::Serializer::Adapter::Json
end

def test_get_adapter_by_symbol_name
assert_equal ActiveModel::Serializer::Adapter.get(:json), ActiveModel::Serializer::Adapter::Json
end

def test_get_adapter_by_class
klass = ActiveModel::Serializer::Adapter::Json
assert_equal ActiveModel::Serializer::Adapter.get(klass), klass
end

def test_get_adapter_from_environment_registers_adapter
ActiveModel::Serializer::Adapter.const_set(:AdapterFromEnvironment, Class.new)
klass = ::ActiveModel::Serializer::Adapter::AdapterFromEnvironment
name = "adapter_from_environment".freeze
assert_equal ActiveModel::Serializer::Adapter.get(name), klass
assert ActiveModel::Serializer::Adapter.adapters.include?(name)
ensure
ActiveModel::Serializer::Adapter::ADAPTER_MAP.delete(name)
ActiveModel::Serializer::Adapter.send(:remove_const, :AdapterFromEnvironment)
end

def test_get_adapter_for_unknown_name
assert_raises UnknownAdapterError do
ActiveModel::Serializer::Adapter.get(:json_simple)
end
end

def test_adapter
assert_equal ActiveModel::Serializer.config.adapter, :flatten_json
assert_equal ActiveModel::Serializer.adapter, ActiveModel::Serializer::Adapter::FlattenJson
end

def test_register_adapter
new_adapter_name = :foo
new_adapter_klass = Class.new
ActiveModel::Serializer::Adapter.register(new_adapter_name, new_adapter_klass)
assert ActiveModel::Serializer::Adapter.adapters.include?("foo".freeze)
assert ActiveModel::Serializer::Adapter.get(:foo), new_adapter_klass
ensure
ActiveModel::Serializer::Adapter::ADAPTER_MAP.delete(new_adapter_name.to_s)
end

def test_inherited_adapter_hooks_register_adapter
Object.const_set(:MyAdapter, Class.new)
my_adapter = MyAdapter
ActiveModel::Serializer::Adapter.inherited(my_adapter)
assert_equal ActiveModel::Serializer::Adapter.get(:my_adapter), my_adapter
ensure
ActiveModel::Serializer::Adapter::ADAPTER_MAP.delete("my_adapter".freeze)
Object.send(:remove_const, :MyAdapter)
end

def test_inherited_adapter_hooks_register_demodulized_adapter
Object.const_set(:MyNamespace, Module.new)
MyNamespace.const_set(:MyAdapter, Class.new)
my_adapter = MyNamespace::MyAdapter
ActiveModel::Serializer::Adapter.inherited(my_adapter)
assert_equal ActiveModel::Serializer::Adapter.get(:my_adapter), my_adapter
ensure
ActiveModel::Serializer::Adapter::ADAPTER_MAP.delete("my_adapter".freeze)
MyNamespace.send(:remove_const, :MyAdapter)
Object.send(:remove_const, :MyNamespace)
end
end
end
end
6 changes: 6 additions & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@
@capture_warnings.after_tests
end
require 'active_model_serializers'
# eager load autoloaded adapters
require 'active_model/serializer/adapter'
ActiveModel::Serializer::Adapter::Null
ActiveModel::Serializer::Adapter::Json
ActiveModel::Serializer::Adapter::FlattenJson
ActiveModel::Serializer::Adapter::JsonApi

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

0 comments on commit 8da44d7

Please sign in to comment.