diff --git a/.travis.yml b/.travis.yml index ad22ffe3..78612434 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,7 @@ language: ruby + +sudo: false + rvm: - 1.9.3 - 2.0.0 @@ -6,13 +9,16 @@ rvm: - jruby-19mode - rbx-2 - ruby-head + install: - bundle install --retry=3 + env: - "RAILS_VERSION=3.2" - "RAILS_VERSION=4.0" - "RAILS_VERSION=4.1" - "RAILS_VERSION=master" + matrix: allow_failures: - rvm: ruby-head diff --git a/README.md b/README.md index 4121dfe2..6bb70f3a 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,18 @@ ActiveModel::Serializer.config.adapter = :hal You won't need to implement an adapter unless you wish to use a new format or media type with AMS. +If you would like the key in the outputted JSON to be different from its name in ActiveRecord, you can use the :key option to customize it: + +```ruby +class PostSerializer < ActiveModel::Serializer + attributes :id, :body + + # look up :subject on the model, but use +title+ in the JSON + attribute :subject, :key => :title + has_many :comments +end +``` + In your controllers, when you use `render :json`, Rails will now first search for a serializer for the object and use it if available. @@ -94,6 +106,27 @@ member when the resource names are included in the `include` option. render @posts, include: 'authors,comments' ``` +### Specify a serializer + +If you wish to use a serializer other than the default, you can explicitly pass it to the renderer. + +#### 1. For a resource: + +```ruby + render json: @post, serializer: PostPreviewSerializer +``` + +#### 2. For an array resource: + +```ruby +# Use the default `ArraySerializer`, which will use `each_serializer` to +# serialize each element +render json: @posts, each_serializer: PostPreviewSerializer + +# Or, you can explicitly provide the collection serializer as well +render json: @posts, serializer: PaginatedSerializer, each_serializer: PostPreviewSerializer +``` + ## Installation Add this line to your application's Gemfile: diff --git a/lib/action_controller/serialization.rb b/lib/action_controller/serialization.rb index aa872ae9..90897a70 100644 --- a/lib/action_controller/serialization.rb +++ b/lib/action_controller/serialization.rb @@ -6,18 +6,32 @@ module ActionController include ActionController::Renderers - ADAPTER_OPTION_KEYS = [:include, :fields, :root] + ADAPTER_OPTION_KEYS = [:include, :fields, :root, :adapter] + + def get_serializer(resource) + @_serializer ||= @_serializer_opts.delete(:serializer) + @_serializer ||= ActiveModel::Serializer.serializer_for(resource) + + if @_serializer_opts.key?(:each_serializer) + @_serializer_opts[:serializer] = @_serializer_opts.delete(:each_serializer) + end + + @_serializer + end + + def use_adapter? + !(@_adapter_opts.key?(:adapter) && !@_adapter_opts[:adapter]) + end [:_render_option_json, :_render_with_renderer_json].each do |renderer_method| define_method renderer_method do |resource, options| - serializer = ActiveModel::Serializer.serializer_for(resource) + @_adapter_opts, @_serializer_opts = + options.partition { |k, _| ADAPTER_OPTION_KEYS.include? k }.map { |h| Hash[h] } - if serializer - adapter_opts, serializer_opts = - options.partition { |k, _| ADAPTER_OPTION_KEYS.include? k } + if use_adapter? && (serializer = get_serializer(resource)) # omg hax - object = serializer.new(resource, Hash[serializer_opts]) - adapter = ActiveModel::Serializer.adapter.new(object, Hash[adapter_opts]) + object = serializer.new(resource, @_serializer_opts) + adapter = ActiveModel::Serializer::Adapter.create(object, @_adapter_opts) super(adapter, options) else super(resource, options) diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index d1e3cdc9..14193333 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -21,7 +21,6 @@ module ActiveModel def self.attributes(*attrs) @_attributes.concat attrs - attrs.each do |attr| define_method attr do object.read_attribute_for_serialization(attr) @@ -29,6 +28,14 @@ module ActiveModel end end + def self.attribute(attr, options = {}) + key = options.fetch(:key, attr) + @_attributes.concat [key] + define_method key do + object.read_attribute_for_serialization(attr) + end unless method_defined?(key) + end + # Defines an association in the object should be rendered. # # The serializer object should implement the association name @@ -83,8 +90,7 @@ module ActiveModel def self.adapter adapter_class = case config.adapter when Symbol - class_name = "ActiveModel::Serializer::Adapter::#{config.adapter.to_s.classify}" - class_name.safe_constantize + ActiveModel::Serializer::Adapter.adapter_class(config.adapter) when Class config.adapter end diff --git a/lib/active_model/serializer/adapter.rb b/lib/active_model/serializer/adapter.rb index 84649285..bf546097 100644 --- a/lib/active_model/serializer/adapter.rb +++ b/lib/active_model/serializer/adapter.rb @@ -20,6 +20,16 @@ module ActiveModel def as_json(options = {}) serializable_hash(options) end + + def self.create(resource, options = {}) + override = options.delete(:adapter) + klass = override ? adapter_class(override) : ActiveModel::Serializer.adapter + klass.new(resource, options) + end + + def self.adapter_class(adapter) + "ActiveModel::Serializer::Adapter::#{adapter.to_s.classify}".safe_constantize + end end end end diff --git a/lib/active_model/serializer/adapter/json_api.rb b/lib/active_model/serializer/adapter/json_api.rb index ea74fc8c..6377ce76 100644 --- a/lib/active_model/serializer/adapter/json_api.rb +++ b/lib/active_model/serializer/adapter/json_api.rb @@ -25,91 +25,129 @@ module ActiveModel else @hash[@root] = attributes_for_serializer(serializer, @options) - serializer.each_association do |name, association, opts| - @hash[@root][:links] ||= {} - - if association.respond_to?(:each) - add_links(name, association, opts) - else - add_link(name, association, opts) - end - end + add_resource_links(@hash[@root], serializer) end @hash end - def add_links(name, serializers, options) - if serializers.first - type = serializers.first.object.class.to_s.underscore.pluralize - end - if name.to_s == type || !type - @hash[@root][:links][name] ||= [] - @hash[@root][:links][name] += serializers.map{|serializer| serializer.id.to_s } - else - @hash[@root][:links][name] ||= {} - @hash[@root][:links][name][:type] = type - @hash[@root][:links][name][:ids] ||= [] - @hash[@root][:links][name][:ids] += serializers.map{|serializer| serializer.id.to_s } - end - - unless serializers.none? || @options[:embed] == :ids - serializers.each do |serializer| - add_linked(name, serializer) - end - end - end - - def add_link(name, serializer, options) - if serializer - type = serializer.object.class.to_s.underscore - if name.to_s == type || !type - @hash[@root][:links][name] = serializer.id.to_s - else - @hash[@root][:links][name] ||= {} - @hash[@root][:links][name][:type] = type - @hash[@root][:links][name][:id] = serializer.id.to_s - end - - unless @options[:embed] == :ids - add_linked(name, serializer) - end - else - @hash[@root][:links][name] = nil - end - end - - def add_linked(resource, serializer, parent = nil) - resource_path = [parent, resource].compact.join('.') - if include_assoc? resource_path - plural_name = resource.to_s.pluralize.to_sym - attrs = attributes_for_serializer(serializer, @options) - @top[:linked] ||= {} - @top[:linked][plural_name] ||= [] - @top[:linked][plural_name].push attrs unless @top[:linked][plural_name].include? attrs - end - - unless serializer.respond_to?(:each) - serializer.each_association do |name, association, opts| - add_linked(name, association, resource) if association - end - end - end - private - def attributes_for_serializer(serializer, options) - if fields = @fieldset && @fieldset.fields_for(serializer) - options[:fields] = fields - end + def add_links(resource, name, serializers) + type = serialized_object_type(serializers) + resource[:links] ||= {} - attributes = serializer.attributes(options) - attributes[:id] = attributes[:id].to_s if attributes[:id] - attributes + if name.to_s == type || !type + resource[:links][name] ||= [] + resource[:links][name] += serializers.map{|serializer| serializer.id.to_s } + else + resource[:links][name] ||= {} + resource[:links][name][:type] = type + resource[:links][name][:ids] ||= [] + resource[:links][name][:ids] += serializers.map{|serializer| serializer.id.to_s } + end end - def include_assoc? assoc - @options[:include] && @options[:include].split(',').include?(assoc.to_s) + def add_link(resource, name, serializer) + resource[:links] ||= {} + resource[:links][name] = nil + + if serializer + type = serialized_object_type(serializer) + if name.to_s == type || !type + resource[:links][name] = serializer.id.to_s + else + resource[:links][name] ||= {} + resource[:links][name][:type] = type + resource[:links][name][:id] = serializer.id.to_s + end + end + end + + def add_linked(resource_name, serializer, parent = nil) + resource_path = [parent, resource_name].compact.join('.') + + if include_assoc?(resource_path) + plural_name = serialized_object_type(serializer).pluralize.to_sym + attrs = [attributes_for_serializer(serializer, @options)].flatten + @top[:linked] ||= {} + @top[:linked][plural_name] ||= [] + + attrs.each do |attrs| + add_resource_links(attrs, serializer, add_linked: false) + + @top[:linked][plural_name].push(attrs) unless @top[:linked][plural_name].include?(attrs) + end + end + + serializer.each_association do |name, association, opts| + add_linked(name, association, resource_path) if association + end if include_nested_assoc? resource_path + end + + + def attributes_for_serializer(serializer, options) + if serializer.respond_to?(:each) + result = [] + serializer.each do |object| + options[:fields] = @fieldset && @fieldset.fields_for(serializer) + attributes = object.attributes(options) + attributes[:id] = attributes[:id].to_s if attributes[:id] + result << attributes + end + else + options[:fields] = @fieldset && @fieldset.fields_for(serializer) + result = serializer.attributes(options) + result[:id] = result[:id].to_s if result[:id] + end + + result + end + + def include_assoc?(assoc) + return false unless @options[:include] + check_assoc("#{assoc}$") + end + + def include_nested_assoc?(assoc) + return false unless @options[:include] + check_assoc("#{assoc}.") + end + + def check_assoc(assoc) + @options[:include].split(',').any? do |s| + s.match(/^#{assoc.gsub('.', '\.')}/) + end + end + + def serialized_object_type(serializer) + return false unless Array(serializer).first + type_name = Array(serializer).first.object.class.to_s.underscore + if serializer.respond_to?(:first) + type_name.pluralize + else + type_name + end + end + + def add_resource_links(attrs, serializer, options = {}) + options[:add_linked] = options.fetch(:add_linked, true) + + Array(serializer).first.each_association do |name, association, opts| + attrs[:links] ||= {} + + if association.respond_to?(:each) + add_links(attrs, name, association) + else + add_link(attrs, name, association) + end + + if @options[:embed] != :ids && options[:add_linked] + Array(association).each do |association| + add_linked(name, association) + end + end + end end end end diff --git a/lib/active_model/serializer/array_serializer.rb b/lib/active_model/serializer/array_serializer.rb index b52a4f51..83693c97 100644 --- a/lib/active_model/serializer/array_serializer.rb +++ b/lib/active_model/serializer/array_serializer.rb @@ -6,7 +6,10 @@ module ActiveModel def initialize(objects, options = {}) @objects = objects.map do |object| - serializer_class = ActiveModel::Serializer.serializer_for(object) + serializer_class = options.fetch( + :serializer, + ActiveModel::Serializer.serializer_for(object) + ) serializer_class.new(object) end end diff --git a/lib/active_model/serializer/fieldset.rb b/lib/active_model/serializer/fieldset.rb index dc3ab857..a09fad18 100644 --- a/lib/active_model/serializer/fieldset.rb +++ b/lib/active_model/serializer/fieldset.rb @@ -2,11 +2,13 @@ module ActiveModel class Serializer class Fieldset - attr_reader :fields, :root - def initialize(fields, root = nil) - @root = root - @fields = parse(fields) + @root = root + @raw_fields = fields + end + + def fields + @fields ||= parsed_fields end def fields_for(serializer) @@ -16,15 +18,17 @@ module ActiveModel private - def parse(fields) - if fields.is_a?(Hash) - fields.inject({}) { |h,(k,v)| h[k.to_sym] = v.map(&:to_sym); h} - elsif fields.is_a?(Array) + attr_reader :raw_fields, :root + + def parsed_fields + if raw_fields.is_a?(Hash) + raw_fields.inject({}) { |h,(k,v)| h[k.to_sym] = v.map(&:to_sym); h} + elsif raw_fields.is_a?(Array) if root.nil? raise ArgumentError, 'The root argument must be specified if the fileds argument is an array.' end hash = {} - hash[root.to_sym] = fields.map(&:to_sym) + hash[root.to_sym] = raw_fields.map(&:to_sym) hash else {} diff --git a/test/action_controller/adapter_selector_test.rb b/test/action_controller/adapter_selector_test.rb new file mode 100644 index 00000000..96d7dd52 --- /dev/null +++ b/test/action_controller/adapter_selector_test.rb @@ -0,0 +1,41 @@ +require 'test_helper' + +module ActionController + module Serialization + class AdapterSelectorTest < ActionController::TestCase + class MyController < ActionController::Base + def render_using_default_adapter + @profile = Profile.new({ name: 'Name 1', description: 'Description 1', comments: 'Comments 1' }) + render json: @profile + end + + def render_using_adapter_override + @profile = Profile.new({ name: 'Name 1', description: 'Description 1', comments: 'Comments 1' }) + render json: @profile, adapter: :json_api + end + + def render_skipping_adapter + @profile = Profile.new({ name: 'Name 1', description: 'Description 1', comments: 'Comments 1' }) + render json: @profile, adapter: false + end + end + + tests MyController + + def test_render_using_default_adapter + get :render_using_default_adapter + assert_equal '{"name":"Name 1","description":"Description 1"}', response.body + end + + def test_render_using_adapter_override + get :render_using_adapter_override + assert_equal '{"profiles":{"name":"Name 1","description":"Description 1"}}', response.body + end + + def test_render_skipping_adapter + get :render_skipping_adapter + assert_equal '{"attributes":{"name":"Name 1","description":"Description 1","comments":"Comments 1"}}', response.body + end + end + end +end diff --git a/test/action_controller/explicit_serializer_test.rb b/test/action_controller/explicit_serializer_test.rb new file mode 100644 index 00000000..cb8b2bd7 --- /dev/null +++ b/test/action_controller/explicit_serializer_test.rb @@ -0,0 +1,78 @@ +require 'test_helper' + +module ActionController + module Serialization + class ExplicitSerializerTest < ActionController::TestCase + class MyController < ActionController::Base + def render_using_explicit_serializer + @profile = Profile.new(name: 'Name 1', + description: 'Description 1', + comments: 'Comments 1') + render json: @profile, serializer: ProfilePreviewSerializer + end + + def render_array_using_explicit_serializer + array = [ + Profile.new(name: 'Name 1', + description: 'Description 1', + comments: 'Comments 1'), + Profile.new(name: 'Name 2', + description: 'Description 2', + comments: 'Comments 2') + ] + render json: array, + serializer: PaginatedSerializer, + each_serializer: ProfilePreviewSerializer + end + + def render_array_using_implicit_serializer + array = [ + Profile.new(name: 'Name 1', + description: 'Description 1', + comments: 'Comments 1'), + Profile.new(name: 'Name 2', + description: 'Description 2', + comments: 'Comments 2') + ] + render json: array, + each_serializer: ProfilePreviewSerializer + end + end + + tests MyController + + def test_render_using_explicit_serializer + get :render_using_explicit_serializer + + assert_equal 'application/json', @response.content_type + assert_equal '{"name":"Name 1"}', @response.body + end + + def test_render_array_using_explicit_serializer + get :render_array_using_explicit_serializer + assert_equal 'application/json', @response.content_type + + expected = { + 'paginated' => [ + { 'name' => 'Name 1' }, + { 'name' => 'Name 2' } + ] + } + + assert_equal expected.to_json, @response.body + end + + def test_render_array_using_explicit_serializer + get :render_array_using_implicit_serializer + assert_equal 'application/json', @response.content_type + + expected = [ + { 'name' => 'Name 1' }, + { 'name' => 'Name 2' } + ] + assert_equal expected.to_json, @response.body + end + + end + end +end diff --git a/test/action_controller/json_api_linked_test.rb b/test/action_controller/json_api_linked_test.rb index f02e90e2..dca55e67 100644 --- a/test/action_controller/json_api_linked_test.rb +++ b/test/action_controller/json_api_linked_test.rb @@ -5,10 +5,18 @@ module ActionController class JsonApiLinkedTest < ActionController::TestCase class MyController < ActionController::Base def setup_post + @role1 = Role.new(id: 1, name: 'admin') + @role2 = Role.new(id: 2, name: 'colab') @author = Author.new(id: 1, name: 'Steve K.') @author.posts = [] + @author.bio = nil + @author.roles = [@role1, @role2] + @role1.author = @author + @role2.author = @author @author2 = Author.new(id: 2, name: 'Anonymous') @author2.posts = [] + @author2.bio = nil + @author2.roles = [] @post = Post.new(id: 1, 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') @@ -20,47 +28,34 @@ module ActionController @second_comment.author = nil end - def with_json_api_adapter - old_adapter = ActiveModel::Serializer.config.adapter - ActiveModel::Serializer.config.adapter = :json_api - yield - ensure - ActiveModel::Serializer.config.adapter = old_adapter - end - def render_resource_without_include - with_json_api_adapter do - setup_post - render json: @post - end + setup_post + render json: @post, adapter: :json_api end def render_resource_with_include - with_json_api_adapter do - setup_post - render json: @post, include: 'author' - end + setup_post + render json: @post, include: 'author', adapter: :json_api end def render_resource_with_nested_include - with_json_api_adapter do - setup_post - render json: @post, include: 'comments.author' - end + setup_post + render json: @post, include: 'comments.author', adapter: :json_api + end + + def render_resource_with_nested_has_many_include + setup_post + render json: @post, include: 'author,author.roles', adapter: :json_api end def render_collection_without_include - with_json_api_adapter do - setup_post - render json: [@post] - end + setup_post + render json: [@post], adapter: :json_api end def render_collection_with_include - with_json_api_adapter do - setup_post - render json: [@post], include: 'author,comments' - end + setup_post + render json: [@post], include: 'author,comments', adapter: :json_api end end @@ -80,6 +75,36 @@ module ActionController assert_equal 'Steve K.', response['linked']['authors'].first['name'] end + def test_render_resource_with_nested_has_many_include + get :render_resource_with_nested_has_many_include + response = JSON.parse(@response.body) + expected_linked = { + "authors" => [{ + "id" => "1", + "name" => "Steve K.", + "links" => { + "posts" => [], + "roles" => ["1", "2"], + "bio" => nil + } + }], + "roles"=>[{ + "id" => "1", + "name" => "admin", + "links" => { + "author" => "1" + } + }, { + "id" => "2", + "name" => "colab", + "links" => { + "author" => "1" + } + }] + } + assert_equal expected_linked, response['linked'] + end + def test_render_resource_with_nested_include get :render_resource_with_nested_include response = JSON.parse(@response.body) diff --git a/test/action_controller/serialization_test.rb b/test/action_controller/serialization_test.rb index fbbb48f9..a4c20a54 100644 --- a/test/action_controller/serialization_test.rb +++ b/test/action_controller/serialization_test.rb @@ -25,13 +25,9 @@ module ActionController end def render_using_custom_root_in_adapter_with_a_default - old_adapter = ActiveModel::Serializer.config.adapter # JSON-API adapter sets root by default - ActiveModel::Serializer.config.adapter = ActiveModel::Serializer::Adapter::JsonApi @profile = Profile.new({ name: 'Name 1', description: 'Description 1', comments: 'Comments 1' }) - render json: @profile, root: "profile" - ensure - ActiveModel::Serializer.config.adapter = old_adapter + render json: @profile, root: "profile", adapter: :json_api end def render_array_using_implicit_serializer diff --git a/test/adapter/json_api/belongs_to_test.rb b/test/adapter/json_api/belongs_to_test.rb index 048b55b7..578896b1 100644 --- a/test/adapter/json_api/belongs_to_test.rb +++ b/test/adapter/json_api/belongs_to_test.rb @@ -7,6 +7,8 @@ module ActiveModel class BelongsToTest < Minitest::Test def setup @author = Author.new(id: 1, name: 'Steve K.') + @author.bio = nil + @author.roles = [] @post = Post.new(id: 42, title: 'New Post', body: 'Body') @anonymous_post = Post.new(id: 43, title: 'Hello!!', body: 'Hello, world!!') @comment = Comment.new(id: 1, body: 'ZOMG A COMMENT') @@ -31,12 +33,28 @@ module ActiveModel def test_includes_linked_post @adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer, include: 'post') - assert_equal([{id: "42", title: 'New Post', body: 'Body'}], @adapter.serializable_hash[:linked][:posts]) + expected = [{ + id: "42", + title: 'New Post', + body: 'Body', + links: { + comments: ["1"], + author: "1" + } + }] + assert_equal expected, @adapter.serializable_hash[:linked][:posts] end def test_limiting_linked_post_fields @adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer, include: 'post', fields: {post: [:title]}) - assert_equal([{title: 'New Post'}], @adapter.serializable_hash[:linked][:posts]) + expected = [{ + title: 'New Post', + links: { + comments: ["1"], + author: "1" + } + }] + assert_equal expected, @adapter.serializable_hash[:linked][:posts] end def test_include_nil_author @@ -49,7 +67,53 @@ module ActiveModel def test_include_type_for_association_when_is_different_than_name serializer = BlogSerializer.new(@blog) adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer) - assert_equal({type: "author", id: "1"}, adapter.serializable_hash[:blogs][:links][:writer]) + links = adapter.serializable_hash[:blogs][:links] + expected = { + writer: { + type: "author", + id: "1" + }, + articles: { + type: "posts", + ids: ["42", "43"] + } + } + assert_equal expected, links + end + + def test_include_linked_resources_with_type_name + serializer = BlogSerializer.new(@blog) + adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer, include: "writer,articles") + linked = adapter.serializable_hash[:linked] + expected = { + authors: [{ + id: "1", + name: "Steve K.", + links: { + posts: [], + roles: [], + bio: nil + } + }], + posts: [{ + title: "New Post", + body: "Body", + id: "42", + links: { + comments: ["1"], + author: "1" + } + }, { + title: "Hello!!", + body: "Hello, world!!", + id: "43", + links: { + comments: [], + author: nil + } + }] + } + assert_equal expected, linked end end end diff --git a/test/adapter/json_api/collection_test.rb b/test/adapter/json_api/collection_test.rb index acea7790..205eaf21 100644 --- a/test/adapter/json_api/collection_test.rb +++ b/test/adapter/json_api/collection_test.rb @@ -7,6 +7,7 @@ module ActiveModel class CollectionTest < Minitest::Test def setup @author = Author.new(id: 1, name: 'Steve K.') + @author.bio = nil @first_post = Post.new(id: 1, title: 'Hello!!', body: 'Hello, world!!') @second_post = Post.new(id: 2, title: 'New Post', body: 'Body') @first_post.comments = [] diff --git a/test/adapter/json_api/has_many_embed_ids_test.rb b/test/adapter/json_api/has_many_embed_ids_test.rb index 4690e3c6..a6072aa4 100644 --- a/test/adapter/json_api/has_many_embed_ids_test.rb +++ b/test/adapter/json_api/has_many_embed_ids_test.rb @@ -7,6 +7,8 @@ module ActiveModel class HasManyEmbedIdsTest < Minitest::Test def setup @author = Author.new(name: 'Steve K.') + @author.bio = nil + @author.roles = nil @first_post = Post.new(id: 1, title: 'Hello!!', body: 'Hello, world!!') @second_post = Post.new(id: 2, title: 'New Post', body: 'Body') @author.posts = [@first_post, @second_post] diff --git a/test/adapter/json_api/has_many_test.rb b/test/adapter/json_api/has_many_test.rb index e4ce2f62..c824d847 100644 --- a/test/adapter/json_api/has_many_test.rb +++ b/test/adapter/json_api/has_many_test.rb @@ -8,6 +8,7 @@ module ActiveModel def setup @author = Author.new(id: 1, name: 'Steve K.') @author.posts = [] + @author.bio = nil @post = Post.new(id: 1, title: 'New Post', body: 'Body') @post_without_comments = Post.new(id: 2, title: 'Second Post', body: 'Second') @first_comment = Comment.new(id: 1, body: 'ZOMG A COMMENT') @@ -31,21 +32,43 @@ module ActiveModel def test_includes_comment_ids assert_equal(["1", "2"], @adapter.serializable_hash[:posts][:links][:comments]) end - + def test_includes_linked_comments @adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer, include: 'comments') - assert_equal([ - {id: "1", body: 'ZOMG A COMMENT'}, - {id: "2", body: 'ZOMG ANOTHER COMMENT'} - ], @adapter.serializable_hash[:linked][:comments]) + expected = [{ + id: "1", + body: 'ZOMG A COMMENT', + links: { + post: "1", + author: nil + } + }, { + id: "2", + body: 'ZOMG ANOTHER COMMENT', + links: { + post: "1", + author: nil + } + }] + assert_equal expected, @adapter.serializable_hash[:linked][:comments] end def test_limit_fields_of_linked_comments - @adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer, include: 'comments', fields: {comment: [:body]}) - assert_equal([ - {body: 'ZOMG A COMMENT'}, - {body: 'ZOMG ANOTHER COMMENT'} - ], @adapter.serializable_hash[:linked][:comments]) + @adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer, include: 'comments', fields: {comment: [:id]}) + expected = [{ + id: "1", + links: { + post: "1", + author: nil + } + }, { + id: "2", + links: { + post: "1", + author: nil + } + }] + assert_equal expected, @adapter.serializable_hash[:linked][:comments] end def test_no_include_linked_if_comments_is_empty diff --git a/test/adapter/json_api/linked_test.rb b/test/adapter/json_api/linked_test.rb index 160d3daa..d25cfa06 100644 --- a/test/adapter/json_api/linked_test.rb +++ b/test/adapter/json_api/linked_test.rb @@ -7,6 +7,7 @@ module ActiveModel class LinkedTest < Minitest::Test def setup @author = Author.new(id: 1, name: 'Steve K.') + @bio = Bio.new(id: 1, content: 'AMS Contributor') @first_post = Post.new(id: 1, title: 'Hello!!', body: 'Hello, world!!') @second_post = Post.new(id: 2, title: 'New Post', body: 'Body') @first_post.comments = [] @@ -14,9 +15,12 @@ module ActiveModel @first_post.author = @author @second_post.author = @author @author.posts = [@first_post, @second_post] + @author.bio = @bio + @author.roles = [] + @bio.author = @author @serializer = ArraySerializer.new([@first_post, @second_post]) - @adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer, include: 'author,comments') + @adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer, include: 'author,author.bio,comments') end def test_include_multiple_posts_and_linked @@ -31,7 +35,42 @@ module ActiveModel { title: "Hello!!", body: "Hello, world!!", id: "1", links: { comments: ['1', '2'], author: "1" } }, { title: "New Post", body: "Body", id: "2", links: { comments: [], :author => "1" } } ], @adapter.serializable_hash[:posts]) - assert_equal({ :comments => [{ :id => "1", :body => "ZOMG A COMMENT" }, { :id => "2", :body => "ZOMG ANOTHER COMMENT" }], :authors => [{ :id => "1", :name => "Steve K." }] }, @adapter.serializable_hash[:linked]) + + + expected = { + comments: [{ + id: "1", + body: "ZOMG A COMMENT", + links: { + post: "1", + author: nil + } + }, { + id: "2", + body: "ZOMG ANOTHER COMMENT", + links: { + post: "1", + author: nil + } + }], + authors: [{ + id: "1", + name: "Steve K.", + links: { + posts: ["1", "2"], + roles: [], + bio: "1" + } + }], + bios: [{ + id: "1", + content: "AMS Contributor", + links: { + author: "1" + } + }] + } + assert_equal expected, @adapter.serializable_hash[:linked] end end end diff --git a/test/adapter_test.rb b/test/adapter_test.rb index 737e0c4e..d76559d5 100644 --- a/test/adapter_test.rb +++ b/test/adapter_test.rb @@ -18,6 +18,26 @@ module ActiveModel 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::Json, adapter.class + end + + def test_create_adapter_with_override + adapter = ActiveModel::Serializer::Adapter.create(@serializer, { adapter: :json_api}) + assert_equal ActiveModel::Serializer::Adapter::JsonApi, adapter.class + end end end end diff --git a/test/fixtures/poro.rb b/test/fixtures/poro.rb index 7d8d57b4..f7c1becb 100644 --- a/test/fixtures/poro.rb +++ b/test/fixtures/poro.rb @@ -35,10 +35,18 @@ class ProfileSerializer < ActiveModel::Serializer urls :posts, :comments end +class ProfilePreviewSerializer < ActiveModel::Serializer + attributes :name + + urls :posts, :comments +end + Post = Class.new(Model) Comment = Class.new(Model) Author = Class.new(Model) +Bio = Class.new(Model) Blog = Class.new(Model) +Role = Class.new(Model) PostSerializer = Class.new(ActiveModel::Serializer) do attributes :title, :body, :id @@ -59,6 +67,20 @@ AuthorSerializer = Class.new(ActiveModel::Serializer) do attributes :id, :name has_many :posts, embed: :ids + has_many :roles, embed: :ids + belongs_to :bio +end + +RoleSerializer = Class.new(ActiveModel::Serializer) do + attributes :id, :name + + belongs_to :author +end + +BioSerializer = Class.new(ActiveModel::Serializer) do + attributes :id, :content + + belongs_to :author end BlogSerializer = Class.new(ActiveModel::Serializer) do @@ -67,3 +89,14 @@ BlogSerializer = Class.new(ActiveModel::Serializer) do belongs_to :writer has_many :articles end + +PaginatedSerializer = Class.new(ActiveModel::Serializer::ArraySerializer) do + def json_key + 'paginated' + end +end + +AlternateBlogSerializer = Class.new(ActiveModel::Serializer) do + attribute :id + attribute :name, key: :title +end diff --git a/test/serializers/associations_test.rb b/test/serializers/associations_test.rb index 99162acc..b2278b45 100644 --- a/test/serializers/associations_test.rb +++ b/test/serializers/associations_test.rb @@ -26,6 +26,8 @@ module ActiveModel def setup @author = Author.new(name: 'Steve K.') + @author.bio = nil + @author.roles = [] @post = Post.new({ title: 'New Post', body: 'Body' }) @comment = Comment.new({ id: 1, body: 'ZOMG A COMMENT' }) @post.comments = [@comment] @@ -39,11 +41,25 @@ module ActiveModel end def test_has_many - assert_equal({posts: {type: :has_many, options: {embed: :ids}}}, @author_serializer.class._associations) + assert_equal( + { posts: { type: :has_many, options: { embed: :ids } }, + roles: { type: :has_many, options: { embed: :ids } }, + bio: { type: :belongs_to, options: {} } }, + @author_serializer.class._associations + ) @author_serializer.each_association do |name, serializer, options| - assert_equal(:posts, name) - assert_equal({embed: :ids}, options) - assert_kind_of(ActiveModel::Serializer.config.array_serializer, serializer) + if name == :posts + assert_equal({embed: :ids}, options) + assert_kind_of(ActiveModel::Serializer.config.array_serializer, serializer) + elsif name == :bio + assert_equal({}, options) + assert_nil serializer + elsif name == :roles + assert_equal({embed: :ids}, options) + assert_kind_of(ActiveModel::Serializer.config.array_serializer, serializer) + else + flunk "Unknown association: #{name}" + end end end diff --git a/test/serializers/attribute_test.rb b/test/serializers/attribute_test.rb new file mode 100644 index 00000000..35f27ee4 --- /dev/null +++ b/test/serializers/attribute_test.rb @@ -0,0 +1,23 @@ +require 'test_helper' + +module ActiveModel + class Serializer + class AttributeTest < Minitest::Test + def setup + @blog = Blog.new({ id: 1, name: 'AMS Hints' }) + @blog_serializer = AlternateBlogSerializer.new(@blog) + end + + def test_attributes_definition + assert_equal([:id, :title], + @blog_serializer.class._attributes) + end + + def test_json_serializable_hash + adapter = ActiveModel::Serializer::Adapter::Json.new(@blog_serializer) + assert_equal({:id=>1, :title=>"AMS Hints"}, adapter.serializable_hash) + end + end + end +end + diff --git a/test/serializers/configuration_test.rb b/test/serializers/configuration_test.rb index eec00436..9c6c5fea 100644 --- a/test/serializers/configuration_test.rb +++ b/test/serializers/configuration_test.rb @@ -7,7 +7,7 @@ module ActiveModel assert_equal ActiveModel::Serializer::ArraySerializer, ActiveModel::Serializer.config.array_serializer end - def test_adapter + def test_default_adapter assert_equal :json, ActiveModel::Serializer.config.adapter end end