diff --git a/.ruby-version b/.ruby-version index 879b416e..2bf1c1cc 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.1 +2.3.1 diff --git a/.travis.yml b/.travis.yml index a1eec966..99ffeaa7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,7 @@ rvm: - 2.0 - 2.1 - 2.2 + - 2.3 - jruby - rbx - ruby-head @@ -16,8 +17,4 @@ matrix: - rvm: rbx notifications: email: - - piotr.solnica@gmail.com - - dan.kubb@gmail.com -addons: - code_climate: - repo_token: 2b66fbb7c7c72503eb7841a479c0ad923f691729f4109b4aa8c9b4def1ebb42d + - gabe.saravia@fooda.com diff --git a/lib/virtus/instance_methods.rb b/lib/virtus/instance_methods.rb index 4448bf66..626de57d 100644 --- a/lib/virtus/instance_methods.rb +++ b/lib/virtus/instance_methods.rb @@ -41,8 +41,39 @@ module MassAssignment def attributes attribute_set.get(self) end - alias_method :to_hash, :attributes - alias_method :to_h, :attributes + + # Returns a hash of all publicly accessible attributes (including nested attributes) + # + # @example + # class Child + # include Virtus + # + # attribute :name, String + # end + # + # class Parent + # include Virtus + # + # attribute :name, String + # attribute :child, Child + # end + # + # parent = Parent.new(name: 'John', child: {name: 'Jim'}) + # parent.to_h # => { name: 'John', child: {name: 'Jim'} } + # + # @return [Hash] + # + # @api public + def to_h + attributes.each_with_object({}) do |(k, v), h| + if v.is_a? Array + h[k] = v.map { |v| hash_if_responds_or_value v } + else + h[k] = hash_if_responds_or_value v + end + end + end + alias_method :to_hash, :to_h # Mass-assign attribute values # @@ -198,6 +229,10 @@ def set_default_attributes! private + def hash_if_responds_or_value(value) + value.respond_to?(:to_h) ? value.to_h : value + end + # The list of allowed public methods # # @return [Array] diff --git a/spec/unit/virtus/attributes_reader_spec.rb b/spec/unit/virtus/attributes_reader_spec.rb index 82774c37..d1c98699 100644 --- a/spec/unit/virtus/attributes_reader_spec.rb +++ b/spec/unit/virtus/attributes_reader_spec.rb @@ -38,4 +38,30 @@ it_behaves_like 'attribute hash' end + + context "#to_h / #to_hash" do + let(:model) { + child = Class.new { + include Virtus + + attribute :foo, String + } + + Class.new { + include Virtus + + attribute :bar, String + attribute :nested_model, child + attribute :array_of_nested_model, Array[child] + + } + } + + subject { model.new bar: "1", nested_model: { foo: "2" }, array_of_nested_model: [{ foo: "3" }, { foo: "4" }] } + + it "deeply converts to a hash" do + expect(subject.to_h).to eql(bar: "1", nested_model: { foo: "2" }, array_of_nested_model: [{ foo: "3" }, { foo: "4" }] ) + expect(subject.to_hash).to eql(bar: "1", nested_model: { foo: "2" }, array_of_nested_model: [{ foo: "3" }, { foo: "4" }] ) + end + end end