diff --git a/.gitignore b/.gitignore index ad7f37d1a..43b0fba4c 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ .config .yardoc Gemfile.lock +Gemfile.local InstalledFiles _yardoc coverage diff --git a/Gemfile b/Gemfile index bdfff8565..6472755bc 100644 --- a/Gemfile +++ b/Gemfile @@ -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 diff --git a/docs/general/adapters.md b/docs/general/adapters.md index 60dc9842d..1302c439b 100644 --- a/docs/general/adapters.md +++ b/docs/general/adapters.md @@ -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) diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index a9f0e756c..2b8c1cf4e 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -105,19 +105,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 diff --git a/lib/active_model/serializer/adapter.rb b/lib/active_model/serializer/adapter.rb index 69593828a..c5eedd080 100644 --- a/lib/active_model/serializer/adapter.rb +++ b/lib/active_model/serializer/adapter.rb @@ -1,6 +1,8 @@ module ActiveModel class Serializer class Adapter + UnknownAdapterError = Class.new(ArgumentError) + ADAPTER_MAP = {} extend ActiveSupport::Autoload require 'active_model/serializer/adapter/json' require 'active_model/serializer/adapter/json_api' @@ -14,13 +16,71 @@ 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 attr_reader :serializer + # Only the Adapter class has these methods. + # None of the sublasses have them. + class << ActiveModel::Serializer::Adapter + # @return Hash + def adapter_map + ADAPTER_MAP + end + + # @return Array + 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 diff --git a/test/adapter_test.rb b/test/adapter_test.rb index 3349d8a76..897c89f0e 100644 --- a/test/adapter_test.rb +++ b/test/adapter_test.rb @@ -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 diff --git a/test/serializers/adapter_for_test.rb b/test/serializers/adapter_for_test.rb index 507b6bf1c..4acda1379 100644 --- a/test/serializers/adapter_for_test.rb +++ b/test/serializers/adapter_for_test.rb @@ -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 @@ -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 @@ -33,7 +35,7 @@ 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 @@ -41,10 +43,106 @@ def test_raises_exception_if_invalid_symbol_given 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 diff --git a/test/test_helper.rb b/test/test_helper.rb index 1327188e2..8121af446 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -13,7 +13,6 @@ # Ensure backward compatibility with Minitest 4 Minitest::Test = MiniTest::Unit::TestCase unless defined?(Minitest::Test) - require 'capture_warnings' @capture_warnings = CaptureWarnings.new(fail_build = true) @capture_warnings.before_tests @@ -29,6 +28,12 @@ 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 require 'support/stream_capture'