diff --git a/README.md b/README.md index 441a5379..d98ab729 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,13 @@ by AMS. If you want to use a different adapter, such as a HalAdapter, you can change this in an initializer: ```ruby -ActiveModel::Serializer.default_adapter = ActiveModel::Serializer::Adapter::HalAdapter +ActiveModel::Serializer.config.adapter = ActiveModel::Serializer::Adapter::HalAdapter +``` + +or + +```ruby +ActiveModel::Serializer.config.adapter = :hal ``` You won't need to implement an adapter unless you wish to use a new format or diff --git a/lib/action_controller/serialization.rb b/lib/action_controller/serialization.rb index d51aba0f..a69109e5 100644 --- a/lib/action_controller/serialization.rb +++ b/lib/action_controller/serialization.rb @@ -12,7 +12,7 @@ module ActionController if serializer # omg hax object = serializer.new(resource) - adapter = ActiveModel::Serializer::Adapter::NullAdapter.new(object) + adapter = ActiveModel::Serializer.adapter.new(object) super(adapter, options) else diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index b94b8a46..05a79900 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -3,6 +3,7 @@ module ActiveModel extend ActiveSupport::Autoload autoload :Configuration autoload :ArraySerializer + autoload :Adapter include Configuration class << self @@ -65,26 +66,49 @@ module ActiveModel if resource.respond_to?(:to_ary) config.array_serializer else - serializer_name = "#{resource.class.name}Serializer" - - begin - Object.const_get(serializer_name) - rescue NameError - nil - end + serializer_class = "#{resource.class.name}Serializer" + serializer_class.safe_constantize end end + def self.adapter + adapter_class = case config.adapter + when Symbol + class_name = "ActiveModel::Serializer::Adapter::#{config.adapter.to_s.classify}" + class_name.safe_constantize + 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 + end + attr_accessor :object def initialize(object) @object = object end - def attributes + def attributes(options = {}) self.class._attributes.dup.each_with_object({}) do |name, hash| hash[name] = send(name) end end + + def each_association(&block) + self.class._associations.dup.each do |name, options| + association = object.send(name) + serializer_class = ActiveModel::Serializer.serializer_for(association) + serializer = serializer_class.new(association) + + if block_given? + block.call(name, serializer, options[:options]) + end + end + end end end diff --git a/lib/active_model/serializer/adapter.rb b/lib/active_model/serializer/adapter.rb new file mode 100644 index 00000000..2fae98ff --- /dev/null +++ b/lib/active_model/serializer/adapter.rb @@ -0,0 +1,24 @@ +module ActiveModel + class Serializer + class Adapter + extend ActiveSupport::Autoload + autoload :Json + autoload :Null + autoload :JsonApi + + attr_reader :serializer + + def initialize(serializer) + @serializer = serializer + end + + def serializable_hash(options = {}) + raise NotImplementedError, 'This is an abstract method. Should be implemented at the concrete adapter.' + end + + def to_json(options={}) + serializable_hash(options).to_json + end + end + end +end diff --git a/lib/active_model/serializer/adapter/json.rb b/lib/active_model/serializer/adapter/json.rb new file mode 100644 index 00000000..f37fc8d4 --- /dev/null +++ b/lib/active_model/serializer/adapter/json.rb @@ -0,0 +1,21 @@ +module ActiveModel + class Serializer + class Adapter + class Json < Adapter + def serializable_hash(options = {}) + @hash = serializer.attributes(options) + + serializer.each_association do |name, association, options| + if association.respond_to?(:each) + array_serializer = association + @hash[name] = array_serializer.map { |item| item.attributes(options) } + else + @hash[name] = association.attributes(options) + end + end + @hash + end + end + end + end +end diff --git a/lib/active_model/serializer/adapter/json_api.rb b/lib/active_model/serializer/adapter/json_api.rb new file mode 100644 index 00000000..a4f516dc --- /dev/null +++ b/lib/active_model/serializer/adapter/json_api.rb @@ -0,0 +1,39 @@ +module ActiveModel + class Serializer + class Adapter + class JsonApi < Adapter + def serializable_hash(options = {}) + @hash = serializer.attributes + + serializer.each_association do |name, association, options| + @hash[:links] ||= {} + @hash[:linked] ||= {} + + if association.respond_to?(:each) + add_links(name, association, options) + else + add_link(name, association, options) + end + end + + @hash + end + + def add_links(name, serializers, options) + @hash[:links][name] ||= [] + @hash[:linked][name] ||= [] + @hash[:links][name] += serializers.map(&:id) + @hash[:linked][name] += serializers.map { |item| item.attributes(options) } + end + + def add_link(name, serializer, options) + plural_name = name.to_s.pluralize.to_sym + @hash[:linked][plural_name] ||= [] + + @hash[:links][name] = serializer.id + @hash[:linked][plural_name].push serializer.attributes(options) + end + end + end + end +end diff --git a/lib/active_model/serializer/adapter/null.rb b/lib/active_model/serializer/adapter/null.rb new file mode 100644 index 00000000..547c08ba --- /dev/null +++ b/lib/active_model/serializer/adapter/null.rb @@ -0,0 +1,11 @@ +module ActiveModel + class Serializer + class Adapter + class Null < Adapter + def serializable_hash(options = {}) + {} + end + end + end + end +end diff --git a/lib/active_model/serializer/adapter/null_adapter.rb b/lib/active_model/serializer/adapter/null_adapter.rb deleted file mode 100644 index 7ef77f21..00000000 --- a/lib/active_model/serializer/adapter/null_adapter.rb +++ /dev/null @@ -1,17 +0,0 @@ -module ActiveModel - class Serializer - class Adapter - class NullAdapter - def initialize(adapter) - @attributes = adapter.attributes - end - - def to_json(options={}) - @attributes.each_with_object({}) do |(attr, value), h| - h[attr] = value - end.to_json # FIXME: why does passing options here cause {}? - end - end - end - end -end diff --git a/lib/active_model/serializer/array_serializer.rb b/lib/active_model/serializer/array_serializer.rb index d385d6bf..697c4454 100644 --- a/lib/active_model/serializer/array_serializer.rb +++ b/lib/active_model/serializer/array_serializer.rb @@ -1,7 +1,15 @@ module ActiveModel class Serializer class ArraySerializer + include Enumerable + delegate :each, to: :@objects + def initialize(objects, options = {}) + @objects = objects.map do |object| + serializer_class = ActiveModel::Serializer.serializer_for(object) + serializer_class.new(object) + end + end end end end diff --git a/lib/active_model/serializer/configuration.rb b/lib/active_model/serializer/configuration.rb index ac89443c..ef57262c 100644 --- a/lib/active_model/serializer/configuration.rb +++ b/lib/active_model/serializer/configuration.rb @@ -6,6 +6,7 @@ module ActiveModel included do |base| base.config.array_serializer = ActiveModel::Serializer::ArraySerializer + base.config.adapter = :json end end end diff --git a/lib/active_model_serializers.rb b/lib/active_model_serializers.rb index f7774290..c96b90a9 100644 --- a/lib/active_model_serializers.rb +++ b/lib/active_model_serializers.rb @@ -1,7 +1,6 @@ require "active_model" require "active_model/serializer/version" require "active_model/serializer" -require "active_model/serializer/adapter/null_adapter" begin require 'action_controller' diff --git a/test/adapter/json/belongs_to_test.rb b/test/adapter/json/belongs_to_test.rb new file mode 100644 index 00000000..498f1d98 --- /dev/null +++ b/test/adapter/json/belongs_to_test.rb @@ -0,0 +1,25 @@ +require 'test_helper' + +module ActiveModel + class Serializer + class Adapter + class Json + class BelongsToTest < Minitest::Test + def setup + @post = Post.new(id: 42, title: 'New Post', body: 'Body') + @comment = Comment.new(id: 1, body: 'ZOMG A COMMENT') + @post.comments = [@comment] + @comment.post = @post + + @serializer = CommentSerializer.new(@comment) + @adapter = ActiveModel::Serializer::Adapter::Json.new(@serializer) + end + + def test_includes_post + assert_equal({id: 42, title: 'New Post', body: 'Body'}, @adapter.serializable_hash[:post]) + end + end + end + end + end +end diff --git a/test/adapter/json/has_many_test.rb b/test/adapter/json/has_many_test.rb new file mode 100644 index 00000000..d1534e6b --- /dev/null +++ b/test/adapter/json/has_many_test.rb @@ -0,0 +1,31 @@ +require 'test_helper' + +module ActiveModel + class Serializer + class Adapter + class Json + class HasManyTestTest < Minitest::Test + def setup + @post = Post.new(title: 'New Post', body: 'Body') + @first_comment = Comment.new(id: 1, body: 'ZOMG A COMMENT') + @second_comment = Comment.new(id: 2, body: 'ZOMG ANOTHER COMMENT') + @post.comments = [@first_comment, @second_comment] + @first_comment.post = @post + @second_comment.post = @post + + @serializer = PostSerializer.new(@post) + @adapter = ActiveModel::Serializer::Adapter::Json.new(@serializer) + end + + def test_has_many + assert_equal([ + {id: 1, body: 'ZOMG A COMMENT'}, + {id: 2, body: 'ZOMG ANOTHER COMMENT'} + ], @adapter.serializable_hash[:comments]) + end + end + end + end + end +end + diff --git a/test/adapter/json_api/belongs_to_test.rb b/test/adapter/json_api/belongs_to_test.rb new file mode 100644 index 00000000..151fd8cc --- /dev/null +++ b/test/adapter/json_api/belongs_to_test.rb @@ -0,0 +1,29 @@ +require 'test_helper' + +module ActiveModel + class Serializer + class Adapter + class JsonApi + class BelongsToTest < Minitest::Test + def setup + @post = Post.new(id: 42, title: 'New Post', body: 'Body') + @comment = Comment.new(id: 1, body: 'ZOMG A COMMENT') + @post.comments = [@comment] + @comment.post = @post + + @serializer = CommentSerializer.new(@comment) + @adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer) + end + + def test_includes_post_id + assert_equal(42, @adapter.serializable_hash[:links][:post]) + end + + def test_includes_linked_post + assert_equal([{id: 42, title: 'New Post', body: 'Body'}], @adapter.serializable_hash[:linked][:posts]) + end + end + end + end + end +end diff --git a/test/adapter/json_api/has_many.rb b/test/adapter/json_api/has_many.rb new file mode 100644 index 00000000..435079d7 --- /dev/null +++ b/test/adapter/json_api/has_many.rb @@ -0,0 +1,34 @@ +require 'test_helper' + +module ActiveModel + class Serializer + class Adapter + class JsonApi + class HasManyTest < Minitest::Test + def setup + @post = Post.new(title: 'New Post', body: 'Body') + @first_comment = Comment.new(id: 1, body: 'ZOMG A COMMENT') + @second_comment = Comment.new(id: 2, body: 'ZOMG ANOTHER COMMENT') + @post.comments = [@first_comment, @second_comment] + @first_comment.post = @post + @second_comment.post = @post + + @serializer = PostSerializer.new(@post) + @adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer) + end + + def test_includes_comment_ids + assert_equal([1, 2], @adapter.serializable_hash[:links][:comments]) + end + + def test_includes_linked_comments + assert_equal([ + {id: 1, body: 'ZOMG A COMMENT'}, + {id: 2, body: 'ZOMG ANOTHER COMMENT'} + ], @adapter.serializable_hash[:linked][:comments]) + end + end + end + end + end +end diff --git a/test/adapter/json_test.rb b/test/adapter/json_test.rb new file mode 100644 index 00000000..1d2e35a6 --- /dev/null +++ b/test/adapter/json_test.rb @@ -0,0 +1,29 @@ +require 'test_helper' + +module ActiveModel + class Serializer + class Adapter + class JsonTest < Minitest::Test + def setup + @post = Post.new(title: 'New Post', body: 'Body') + @first_comment = Comment.new(id: 1, body: 'ZOMG A COMMENT') + @second_comment = Comment.new(id: 2, body: 'ZOMG ANOTHER COMMENT') + @post.comments = [@first_comment, @second_comment] + @first_comment.post = @post + @second_comment.post = @post + + @serializer = PostSerializer.new(@post) + @adapter = ActiveModel::Serializer::Adapter::Json.new(@serializer) + end + + def test_has_many + assert_equal([ + {id: 1, body: 'ZOMG A COMMENT'}, + {id: 2, body: 'ZOMG ANOTHER COMMENT'} + ], @adapter.serializable_hash[:comments]) + end + end + end + end +end + diff --git a/test/adapter/null_test.rb b/test/adapter/null_test.rb new file mode 100644 index 00000000..9f23b778 --- /dev/null +++ b/test/adapter/null_test.rb @@ -0,0 +1,25 @@ +require 'test_helper' + +module ActiveModel + class Serializer + class Adapter + class NullTest < Minitest::Test + def setup + profile = Profile.new({ name: 'Name 1', description: 'Description 1', comments: 'Comments 1' }) + serializer = ProfileSerializer.new(profile) + + @adapter = Null.new(serializer) + end + + def test_serializable_hash + assert_equal({}, @adapter.serializable_hash) + end + + def test_it_returns_empty_json + assert_equal('{}', @adapter.to_json) + end + end + end + end +end + diff --git a/test/adapter_test.rb b/test/adapter_test.rb new file mode 100644 index 00000000..737e0c4e --- /dev/null +++ b/test/adapter_test.rb @@ -0,0 +1,23 @@ +require 'test_helper' + +module ActiveModel + class Serializer + class AdapterTest < Minitest::Test + def setup + profile = Profile.new + @serializer = ProfileSerializer.new(profile) + @adapter = ActiveModel::Serializer::Adapter.new(@serializer) + end + + def test_serializable_hash_is_abstract_method + assert_raises(NotImplementedError) do + @adapter.serializable_hash(only: [:name]) + end + end + + def test_serializer + assert_equal @serializer, @adapter.serializer + end + end + end +end diff --git a/test/adapters/null_adapter_test.rb b/test/adapters/null_adapter_test.rb deleted file mode 100644 index b50e9e12..00000000 --- a/test/adapters/null_adapter_test.rb +++ /dev/null @@ -1,24 +0,0 @@ -require 'test_helper' - -module ActiveModel - class Serializer - class Adapter - class NullAdapterTest < Minitest::Test - def setup - @profile = Profile.new({ name: 'Name 1', description: 'Description 1', comments: 'Comments 1' }) - @profile_serializer = ProfileSerializer.new(@profile) - - @adapter = NullAdapter.new(@profile_serializer) - end - - def test_null_adapter - assert_equal('{"name":"Name 1","description":"Description 1"}', - @adapter.to_json) - -JSON - end - end - end - end -end - diff --git a/test/array_serializer_test.rb b/test/array_serializer_test.rb new file mode 100644 index 00000000..66840a4a --- /dev/null +++ b/test/array_serializer_test.rb @@ -0,0 +1,27 @@ +require 'test_helper' + +module ActiveModel + class Serializer + class ArraySerializerTest < Minitest::Test + def setup + @comment = Comment.new + @post= Post.new + @serializer = ArraySerializer.new([@comment, @post]) + end + + def test_respond_to_each + assert_respond_to @serializer, :each + end + + def test_each_object_should_be_serializer_with_appropriate_serializer + serializers = @serializer.to_a + + assert_kind_of CommentSerializer, serializers.first + assert_kind_of Comment, serializers.first.object + + assert_kind_of PostSerializer, serializers.last + assert_kind_of Post, serializers.last.object + end + end + end +end diff --git a/test/fixtures/poro.rb b/test/fixtures/poro.rb index 00594811..8359fcb7 100644 --- a/test/fixtures/poro.rb +++ b/test/fixtures/poro.rb @@ -5,11 +5,25 @@ class Model def read_attribute_for_serialization(name) if name == :id || name == 'id' - object_id + id else @attributes[name] end end + + def id + @attributes[:id] || @attributes['id'] || object_id + end + + def method_missing(meth, *args) + if meth.to_s =~ /^(.*)=$/ + @attributes[$1.to_sym] = args[0] + elsif @attributes.key?(meth) + @attributes[meth] + else + super + end + end end class Profile < Model @@ -18,3 +32,18 @@ end class ProfileSerializer < ActiveModel::Serializer attributes :name, :description end + +Post = Class.new(Model) +Comment = Class.new(Model) + +PostSerializer = Class.new(ActiveModel::Serializer) do + attributes :title, :body, :id + + has_many :comments +end + +CommentSerializer = Class.new(ActiveModel::Serializer) do + attributes :id, :body + + belongs_to :post +end diff --git a/test/serializers/adapter_for_test.rb b/test/serializers/adapter_for_test.rb new file mode 100644 index 00000000..60641080 --- /dev/null +++ b/test/serializers/adapter_for_test.rb @@ -0,0 +1,50 @@ +module ActiveModel + class Serializer + class AdapterForTest < Minitest::Test + def setup + @previous_adapter = ActiveModel::Serializer.config.adapter + end + + def teardown + ActiveModel::Serializer.config.adapter = @previous_adapter + end + + def test_returns_default_adapter + adapter = ActiveModel::Serializer.adapter + assert_equal ActiveModel::Serializer::Adapter::Json, adapter + end + + def test_overwrite_adapter_with_symbol + ActiveModel::Serializer.config.adapter = :null + + adapter = ActiveModel::Serializer.adapter + assert_equal ActiveModel::Serializer::Adapter::Null, adapter + ensure + + end + + def test_overwrite_adapter_with_class + ActiveModel::Serializer.config.adapter = ActiveModel::Serializer::Adapter::Null + + adapter = ActiveModel::Serializer.adapter + assert_equal ActiveModel::Serializer::Adapter::Null, adapter + end + + def test_raises_exception_if_invalid_symbol_given + ActiveModel::Serializer.config.adapter = :unknown + + assert_raises ArgumentError 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 + ActiveModel::Serializer.adapter + end + end + end + end +end diff --git a/test/serializers/associations_test.rb b/test/serializers/associations_test.rb index e019c073..6d69b563 100644 --- a/test/serializers/associations_test.rb +++ b/test/serializers/associations_test.rb @@ -3,10 +3,6 @@ require 'test_helper' module ActiveModel class Serializer class AssocationsTest < Minitest::Test - def def_serializer(&block) - Class.new(ActiveModel::Serializer, &block) - end - class Model def initialize(hash={}) @attributes = hash @@ -27,38 +23,33 @@ module ActiveModel end end + def setup - @post = Model.new({ title: 'New Post', body: 'Body' }) - @comment = Model.new({ id: 1, body: 'ZOMG A COMMENT' }) + @post = Post.new({ title: 'New Post', body: 'Body' }) + @comment = Comment.new({ id: 1, body: 'ZOMG A COMMENT' }) @post.comments = [@comment] @comment.post = @post - @post_serializer_class = def_serializer do - attributes :title, :body - end - - @comment_serializer_class = def_serializer do - attributes :id, :body - end - - @post_serializer = @post_serializer_class.new(@post) - @comment_serializer = @comment_serializer_class.new(@comment) + @post_serializer = PostSerializer.new(@post) + @comment_serializer = CommentSerializer.new(@comment) end def test_has_many - @post_serializer_class.class_eval do - has_many :comments - end - assert_equal({comments: {type: :has_many, options: {}}}, @post_serializer.class._associations) + @post_serializer.each_association do |name, serializer, options| + assert_equal(:comments, name) + assert_equal({}, options) + assert_kind_of(ActiveModel::Serializer.config.array_serializer, serializer) + end end def test_has_one - @comment_serializer_class.class_eval do - belongs_to :post - end - assert_equal({post: {type: :belongs_to, options: {}}}, @comment_serializer.class._associations) + @comment_serializer.each_association do |name, serializer, options| + assert_equal(:post, name) + assert_equal({}, options) + assert_kind_of(PostSerializer, serializer) + end end end end diff --git a/test/serializers/configuration_test.rb b/test/serializers/configuration_test.rb index 91110229..eec00436 100644 --- a/test/serializers/configuration_test.rb +++ b/test/serializers/configuration_test.rb @@ -6,6 +6,10 @@ module ActiveModel def test_array_serializer assert_equal ActiveModel::Serializer::ArraySerializer, ActiveModel::Serializer.config.array_serializer end + + def test_adapter + assert_equal :json, ActiveModel::Serializer.config.adapter + end end end end diff --git a/test/test_helper.rb b/test/test_helper.rb index b7875f75..bd41fb6a 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -27,3 +27,7 @@ ActionController::TestCase.class_eval do @routes = TestHelper::Routes end end + +def def_serializer(&block) + Class.new(ActiveModel::Serializer, &block) +end