diff --git a/lib/hashie.rb b/lib/hashie.rb index 383e59f1..dfd38c6d 100644 --- a/lib/hashie.rb +++ b/lib/hashie.rb @@ -12,6 +12,7 @@ module Extensions autoload :Coercion, 'hashie/extensions/coercion' autoload :DeepMerge, 'hashie/extensions/deep_merge' autoload :IgnoreUndeclared, 'hashie/extensions/ignore_undeclared' + autoload :IgnoreRequired, 'hashie/extensions/ignore_required' autoload :IndifferentAccess, 'hashie/extensions/indifferent_access' autoload :MergeInitializer, 'hashie/extensions/merge_initializer' autoload :MethodAccess, 'hashie/extensions/method_access' diff --git a/lib/hashie/extensions/ignore_required.rb b/lib/hashie/extensions/ignore_required.rb new file mode 100644 index 00000000..ed0f0a54 --- /dev/null +++ b/lib/hashie/extensions/ignore_required.rb @@ -0,0 +1,42 @@ +module Hashie + module Extensions + # IgnoreRequired is a simple mixin that silently ignores + # required properties on initialization and assignment instead of + # raising an error. This is useful when using a building an object + # that will eventually be match a Dash but is temporarily incomplete. + # + # @example + # class Person < Hashie::Dash + # + # property :first_name, required: true + # property :last_name, required: true + # property :email + # end + # + # class PartialPerson < Person + # include Hashie::Extensions::IgnoreRequired + # end + # + # user_data = { + # :first_name => 'Freddy', + # } + # + # p = Person.new(user_data) # ArgumentError: The property 'last_name' is required for Person. + # + # p = PartialPerson.new(user_data) + # p.last_name = 'Nostrils' + # p.first_name # => 'Freddy' + # p.first_name # => 'Nostrils' + # p.email # => nil + # p.foo # => NoMethodError + module IgnoreRequired + def assert_property_required!(_property, _value) + # do nothing + end + + def assert_property_set!(_property) + # do nothing + end + end + end +end diff --git a/spec/hashie/extensions/ignore_required_spec.rb b/spec/hashie/extensions/ignore_required_spec.rb new file mode 100644 index 00000000..7645cbc2 --- /dev/null +++ b/spec/hashie/extensions/ignore_required_spec.rb @@ -0,0 +1,65 @@ +require 'spec_helper' + +describe Hashie::Extensions::IgnoreRequired do + context 'included in Dash' do + class ForgivingDash < Hashie::Dash + include Hashie::Extensions::IgnoreRequired + property :city, required: true + property :state, required: true, from: :province + property :zip, required: true + end + + subject { ForgivingDash } + + it 'silently ignores required properties on initialization' do + expect { subject.new(city: 'New York') }.to_not raise_error + end + + it 'raises errors for undefined properties on initialization' do + expect { subject.new(city: 'Toronto', province: 'Ontario') }.to raise_error(NoMethodError, /property 'province' is not defined/) + end + + it 'requires properties to be declared on assignment' do + hash = subject.new(city: 'Toronto') + expect { hash.country = 'Canada' }.to raise_error(NoMethodError) + end + + it 'requires properties to be declared on access' do + hash = subject.new(city: 'Toronto') + expect { hash.country }.to raise_error(NoMethodError) + end + end + + context 'combined with Coercion' do + class ForgivingDashWithCoercion < ForgivingDash + include Hashie::Extensions::Coercion + coerce_key :zip, ->(v) { format('%05d', v) } + end + + subject { ForgivingDashWithCoercion } + + it 'works with coerced properties' do + expect(subject.new(zip: 501).zip).to eq('00501') + end + + context 'with nested, coerced Dashes' do + class Address < Hashie::Dash + property :number, required: true + property :street, required: true + property :apartment + end + + class ForgivingDashWithAddress < ForgivingDashWithCoercion + property :address, required: true + coerce_key :address, Address + end + + subject { ForgivingDashWithAddress } + + it 'does not work propagate to nested, coercable properties' do + address = { street: 'Pennsylvania Avenue' } + expect { subject.new(address: address) }.to raise_error(ArgumentError, /property 'number' is required for Address/) + end + end + end +end