Merge pull request #612 from bolshakov/feature/adapter

Feature/adapter
This commit is contained in:
Steve Klabnik 2014-08-31 19:58:25 -04:00
commit 98a3e5696e
25 changed files with 470 additions and 77 deletions

View File

@ -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: change this in an initializer:
```ruby ```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 You won't need to implement an adapter unless you wish to use a new format or

View File

@ -12,7 +12,7 @@ module ActionController
if serializer if serializer
# omg hax # omg hax
object = serializer.new(resource) object = serializer.new(resource)
adapter = ActiveModel::Serializer::Adapter::NullAdapter.new(object) adapter = ActiveModel::Serializer.adapter.new(object)
super(adapter, options) super(adapter, options)
else else

View File

@ -3,6 +3,7 @@ module ActiveModel
extend ActiveSupport::Autoload extend ActiveSupport::Autoload
autoload :Configuration autoload :Configuration
autoload :ArraySerializer autoload :ArraySerializer
autoload :Adapter
include Configuration include Configuration
class << self class << self
@ -65,26 +66,49 @@ module ActiveModel
if resource.respond_to?(:to_ary) if resource.respond_to?(:to_ary)
config.array_serializer config.array_serializer
else else
serializer_name = "#{resource.class.name}Serializer" serializer_class = "#{resource.class.name}Serializer"
serializer_class.safe_constantize
begin
Object.const_get(serializer_name)
rescue NameError
nil
end
end end
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 attr_accessor :object
def initialize(object) def initialize(object)
@object = object @object = object
end end
def attributes def attributes(options = {})
self.class._attributes.dup.each_with_object({}) do |name, hash| self.class._attributes.dup.each_with_object({}) do |name, hash|
hash[name] = send(name) hash[name] = send(name)
end end
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
end end

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,11 @@
module ActiveModel
class Serializer
class Adapter
class Null < Adapter
def serializable_hash(options = {})
{}
end
end
end
end
end

View File

@ -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

View File

@ -1,7 +1,15 @@
module ActiveModel module ActiveModel
class Serializer class Serializer
class ArraySerializer 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 end
end end

View File

@ -6,6 +6,7 @@ module ActiveModel
included do |base| included do |base|
base.config.array_serializer = ActiveModel::Serializer::ArraySerializer base.config.array_serializer = ActiveModel::Serializer::ArraySerializer
base.config.adapter = :json
end end
end end
end end

View File

@ -1,7 +1,6 @@
require "active_model" require "active_model"
require "active_model/serializer/version" require "active_model/serializer/version"
require "active_model/serializer" require "active_model/serializer"
require "active_model/serializer/adapter/null_adapter"
begin begin
require 'action_controller' require 'action_controller'

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

29
test/adapter/json_test.rb Normal file
View File

@ -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

25
test/adapter/null_test.rb Normal file
View File

@ -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

23
test/adapter_test.rb Normal file
View File

@ -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

View File

@ -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

View File

@ -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

31
test/fixtures/poro.rb vendored
View File

@ -5,11 +5,25 @@ class Model
def read_attribute_for_serialization(name) def read_attribute_for_serialization(name)
if name == :id || name == 'id' if name == :id || name == 'id'
object_id id
else else
@attributes[name] @attributes[name]
end end
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 end
class Profile < Model class Profile < Model
@ -18,3 +32,18 @@ end
class ProfileSerializer < ActiveModel::Serializer class ProfileSerializer < ActiveModel::Serializer
attributes :name, :description attributes :name, :description
end 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

View File

@ -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

View File

@ -3,10 +3,6 @@ require 'test_helper'
module ActiveModel module ActiveModel
class Serializer class Serializer
class AssocationsTest < Minitest::Test class AssocationsTest < Minitest::Test
def def_serializer(&block)
Class.new(ActiveModel::Serializer, &block)
end
class Model class Model
def initialize(hash={}) def initialize(hash={})
@attributes = hash @attributes = hash
@ -27,38 +23,33 @@ module ActiveModel
end end
end end
def setup def setup
@post = Model.new({ title: 'New Post', body: 'Body' }) @post = Post.new({ title: 'New Post', body: 'Body' })
@comment = Model.new({ id: 1, body: 'ZOMG A COMMENT' }) @comment = Comment.new({ id: 1, body: 'ZOMG A COMMENT' })
@post.comments = [@comment] @post.comments = [@comment]
@comment.post = @post @comment.post = @post
@post_serializer_class = def_serializer do @post_serializer = PostSerializer.new(@post)
attributes :title, :body @comment_serializer = CommentSerializer.new(@comment)
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)
end end
def test_has_many 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) 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 end
def test_has_one 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) 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 end
end end

View File

@ -6,6 +6,10 @@ module ActiveModel
def test_array_serializer def test_array_serializer
assert_equal ActiveModel::Serializer::ArraySerializer, ActiveModel::Serializer.config.array_serializer assert_equal ActiveModel::Serializer::ArraySerializer, ActiveModel::Serializer.config.array_serializer
end end
def test_adapter
assert_equal :json, ActiveModel::Serializer.config.adapter
end
end end
end end
end end

View File

@ -27,3 +27,7 @@ ActionController::TestCase.class_eval do
@routes = TestHelper::Routes @routes = TestHelper::Routes
end end
end end
def def_serializer(&block)
Class.new(ActiveModel::Serializer, &block)
end