diff options
author | Peter Boling <peter.boling@gmail.com> | 2015-10-23 09:53:42 -0700 |
---|---|---|
committer | Peter Boling <peter.boling@gmail.com> | 2015-10-23 09:53:42 -0700 |
commit | 61b76fab81f9b876a3bdd7239b8509d2b69dd625 (patch) | |
tree | 80ef78e8fad669eb652ba3c8026fbdaa3189eae7 | |
parent | f0e5eac1997946975c447b388889dd9675b41ce9 (diff) | |
download | hashie-61b76fab81f9b876a3bdd7239b8509d2b69dd625.tar.gz |
SRP: The StrictKeyAccess extension will raise an error whenever a key is accessed that does not exist in the hash.
In Python a "Hash" is called a "Dictionary", and ...
> "It is an error to extract a value using a non-existent key."
See: https://docs.python.org/2/tutorial/datastructures.html#dictionaries
EXAMPLE:
class StrictHash < Hash
include Hashie::Extensions::StrictKeyAccess
end
>> hash = StrictHash[foo: "bar"]
=> {:foo=>"bar"}
>> hash[:foo]
=> "bar"
>> hash[:cow]
KeyError: key not found: :cow
-rw-r--r-- | CHANGELOG.md | 1 | ||||
-rw-r--r-- | README.md | 19 | ||||
-rw-r--r-- | lib/hashie.rb | 1 | ||||
-rw-r--r-- | lib/hashie/extensions/strict_key_access.rb | 71 | ||||
-rw-r--r-- | spec/hashie/extensions/strict_key_access_spec.rb | 99 |
5 files changed, 191 insertions, 0 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 57323f9..b758559 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## Next Release * Your contribution here. +* [#314](https://github.com/intridea/hashie/pull/314): Added a `StrictKeyAccess` extension that will raise an error whenever a key is accessed that does not exist in the hash - [@pboling](https://github.com/pboling). * [#304](https://github.com/intridea/hashie/pull/304): Ensured compatibility of `Hash` extensions with singleton objects - [@regexident](https://github.com/regexident). * [#306](https://github.com/intridea/hashie/pull/306): Added `Hashie::Extensions::Dash::Coercion` - [@marshall-lee](https://github.com/marshall-lee). * [#310](https://github.com/intridea/hashie/pull/310): Fixed `Hashie::Extensions::SafeAssignment` bug with private methods - [@marshall-lee](https://github.com/marshall-lee). @@ -392,6 +392,25 @@ books.deep_locate -> (key, value, object) { key == :pages && value <= 120 } # => [{:title=>"Ruby for beginners", :pages=>120}, {:title=>"CSS for intermediates", :pages=>80}] ``` +## StrictKeyAccess + +This extension can be mixed in to allow a Hash to raise an error when attempting to extract a value using a non-existent key. + +### Example: + +```ruby +class StrictKeyAccessHash < Hash + include Hashie::Extensions::StrictKeyAccess +end + +>> hash = StrictKeyAccessHash[foo: "bar"] +=> {:foo=>"bar"} +>> hash[:foo] +=> "bar" +>> hash[:cow] + KeyError: key not found: :cow +``` + ## Mash Mash is an extended Hash that gives simple pseudo-object functionality that can be built from hashes and easily extended. It is intended to give the user easier access to the objects within the Mash through a property-like syntax, while still retaining all Hash functionality. diff --git a/lib/hashie.rb b/lib/hashie.rb index b0f771c..59a7c25 100644 --- a/lib/hashie.rb +++ b/lib/hashie.rb @@ -26,6 +26,7 @@ module Hashie autoload :PrettyInspect, 'hashie/extensions/pretty_inspect' autoload :KeyConversion, 'hashie/extensions/key_conversion' autoload :MethodAccessWithOverride, 'hashie/extensions/method_access' + autoload :StrictKeyAccess, 'hashie/extensions/strict_key_access' module Parsers autoload :YamlErbParser, 'hashie/extensions/parsers/yaml_erb_parser' diff --git a/lib/hashie/extensions/strict_key_access.rb b/lib/hashie/extensions/strict_key_access.rb new file mode 100644 index 0000000..6972252 --- /dev/null +++ b/lib/hashie/extensions/strict_key_access.rb @@ -0,0 +1,71 @@ +module Hashie + module Extensions + # SRP: This extension will fail an error whenever a key is accessed that does not exist in the hash. + # + # EXAMPLE: + # + # class StrictKeyAccessHash < Hash + # include Hashie::Extensions::StrictKeyAccess + # end + # + # >> hash = StrictKeyAccessHash[foo: "bar"] + # => {:foo=>"bar"} + # >> hash[:foo] + # => "bar" + # >> hash[:cow] + # KeyError: key not found: :cow + # + # NOTE: For googlers coming from Python to Ruby, this extension makes a Hash behave like a "Dictionary". + # + module StrictKeyAccess + class DefaultError < StandardError + def initialize(msg = 'Setting or using a default with Hashie::Extensions::StrictKeyAccess does not make sense', *args) + super + end + end + + # NOTE: This extension would break the default behavior of Hash initialization: + # + # >> a = StrictKeyAccessHash.new(a: :b) + # => {} + # >> a[:a] + # KeyError: key not found: :a + # + # Includes the Hashie::Extensions::MergeInitializer extension to get around that problem. + # Also note that defaults still don't make any sense with a StrictKeyAccess. + def self.included(base) + # Can only include into classes with a hash initializer + base.send(:include, Hashie::Extensions::MergeInitializer) + end + + def [](key) + fetch(key) + end + + def default(_ = nil) + fail DefaultError + end + + def default=(_) + fail DefaultError + end + + def default_proc + fail DefaultError + end + + def default_proc=(_) + fail DefaultError + end + + def key(value) + result = super + if result.nil? && (!key?(result) || self[result] != value) + fail KeyError, "key not found with value of #{value.inspect}" + else + result + end + end + end + end +end diff --git a/spec/hashie/extensions/strict_key_access_spec.rb b/spec/hashie/extensions/strict_key_access_spec.rb new file mode 100644 index 0000000..2e47064 --- /dev/null +++ b/spec/hashie/extensions/strict_key_access_spec.rb @@ -0,0 +1,99 @@ +require 'spec_helper' + +describe Hashie::Extensions::StrictKeyAccess do + class StrictKeyAccessHash < Hash + include Hashie::Extensions::StrictKeyAccess + end + + shared_examples_for 'StrictKeyAccess with valid key' do |options = {}| + before { pending_for(options[:pending]) } if options[:pending] + context 'access' do + it('returns value') do + expect(instance[valid_key]).to eq valid_value + end + end + context 'lookup' do + it('returns key') do + expect(instance.key(valid_value)).to eq valid_key + end + end + end + shared_examples_for 'StrictKeyAccess with invalid key' do |options = {}| + before { pending_for(options[:pending]) } if options[:pending] + context 'access' do + it('raises an error') do + # Formatting of the error message varies on Rubinius and ruby-head + expect { instance[invalid_key] }.to raise_error KeyError + end + end + context 'lookup' do + it('raises an error') do + # Formatting of the error message does not vary here because raised by StrictKeyAccess + expect { instance.key(invalid_value) }.to raise_error KeyError, + %(key not found with value of #{invalid_value.inspect}) + end + end + end + shared_examples_for 'StrictKeyAccess with exploding defaults' do + context '#default' do + it 'raises an error' do + expect { instance.default(invalid_key) }.to raise_error Hashie::Extensions::StrictKeyAccess::DefaultError, + 'Setting or using a default with Hashie::Extensions::StrictKeyAccess does not make sense' + end + end + context '#default=' do + it 'raises an error' do + expect { instance.default = invalid_key }.to raise_error Hashie::Extensions::StrictKeyAccess::DefaultError, + 'Setting or using a default with Hashie::Extensions::StrictKeyAccess does not make sense' + end + end + context '#default_proc' do + it 'raises an error' do + expect { instance.default_proc }.to raise_error Hashie::Extensions::StrictKeyAccess::DefaultError, + 'Setting or using a default with Hashie::Extensions::StrictKeyAccess does not make sense' + end + end + context '#default_proc=' do + it 'raises an error' do + expect { instance.default_proc = proc {} }.to raise_error Hashie::Extensions::StrictKeyAccess::DefaultError, + 'Setting or using a default with Hashie::Extensions::StrictKeyAccess does not make sense' + end + end + end + + let(:klass) { StrictKeyAccessHash } + let(:instance) { StrictKeyAccessHash.new(*args) } + let(:args) do + [ + { valid_key => valid_value } + ] + end + let(:valid_key) { :abc } + let(:valid_value) { 'def' } + let(:invalid_key) { :mega } + let(:invalid_value) { 'death' } + + context '.new' do + it_behaves_like 'StrictKeyAccess with valid key' + it_behaves_like 'StrictKeyAccess with invalid key' + it_behaves_like 'StrictKeyAccess with exploding defaults' + end + + context '[]' do + let(:instance) { StrictKeyAccessHash[*args] } + it_behaves_like 'StrictKeyAccess with valid key', pending: { engine: 'rbx' } + it_behaves_like 'StrictKeyAccess with invalid key', pending: { engine: 'rbx' } + it_behaves_like 'StrictKeyAccess with exploding defaults' + end + + context 'with default' do + let(:args) do + [ + { valid_key => valid_value }, invalid_value + ] + end + it_behaves_like 'StrictKeyAccess with valid key' + it_behaves_like 'StrictKeyAccess with invalid key' + it_behaves_like 'StrictKeyAccess with exploding defaults' + end +end |