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

Already on GitHub? Sign in to your account

Feature/recursive to hash #84

Closed
wants to merge 5 commits into
from
@@ -112,24 +112,41 @@ def attributes=(attributes)
set_attributes(attributes)
end
- # Returns a hash of all publicly accessible attributes
+ # Returns a hash of all publicly accessible attributes by
+ # recursively calling #to_hash on the objects that respond to it.
#
# @example
- # class User
+ # class Person
# include Virtus
#
- # attribute :name, String
- # attribute :age, Integer
+ # attribute :name, String
+ # attribute :age, Integer
+ # attribute :email, String, :accessor => :private
+ #
+ # attribute :friend, Person
# end
#
- # user = User.new(:name => 'John', :age => 28)
- # user.attributes # => { :name => 'John', :age => 28 }
+ # john = Person.new({ :name => 'John', :age => 28 })
+ # jack = Person.new({ :name => 'Jack', :age => 31, friend => john })
+ #
+ # user.to_hash # => { :name => 'John', :age => 28, :friend => { :name => 'Jack', :age => 31 } }
#
# @return [Hash]
#
# @api public
def to_hash
- attributes
+ hash = attributes.dup
+ hash.each do |key, value|
+ case
+ when value.is_a?(Array)
+ hash[key] = value.collect do |item_within_value|
+ safely_recurse_into(item_within_value) { |i| i.respond_to?(:to_hash) ? i.to_hash : i }
+ end
+ when value.respond_to?(:to_hash)
+ hash[key] = safely_recurse_into(value) do |v| v.to_hash end
+ end
+ end
+ hash
end
private
@@ -181,5 +198,27 @@ def set_attribute(name, value)
__send__("#{name}=", value)
end
+ # Safely recurses into the value, avoiding StackOverflow errors.
+ #
+ # Accepts any value parameter, and a block, which will receive this value parameter.
+ #
+ # @return [Object]
+ #
+ # @api private
+ def safely_recurse_into(value)
+ Thread.current[caller.first] ||= []
+ caller_stack = Thread.current[caller.first]
+
+ return_value = nil
+
+ if !caller_stack.include?(value.object_id)
+ caller_stack.push(self.object_id)
+ return_value = yield(value)
+ caller_stack.pop
+ end
+
+ return_value
+ end
+
end # module InstanceMethods
end # module Virtus
@@ -10,7 +10,7 @@ class Person
attribute :name, String
attribute :age, Integer
attribute :doctor, Boolean
- attribute :salery, Decimal
+ attribute :salary, Decimal
end
class Manager < Person
@@ -39,7 +39,7 @@ class Manager < Person
end
context 'with attributes' do
- let(:attributes) { {:name => 'Jane', :age => 45, :doctor => true, :salery => 4500} }
+ let(:attributes) { {:name => 'Jane', :age => 45, :doctor => true, :salary => 4500} }
subject { Examples::Person.new(attributes) }
specify "#attributes returns the object's attributes as a hash" do
@@ -3,21 +3,67 @@
describe Virtus::InstanceMethods, '#to_hash' do
subject { object.to_hash }
- class Model
- include Virtus
+ context "when object has only singular associations" do
+ class Person
+ include Virtus
- attribute :name, String
- attribute :age, Integer
- attribute :email, String, :accessor => :private
+ attribute :name, String
+ attribute :age, Integer
+
+ attribute :friend, Person
+ end
+
+ context "when object has no recursive relation" do
+ let(:model) { Person }
+ let(:attributes) { { :name => 'john', :age => 28 } }
+ let(:object) { model.new(attributes) }
+
+ it { should be_instance_of(Hash) }
+
+ it "returns object hash" do
+ should eql({ :name => 'john', :age => 28, :friend => nil})
+ end
+ end
+
+ context "when object has a recursive relation" do
+ let(:model) { Person }
+ let(:attributes_child) { { :name => 'jack', :age => 31 } }
+
+ let(:attributes) { { :name => 'john', :age => 28, :friend => attributes_child } }
+
+ let(:object) { model.new(attributes) }
+
+ it { should be_instance_of(Hash) }
+
+ it "returns object hash, excluding first level caller from the underlying hash" do
+ should eql({ :name => 'john', :age => 28,
+ :friend => { :name => 'jack', :age => 31, :friend => nil } })
+ end
+ end
end
- let(:model) { Model }
- let(:object) { model.new(attributes) }
- let(:attributes) { { :name => 'john', :age => 28 } }
+ context "when object has plural (array) recursive relation within" do
+ class User
+ include Virtus
+
+ attribute :name, String
+ attribute :age, Integer
+ attribute :friends, Array[User]
+ end
+
+ let(:model) { User }
+ let(:attributes_child_1) { { :name => 'jack', :age => 31 } }
+ let(:attributes_child_2) { { :name => 'jim', :age => 25 } }
+ let(:attributes) { { :name => 'john', :age => 28, :friends => [attributes_child_1, attributes_child_2] } }
+ let(:object) { model.new(attributes) }
- it { should be_instance_of(Hash) }
+ it { should be_instance_of(Hash) }
- it 'returns attributes' do
- should eql(attributes)
+ it "returns object hash, excluding first level caller from the underlying hash" do
+ should eql({ :name => 'john', :age => 28,
+ :friends => [
+ { :name => 'jack', :age => 31, :friends => nil },
+ { :name => 'jim', :age => 25, :friends => nil } ] })
+ end
end
end