diff --git a/lib/action_controller/serialization.rb b/lib/action_controller/serialization.rb index 54f321f0..c19d1ea3 100644 --- a/lib/action_controller/serialization.rb +++ b/lib/action_controller/serialization.rb @@ -30,6 +30,12 @@ module ActionController included do class_attribute :_serialization_scope self._serialization_scope = :current_user + self.responder = ActiveModel::Serializer::Responder + self.respond_to :json + + unless ActiveModel::Serializer.use_default_render_json + self.send(:include, RenderJsonOverride) + end end def serialization_scope @@ -39,31 +45,33 @@ module ActionController def default_serializer_options end - def _render_option_json(json, options) - options = default_serializer_options.merge(options) if default_serializer_options + module RenderJsonOverride + def _render_option_json(json, options) + options = default_serializer_options.merge(options) if default_serializer_options - serializer = options.delete(:serializer) || - (json.respond_to?(:active_model_serializer) && json.active_model_serializer) + serializer = options.delete(:serializer) || + (json.respond_to?(:active_model_serializer) && json.active_model_serializer) - if json.respond_to?(:to_ary) - unless serializer <= ActiveModel::ArraySerializer - raise ArgumentError.new("#{serializer.name} is not an ArraySerializer. " + - "You may want to use the :each_serializer option instead.") + if json.respond_to?(:to_ary) + unless serializer <= ActiveModel::ArraySerializer + raise ArgumentError.new("#{serializer.name} is not an ArraySerializer. " + + "You may want to use the :each_serializer option instead.") + end + + if options[:root] != false && serializer.root != false + # default root element for arrays is serializer's root or the controller name + # the serializer for an Array is ActiveModel::ArraySerializer + options[:root] ||= serializer.root || controller_name + end end - if options[:root] != false && serializer.root != false - # default root element for arrays is serializer's root or the controller name - # the serializer for an Array is ActiveModel::ArraySerializer - options[:root] ||= serializer.root || controller_name + if serializer + options[:scope] = serialization_scope unless options.has_key?(:scope) + options[:url_options] = url_options + json = serializer.new(json, options) end + super end - - if serializer - options[:scope] = serialization_scope unless options.has_key?(:scope) - options[:url_options] = url_options - json = serializer.new(json, options) - end - super end module ClassMethods diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index ee5e917c..560f7d2e 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -66,6 +66,9 @@ module ActiveModel self._embed = :objects class_attribute :_root_embed + class_attribute :use_default_render_json + self.use_default_render_json = false + class << self # Define attributes to be used in the serialization. def attributes(*attrs) diff --git a/lib/active_model/serializer/responder.rb b/lib/active_model/serializer/responder.rb new file mode 100644 index 00000000..caa7dfda --- /dev/null +++ b/lib/active_model/serializer/responder.rb @@ -0,0 +1,43 @@ +module ActiveModel + class Serializer + class Responder < ::ActionController::Responder #:nodoc: + attr_reader :serializer + + protected + def display(resource, given_options = {}) + if format != :json + super + else + default_options = controller.send(:default_serializer_options) + options = self.options.reverse_merge(default_options || {}) + + serializer = options[:serializer] || + (resource.respond_to?(:active_model_serializer) && + resource.active_model_serializer) + + if resource.respond_to?(:to_ary) + unless serializer <= ActiveModel::ArraySerializer + raise ArgumentError.new("#{serializer.name} is not an ArraySerializer. " + + "You may want to use the :each_serializer option instead.") + end + + if options[:root] != false && serializer.root != false + # default root element for arrays is serializer's root or the controller name + # the serializer for an Array is ActiveModel::ArraySerializer + options[:root] ||= serializer.root || controller.send(:controller_name) + end + end + + if serializer + serialization_scope = controller.send(:serialization_scope) + options[:scope] = serialization_scope unless options.has_key?(:scope) + options[:url_options] = controller.send(:url_options) + render(given_options.merge(:json => serializer.new(resource, options))) + else + super + end + end + end + end + end +end \ No newline at end of file diff --git a/lib/active_model_serializers.rb b/lib/active_model_serializers.rb index d27eb4b7..3abb9c1e 100644 --- a/lib/active_model_serializers.rb +++ b/lib/active_model_serializers.rb @@ -76,6 +76,7 @@ begin require 'action_controller/serialization' ActiveSupport.on_load(:action_controller) do + require 'active_model/serializer/responder' include ::ActionController::Serialization end rescue LoadError => ex diff --git a/test/no_serialization_scope_test.rb b/test/no_serialization_scope_test.rb index 5a109cc0..b533cc40 100644 --- a/test/no_serialization_scope_test.rb +++ b/test/no_serialization_scope_test.rb @@ -21,14 +21,14 @@ class NoSerializationScopeTest < ActionController::TestCase serialization_scope nil def index - render :json => ScopeSerializable.new + respond_with(ScopeSerializable.new) end end tests NoSerializationScopeController def test_disabled_serialization_scope - get :index + get :index, :format => :json assert_equal '{"scope":null}', @response.body end end diff --git a/test/responder_test.rb b/test/responder_test.rb new file mode 100644 index 00000000..0a28e9af --- /dev/null +++ b/test/responder_test.rb @@ -0,0 +1,383 @@ +require 'test_helper' +require 'pathname' + +class ResponderTest < ActionController::TestCase + class JsonRenderable + def as_json(options={}) + hash = { :a => :b, :c => :d, :e => :f } + hash.except!(*options[:except]) if options[:except] + hash + end + + def to_json(options = {}) + super :except => [:c, :e] + end + end + + class JsonSerializer + def initialize(object, options={}) + @object, @options = object, options + end + + def as_json(*) + hash = { :object => serializable_hash, :scope => @options[:scope].as_json } + hash.merge!(:options => true) if @options[:options] + hash.merge!(:check_defaults => true) if @options[:check_defaults] + hash + end + + def serializable_hash + @object.as_json + end + end + + class JsonSerializable + def initialize(skip=false) + @skip = skip + end + + def active_model_serializer + JsonSerializer unless @skip + end + + def as_json(*) + { :serializable_object => true } + end + end + + class CustomSerializer + def initialize(*) + end + + def as_json(*) + { :hello => true } + end + end + + class AnotherCustomSerializer + def initialize(*) + end + + def as_json(*) + { :rails => 'rocks' } + end + end + + class HypermediaSerializable + def active_model_serializer + HypermediaSerializer + end + end + + class HypermediaSerializer < ActiveModel::Serializer + def as_json(*) + { :link => hypermedia_url } + end + end + + class CustomArraySerializer < ActiveModel::ArraySerializer + self.root = "items" + end + + class TestController < ActionController::Base + protect_from_forgery + + serialization_scope :current_user + attr_reader :current_user + before_filter do + request.format = :json + end + + def self.controller_path + 'test' + end + + def render_json_nil + respond_with(nil) + end + + def render_json_render_to_string + respond_with render_to_string(:json => '[]') + end + + def render_json_hello_world + respond_with ActiveSupport::JSON.encode(:hello => 'world') + end + + def render_json_hello_world_with_status + respond_with ActiveSupport::JSON.encode(:hello => 'world'), :status => 401 + end + + def render_json_hello_world_with_callback + respond_with ActiveSupport::JSON.encode(:hello => 'world'), :callback => 'alert' + end + + def render_json_with_custom_content_type + respond_with ActiveSupport::JSON.encode(:hello => 'world'), :content_type => 'text/javascript' + end + + def render_symbol_json + respond_with ActiveSupport::JSON.encode(:hello => 'world') + end + + def render_json_with_extra_options + respond_with JsonRenderable.new, :except => [:c, :e] + end + + def render_json_without_options + respond_with JsonRenderable.new + end + + def render_json_with_serializer + @current_user = Struct.new(:as_json).new(:current_user => true) + respond_with JsonSerializable.new + end + + def render_json_with_serializer_and_implicit_root + @current_user = Struct.new(:as_json).new(:current_user => true) + respond_with [JsonSerializable.new] + end + + def render_json_with_serializer_and_options + @current_user = Struct.new(:as_json).new(:current_user => true) + respond_with JsonSerializable.new, :options => true + end + + def render_json_with_serializer_and_scope_option + @current_user = Struct.new(:as_json).new(:current_user => true) + scope = Struct.new(:as_json).new(:current_user => false) + respond_with JsonSerializable.new, :scope => scope + end + + def render_json_with_serializer_api_but_without_serializer + @current_user = Struct.new(:as_json).new(:current_user => true) + respond_with JsonSerializable.new(true) + end + + # To specify a custom serializer for an object, use :serializer. + def render_json_with_custom_serializer + respond_with Object.new, :serializer => CustomSerializer + end + + # To specify a custom serializer for each item in the Array, use :each_serializer. + def render_json_array_with_custom_serializer + respond_with [Object.new], :each_serializer => CustomSerializer + end + + def render_json_array_with_wrong_option + respond_with [Object.new], :serializer => CustomSerializer + end + + def render_json_with_links + respond_with HypermediaSerializable.new + end + + def render_json_array_with_no_root + respond_with [], :root => false + end + + def render_json_empty_array + respond_with [] + end + + def render_json_array_with_custom_array_serializer + respond_with [], :serializer => CustomArraySerializer + end + + + private + def default_serializer_options + defaults = {} + defaults.merge!(:check_defaults => true) if params[:check_defaults] + defaults.merge!(:root => :awesome) if params[:check_default_root] + defaults.merge!(:scope => :current_admin) if params[:check_default_scope] + defaults.merge!(:serializer => AnotherCustomSerializer) if params[:check_default_serializer] + defaults.merge!(:each_serializer => AnotherCustomSerializer) if params[:check_default_each_serializer] + defaults + end + end + + tests TestController + + def setup + # enable a logger so that (e.g.) the benchmarking stuff runs, so we can get + # a more accurate simulation of what happens in "real life". + super + @controller.logger = Logger.new(nil) + + @request.host = "www.nextangle.com" + end + + def test_render_json_nil + get :render_json_nil + assert_equal 'null', @response.body + assert_equal 'application/json', @response.content_type + end + + def test_render_json_render_to_string + get :render_json_render_to_string + assert_equal '[]', @response.body + end + + def test_render_json + get :render_json_hello_world + assert_equal '{"hello":"world"}', @response.body + assert_equal 'application/json', @response.content_type + end + + def test_render_json_with_status + get :render_json_hello_world_with_status + assert_equal '{"hello":"world"}', @response.body + assert_equal 401, @response.status + end + + def test_render_json_with_callback + get :render_json_hello_world_with_callback + assert_equal 'alert({"hello":"world"})', @response.body + # For JSONP, Rails 3 uses application/json, but Rails 4 uses text/javascript + assert_match %r(application/json|text/javascript), @response.content_type.to_s + end + + def test_render_json_with_custom_content_type + get :render_json_with_custom_content_type + assert_equal '{"hello":"world"}', @response.body + assert_equal 'text/javascript', @response.content_type + end + + def test_render_symbol_json + get :render_symbol_json + assert_equal '{"hello":"world"}', @response.body + assert_equal 'application/json', @response.content_type + end + + def test_render_json_forwards_extra_options + get :render_json_with_extra_options + assert_equal '{"a":"b"}', @response.body + assert_equal 'application/json', @response.content_type + end + + def test_render_json_calls_to_json_from_object + get :render_json_without_options + assert_equal '{"a":"b"}', @response.body + end + + def test_render_json_with_serializer + get :render_json_with_serializer + assert_match '"scope":{"current_user":true}', @response.body + assert_match '"object":{"serializable_object":true}', @response.body + end + + def test_render_json_with_serializer_checking_defaults + get :render_json_with_serializer, :check_defaults => true + assert_match '"scope":{"current_user":true}', @response.body + assert_match '"object":{"serializable_object":true}', @response.body + assert_match '"check_defaults":true', @response.body + end + + def test_render_json_with_serializer_checking_default_serailizer + get :render_json_with_serializer, :check_default_serializer => true + assert_match '{"rails":"rocks"}', @response.body + end + + def test_render_json_with_serializer_checking_default_scope + get :render_json_with_serializer, :check_default_scope => true + assert_match '"scope":"current_admin"', @response.body + end + + def test_render_json_with_serializer_and_implicit_root + get :render_json_with_serializer_and_implicit_root + assert_match '"test":[{"serializable_object":true}]', @response.body + end + + def test_render_json_with_serializer_and_implicit_root_checking_default_each_serailizer + get :render_json_with_serializer_and_implicit_root, :check_default_each_serializer => true + assert_match '"test":[{"rails":"rocks"}]', @response.body + end + + def test_render_json_with_serializer_and_options + get :render_json_with_serializer_and_options + assert_match '"scope":{"current_user":true}', @response.body + assert_match '"object":{"serializable_object":true}', @response.body + assert_match '"options":true', @response.body + end + + def test_render_json_with_serializer_and_scope_option + get :render_json_with_serializer_and_scope_option + assert_match '"scope":{"current_user":false}', @response.body + end + + def test_render_json_with_serializer_and_scope_option_checking_default_scope + get :render_json_with_serializer_and_scope_option, :check_default_scope => true + assert_match '"scope":{"current_user":false}', @response.body + end + + def test_render_json_with_serializer_api_but_without_serializer + get :render_json_with_serializer_api_but_without_serializer + assert_match '{"serializable_object":true}', @response.body + end + + def test_render_json_with_custom_serializer + get :render_json_with_custom_serializer + assert_match '{"hello":true}', @response.body + end + + def test_render_json_with_custom_serializer_checking_default_serailizer + get :render_json_with_custom_serializer, :check_default_serializer => true + assert_match '{"hello":true}', @response.body + end + + def test_render_json_array_with_custom_serializer + get :render_json_array_with_custom_serializer + assert_match '{"test":[{"hello":true}]}', @response.body + end + + def test_render_json_array_with_wrong_option + assert_raise ArgumentError do + get :render_json_array_with_wrong_option + end + end + + def test_render_json_array_with_custom_serializer_checking_default_each_serailizer + get :render_json_array_with_custom_serializer, :check_default_each_serializer => true + assert_match '{"test":[{"hello":true}]}', @response.body + end + + def test_render_json_with_links + get :render_json_with_links + assert_match '{"link":"http://www.nextangle.com/hypermedia"}', @response.body + end + + def test_render_json_array_with_no_root + get :render_json_array_with_no_root + assert_equal '[]', @response.body + end + + def test_render_json_array_with_no_root_checking_default_root + get :render_json_array_with_no_root, :check_default_root => true + assert_equal '[]', @response.body + end + + def test_render_json_empty_array + get :render_json_empty_array + assert_equal '{"test":[]}', @response.body + end + + def test_render_json_empty_array_checking_default_root + get :render_json_empty_array, :check_default_root => true + assert_equal '{"awesome":[]}', @response.body + end + + def test_render_json_empty_arry_with_array_serializer_root_false + ActiveModel::ArraySerializer.root = false + get :render_json_empty_array + assert_equal '[]', @response.body + ensure # teardown + ActiveModel::ArraySerializer.root = nil + end + + def test_render_json_array_with_custom_array_serializer + get :render_json_array_with_custom_array_serializer + assert_equal '{"items":[]}', @response.body + end + +end diff --git a/test/serialization_test.rb b/test/serialization_test.rb index 936ee113..f41b6c76 100644 --- a/test/serialization_test.rb +++ b/test/serialization_test.rb @@ -84,6 +84,7 @@ class RenderJsonTest < ActionController::TestCase end class TestController < ActionController::Base + include ::ActionController::Serialization::RenderJsonOverride protect_from_forgery serialization_scope :current_user diff --git a/test/test_helper.rb b/test/test_helper.rb index c123764c..7b4335f4 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -15,6 +15,9 @@ require "active_model_serializers" require "active_support/json" require "test/unit" +# Manually include RenderJsonOverride where needed +ActiveModel::Serializer.use_default_render_json = true + require 'rails' module TestHelper