diff --git a/CHANGELOG.markdown b/CHANGELOG.markdown index 538d174d45..8428c00bac 100644 --- a/CHANGELOG.markdown +++ b/CHANGELOG.markdown @@ -1,3 +1,7 @@ +Next Release +================= +* [#181](https://github.com/intridea/grape/pull/181): Fix: Corrected JSON serialization of nested hashes containing `Grape::Entity` instances - [@benrosenblum](https://github.com/benrosenblum). + 0.2.1 (7/11/2012) ================= diff --git a/lib/grape/entity.rb b/lib/grape/entity.rb index 890cf83849..a505005e1c 100644 --- a/lib/grape/entity.rb +++ b/lib/grape/entity.rb @@ -259,7 +259,17 @@ def serializable_hash(runtime_options = {}) return nil if object.nil? opts = options.merge(runtime_options || {}) exposures.inject({}) do |output, (attribute, exposure_options)| - output[key_for(attribute)] = value_for(attribute, opts) if conditions_met?(exposure_options, opts) + partial_output = value_for(attribute, opts) if conditions_met?(exposure_options, opts) + + output[key_for(attribute)] = + if partial_output.respond_to? :serializable_hash + partial_output.serializable_hash(runtime_options) + elsif partial_output.kind_of?(Array) && !partial_output.map {|o| o.respond_to? :serializable_hash}.include?(false) + partial_output.map {|o| o.serializable_hash} + else + partial_output + end + output end end diff --git a/lib/grape/middleware/base.rb b/lib/grape/middleware/base.rb index be7de38648..9413c3c593 100644 --- a/lib/grape/middleware/base.rb +++ b/lib/grape/middleware/base.rb @@ -110,20 +110,32 @@ def decode_json(object) MultiJson.load(object) end - def encode_json(object) - return object if object.is_a?(String) + def serializable?(object) + object.respond_to?(:serializable_hash) || + object.kind_of?(Array) && !object.map {|o| o.respond_to? :serializable_hash }.include?(false) || + object.kind_of?(Hash) + end + def serialize(object) if object.respond_to? :serializable_hash - MultiJson.dump(object.serializable_hash) + object.serializable_hash elsif object.kind_of?(Array) && !object.map {|o| o.respond_to? :serializable_hash }.include?(false) - MultiJson.dump(object.map {|o| o.serializable_hash }) - elsif object.respond_to? :to_json - object.to_json + object.map {|o| o.serializable_hash } + elsif object.kind_of?(Hash) + object.inject({}) { |h,(k,v)| h[k] = serialize(v); h } else - MultiJson.dump(object) + object end end + def encode_json(object) + return object if object.is_a?(String) + return MultiJson.dump(serialize(object)) if serializable?(object) + return object.to_json if object.respond_to?(:to_json) + + MultiJson.dump(object) + end + def encode_txt(object) object.respond_to?(:to_txt) ? object.to_txt : object.to_s end diff --git a/spec/grape/entity_spec.rb b/spec/grape/entity_spec.rb index 305806724d..fe241d39ac 100644 --- a/spec/grape/entity_spec.rb +++ b/spec/grape/entity_spec.rb @@ -257,6 +257,50 @@ fresh_class.expose :name expect{ fresh_class.new(nil).serializable_hash }.not_to raise_error end + + it 'should serialize embedded objects which respond to #serializable_hash' do + class EmbeddedExample + def serializable_hash(opts = {}) + {:abc => 'def'} + end + end + + class SimpleExample + def name + "abc" + end + + def embedded + EmbeddedExample.new + end + end + + fresh_class.expose :name, :embedded + presenter = fresh_class.new(SimpleExample.new) + presenter.serializable_hash.should == {:name => "abc", :embedded => {:abc => "def"}} + end + + it 'should serialize embedded arrays of objects which respond to #serializable_hash' do + class EmbeddedExample + def serializable_hash(opts = {}) + {:abc => 'def'} + end + end + + class SimpleExample + def name + "abc" + end + + def embedded + [EmbeddedExample.new, EmbeddedExample.new] + end + end + + fresh_class.expose :name, :embedded + presenter = fresh_class.new(SimpleExample.new) + presenter.serializable_hash.should == {:name => "abc", :embedded => [{:abc => "def"}, {:abc => "def"}]} + end end describe '#value_for' do diff --git a/spec/grape/middleware/formatter_spec.rb b/spec/grape/middleware/formatter_spec.rb index 4461b1ff77..fa58a4dc0d 100644 --- a/spec/grape/middleware/formatter_spec.rb +++ b/spec/grape/middleware/formatter_spec.rb @@ -48,6 +48,18 @@ def serializable_hash subject.call({'PATH_INFO' => '/somewhere', 'HTTP_ACCEPT' => 'application/json'}).last.each{|b| b.should == '{"abc":"def"}'} end + it 'should serialize objects that respond to #serializable_hash if there is a root element' do + class SimpleExample + def serializable_hash + {:abc => 'def'} + end + end + + @body = {"root" => SimpleExample.new} + + subject.call({'PATH_INFO' => '/somewhere', 'HTTP_ACCEPT' => 'application/json'}).last.each{|b| b.should == '{"root":{"abc":"def"}}'} + end + it 'should call #to_xml if the content type is xml' do @body = "string" @body.instance_eval do