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
This commit is contained in:
Peter Boling 2015-10-23 09:53:42 -07:00
parent f0e5eac199
commit 61b76fab81
5 changed files with 191 additions and 0 deletions

View File

@ -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).

View File

@ -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.

View File

@ -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'

View File

@ -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

View File

@ -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