Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix json serialization of nested hash when using root elements with a Grape::Entity presenter #181

Merged
merged 8 commits into from
Jul 17, 2012
4 changes: 4 additions & 0 deletions CHANGELOG.markdown
Original file line number Diff line number Diff line change
@@ -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)
=================

Expand Down
12 changes: 11 additions & 1 deletion lib/grape/entity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 19 additions & 7 deletions lib/grape/middleware/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
44 changes: 44 additions & 0 deletions spec/grape/entity_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should know that these definitions become global in Ruby, and notably pollute other tests. Either use subject that gets cleared every time or move the classes into spec/support with a better defined name.

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
Expand Down
12 changes: 12 additions & 0 deletions spec/grape/middleware/formatter_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down