diff --git a/Gemfile b/Gemfile index 851fabc..cdfab82 100644 --- a/Gemfile +++ b/Gemfile @@ -1,2 +1,7 @@ source 'https://rubygems.org' + gemspec + +group :test do + gem 'rspec' +end diff --git a/Gemfile.lock b/Gemfile.lock index 1f7632b..ccb981e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -6,9 +6,20 @@ PATH GEM remote: https://rubygems.org/ specs: - minitest (5.3.4) - rake (0.9.2) - test_declarative (0.0.5) + diff-lcs (1.2.5) + rspec (3.3.0) + rspec-core (~> 3.3.0) + rspec-expectations (~> 3.3.0) + rspec-mocks (~> 3.3.0) + rspec-core (3.3.2) + rspec-support (~> 3.3.0) + rspec-expectations (3.3.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.3.0) + rspec-mocks (3.3.2) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.3.0) + rspec-support (3.3.0) PLATFORMS java @@ -16,6 +27,7 @@ PLATFORMS DEPENDENCIES hashr! - minitest (>= 5.0.0) - rake - test_declarative (>= 0.0.2) + rspec + +BUNDLED WITH + 1.10.6 diff --git a/README.md b/README.md index c345027..34bed8f 100644 --- a/README.md +++ b/README.md @@ -16,21 +16,36 @@ It supports the following features: Directly use Hashr instances like this: - config = Hashr.new('foo' => { 'bar' => 'bar' }) + config = Hashr.new(foo: { bar: 'bar' }) config.foo? # => true - config.foo # => { :bar => 'bar' } + config.foo # => { bar: 'bar' } config.foo.bar? # => true config.foo.bar # => 'bar' - config.foo.bar = 'bar!' - config.foo.bar # => 'bar!' + config.foo.bar = 'bar' + config.foo.bar # => 'bar' config.foo.baz = 'baz' config.foo.baz # => 'baz' -Be aware that by default missing keys won't raise an exception but instead behave like Hash access: +Hash core methods are not available but assume you mean to look up keys with +the same name: + + config = Hashr.new(count: 1, key: 'key') + config.count # => 1 + config.key # => 'key' + +In order to check a hash stored on a certain key you can convert it to a Ruby +Hash: + + config = Hashr.new(count: 1, key: 'key') + config.to_h.count # => 2 + config.to_h.key # => raises ArgumentError: "wrong number of arguments (0 for 1)" + +By default missing keys won't raise an exception but instead behave like Hash +access: config = Hashr.new config.foo? # => false @@ -43,76 +58,6 @@ You can make Hashr raise an `IndexError` though like this: config.foo? # => false config.foo # => raises an IndexError "Key :foo is not defined." -You can also anonymously overwrite core Hash methods like this: - - config = Hashr.new(:count => 3) do - def count - self[:count] - end - end - config.count # => 3 - -And you can anonymously provide defaults like this: - - data = { :foo => 'foo' } - defaults = { :bar => 'bar' } - config = Hashr.new(data, defaults) - config.foo # => 'foo' - config.bar # => 'bar' - -But you can obvioulsy also derive a custom class to define defaults and overwrite core Hash methods like this: - - class Config < Hashr - define :foo => { :bar => 'bar' } - - def count - self[:count] - end - end - - config = Config.new - config.foo.bar # => 'bar' - -Include modules to nested hashes like this: - - class Config < Hashr - module Boxes - def count - self[:count] # overwrites a Hash method to return the Hash's content here - end - - def names - @names ||= (1..count).map { |num| "box-#{num}" } - end - end - - define :boxes => { :count => 3, :_include => Boxes } - end - - config = Config.new - config.boxes # => { :count => 3 } - config.boxes.count # => 3 - config.boxes.names # => ["box-1", "box-2", "box-3"] - -As overwriting Hash methods for method access to keys is a common pattern there's a short cut to it: - - class Config < Hashr - define :_access => [:count, :key] - end - - config = Config.new(:count => 3, :key => 'key') - config.count # => 3 - config.key # => 'key' - -Both `:_include` and `:_access` can be defined as defaults, i.e. so that they will be used on all nested hashes: - - class Config < Hashr - default :_access => :key - end - - config = Config.new(:key => 'key', :foo => { :key => 'foo.key' }) - config.key # => 'key' - config.foo.key # => 'foo.key' ## Environment defaults @@ -123,7 +68,7 @@ Hashr includes a simple module that makes it easy to overwrite configuration def self.env_namespace = 'foo' - define :boxes => { :memory => '1024' } + define boxes: { memory: '1024' } end Now when an environment variable is defined then it will overwrite the default: @@ -132,16 +77,6 @@ Now when an environment variable is defined then it will overwrite the default: config = Config.new config.boxes.memory # => '2048' -## Running the tests - -You can run the tests as follows: - - # going through bundler - bundle exec rake - - # using just ruby - ruby -rubygems -Ilib:test test/hashr_test.rb - ## Other libraries You also might want to check out OpenStruct and Hashie. diff --git a/hashr.gemspec b/hashr.gemspec index 24fcb64..8e1e735 100644 --- a/hashr.gemspec +++ b/hashr.gemspec @@ -16,8 +16,4 @@ Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY s.require_path = 'lib' s.rubyforge_project = '[none]' - - s.add_development_dependency 'rake' - s.add_development_dependency 'test_declarative', '>=0.0.2' - s.add_development_dependency 'minitest', '>=5.0.0' end diff --git a/spec/core_ext/hash_spec.rb b/spec/core_ext/hash_spec.rb new file mode 100644 index 0000000..344b50a --- /dev/null +++ b/spec/core_ext/hash_spec.rb @@ -0,0 +1,36 @@ +describe Hash do + describe 'deep_symbolize_keys' do + it 'symbolizes keys on nested hashes' do + hash = { 'foo' => { 'bar' => 'bar' } } + expected = { :foo => { :bar => 'bar' } } + expect(hash.deep_symbolize_keys).to eq(expected) + end + + it 'walks arrays' do + hash = { 'foo' => [{ 'bar' => 'bar', 'baz' => { 'buz' => 'buz' } }] } + expected = { :foo => [{ :bar => 'bar', :baz => { :buz => 'buz' } }] } + expect(hash.deep_symbolize_keys).to eq(expected) + end + end + + describe 'deep_symbolize_keys!' do + it 'replaces with deep_symbolize' do + hash = { 'foo' => { 'bar' => 'baz' } } + expected = { :foo => { :bar => 'baz' } } + hash.deep_symbolize_keys! + expect(hash).to eq(expected) + end + end + + describe 'slice' do + it 'returns a new hash containing the given keys' do + hash = { :foo => 'foo', :bar => 'bar', :baz => 'baz' } + expected = { :foo => 'foo', :bar => 'bar' } + expect(hash.slice(:foo, :bar)).to eq(expected) + end + + it 'does not explode on a missing key' do + expect({}.slice(:foo)).to eq({}) + end + end +end diff --git a/spec/hashr/conversion_spec.rb b/spec/hashr/conversion_spec.rb new file mode 100644 index 0000000..b5fd87b --- /dev/null +++ b/spec/hashr/conversion_spec.rb @@ -0,0 +1,27 @@ +describe Hashr do + shared_examples_for 'converts to a hash' do + it 'converts the Hashr instance to a hash' do + expect(hash.class).to eq(Hash) + end + + it 'converts nested instances to hashes' do + expect(hash[:foo].class).to eq(Hash) + end + + it 'populates the hash with symbolized keys' do + expect(hash[:foo][:bar]).to eq('baz') + end + end + + let(:hashr) { Hashr.new(:foo => { :bar => 'baz' }) } + + # describe 'to_h' do + # let(:hash) { hashr.to_h } + # include_examples 'converts to a hash' + # end + + describe 'to_hash' do + let(:hash) { hashr.to_hash } + include_examples 'converts to a hash' + end +end diff --git a/spec/hashr/defaults_spec.rb b/spec/hashr/defaults_spec.rb new file mode 100644 index 0000000..3f3bee5 --- /dev/null +++ b/spec/hashr/defaults_spec.rb @@ -0,0 +1,43 @@ +describe Hashr, 'defaults' do + shared_examples_for 'defaults' do |method| + describe 'using a symbolized hash' do + let(:klass) { Class.new(Hashr) { send method, foo: 'foo' } } + + it 'defines the default' do + expect(klass.new.foo).to eq('foo') + end + end + + describe 'using a stringified hash' do + let(:klass) { Class.new(Hashr) { send method, 'foo' => 'foo' } } + + it 'defines the default' do + expect(klass.new.foo).to eq('foo') + end + end + + describe 'with a nested hash' do + let(:klass) { Class.new(Hashr) { send method, foo: { bar: { baz: 'baz' } } } } + + it 'defines the default' do + expect(klass.new.foo.bar.baz).to eq('baz') + end + end + + describe 'with a nested array' do + let(:klass) { Class.new(Hashr) { send method, foo: ['bar'] } } + + it 'defines the default' do + expect(klass.new.foo.first).to eq('bar') + end + end + end + + # describe 'definition using default' do + # include_examples 'defaults', :default + # end + + describe 'definition using define (deprecated)' do + include_examples 'defaults', :define + end +end diff --git a/spec/hashr/hash_acceess_spec.rb b/spec/hashr/hash_acceess_spec.rb new file mode 100644 index 0000000..bf9217d --- /dev/null +++ b/spec/hashr/hash_acceess_spec.rb @@ -0,0 +1,17 @@ +describe Hashr, 'hash access' do + it 'is indifferent about symbols/strings (string data given, symbol keys used)' do + expect(Hashr.new('foo' => { 'bar' => 'bar' })[:foo][:bar]).to eq('bar') + end + + it 'is indifferent about symbols/strings (symbol data given, string keys used)' do + expect(Hashr.new(foo: { bar: 'bar' })['foo']['bar']).to eq('bar') + end + + # it 'allows accessing keys with Hash core method names (count)' do + # expect(Hashr.new(count: 2).count).to eq(2) + # end + + # it 'allows accessing keys with Hash core method names (key)' do + # expect(Hashr.new(key: 'key').key).to eq('key') + # end +end diff --git a/spec/hashr/hash_assignment_spec.rb b/spec/hashr/hash_assignment_spec.rb new file mode 100644 index 0000000..c61f4d6 --- /dev/null +++ b/spec/hashr/hash_assignment_spec.rb @@ -0,0 +1,13 @@ +describe Hashr, 'hash assignment' do + let(:hashr) { Hashr.new } + + it 'works with a string key' do + hashr['foo'] = 'foo' + expect(hashr.foo).to eq('foo') + end + + it 'works with a symbol key' do + hashr[:foo] = 'foo' + expect(hashr.foo).to eq('foo') + end +end diff --git a/spec/hashr/initialize_spec.rb b/spec/hashr/initialize_spec.rb new file mode 100644 index 0000000..8c7bcff --- /dev/null +++ b/spec/hashr/initialize_spec.rb @@ -0,0 +1,9 @@ +describe Hashr, 'initialize' do + it 'takes nil' do + expect { Hashr.new(nil) }.to_not raise_error + end + + it 'raises an ArgumentError when given a string' do + expect { Hashr.new('foo') }.to raise_error(ArgumentError) + end +end diff --git a/spec/hashr/method_access_spec.rb b/spec/hashr/method_access_spec.rb new file mode 100644 index 0000000..942aed6 --- /dev/null +++ b/spec/hashr/method_access_spec.rb @@ -0,0 +1,70 @@ +describe Hashr, 'method access' do + describe 'on an existing key' do + it 'returns the value' do + expect(Hashr.new(foo: 'foo').foo).to eq('foo') + end + + it 'returns a nested hash' do + expect(Hashr.new(foo: { bar: 'bar' }).foo).to eq(bar: 'bar') + end + + it 'returns a nested array' do + expect(Hashr.new(foo: ['bar', 'buz']).foo).to eq(['bar', 'buz']) + end + end + + describe 'on an existing nested key' do + it 'returns the value' do + expect(Hashr.new(foo: { bar: 'bar' }).foo.bar).to eq('bar') + end + + it 'returns a nested array' do + expect(Hashr.new(foo: { bar: ['bar', 'buz'] }).foo.bar).to eq(['bar', 'buz']) + end + end + + describe 'with :raise_missing_keys set to false' do + before { Hashr.raise_missing_keys = false } + + it 'it returns nil on a non-existing key' do + expect(Hashr.new(foo: 'foo').bar).to eq(nil) + end + + it 'it returns nil on a non-existing nested key' do + expect(Hashr.new(foo: { bar: 'bar' }).foo.baz).to eq(nil) + end + end + + describe 'with :raise_missing_keys set to true' do + before { Hashr.raise_missing_keys = true } + + it 'it raises an IndexError on a non-existing key' do + expect { Hashr.new(foo: 'foo').bar }.to raise_error(IndexError) + end + + it 'it raises an IndexError on a non-existing nested key' do + expect { Hashr.new(foo: { bar: 'bar' }).foo.baz }.to raise_error(IndexError) + end + + end + + describe 'predicate methods' do + it 'returns true if the key has a value' do + expect(Hashr.new(foo: { bar: 'bar' }).foo.bar?).to eq(true) + end + + it 'returns false if the key does not have a value' do + expect(Hashr.new(foo: { bar: 'bar' }).foo.baz?).to eq(false) + end + end + + describe 'respond_to?' do + it 'returns true for existing keys' do + expect(Hashr.new(foo: 'bar').respond_to?(:foo)).to eq(true) + end + + it 'returns false for missing keys' do + expect(Hashr.new.respond_to?(:foo)).to eq(false) + end + end +end diff --git a/spec/hashr/method_assignment_spec.rb b/spec/hashr/method_assignment_spec.rb new file mode 100644 index 0000000..82473cf --- /dev/null +++ b/spec/hashr/method_assignment_spec.rb @@ -0,0 +1,8 @@ +describe Hashr, 'method assignment' do + let(:hashr) { Hashr.new } + + it 'works' do + hashr.foo = 'foo' + expect(hashr.foo).to eq('foo') + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..3331fc7 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1 @@ +require 'hashr' diff --git a/test/core_ext_test.rb b/test/core_ext_test.rb deleted file mode 100644 index f2fc0f7..0000000 --- a/test/core_ext_test.rb +++ /dev/null @@ -1,28 +0,0 @@ -require 'test_helper' - -class CoreExtTest < Minitest::Test - test 'Hash#deep_symbolize_keys walks arrays, too' do - hash = { 'foo' => [{ 'bar' => 'bar', 'baz' => { 'buz' => 'buz' } }] } - expected = { :foo => [{ :bar => 'bar', :baz => { :buz => 'buz' } }] } - assert_equal expected, hash.deep_symbolize_keys - end - - test 'Hash#deep_symbolize_keys! replaces with deep_symbolize' do - hash = { 'foo' => { 'bar' => 'baz' } } - expected = { :foo => { :bar => 'baz' } } - hash.deep_symbolize_keys! - assert_equal expected, hash - end - - test 'Hash#slice returns a new hash containing the given keys' do - hash = { :foo => 'foo', :bar => 'bar', :baz => 'baz' } - expected = { :foo => 'foo', :bar => 'bar' } - assert_equal expected, hash.slice(:foo, :bar) - end - - test 'Hash#slice does not explode on a missing key' do - hash = {} - expected = {} - assert_equal expected, hash.slice(:foo) - end -end diff --git a/test/hashr_test.rb b/test/hashr_test.rb index 5692800..3d87a07 100644 --- a/test/hashr_test.rb +++ b/test/hashr_test.rb @@ -5,58 +5,6 @@ def teardown Hashr.raise_missing_keys = false end - test 'initialize takes nil' do - Hashr.new(nil) - end - - test 'initialize raises an ArgumentError when given a string' do - assert_raises(ArgumentError) { Hashr.new("foo") } - end - - test 'method access on an existing key returns the value' do - assert_equal 'foo', Hashr.new(:foo => 'foo').foo - end - - test 'method access on a non-existing key returns nil when raise_missing_keys is false' do - Hashr.raise_missing_keys = false - assert_nil Hashr.new(:foo => 'foo').bar - end - - test 'method access on a non-existing key raises an IndexError when raise_missing_keys is true' do - Hashr.raise_missing_keys = true - assert_raises(IndexError) { Hashr.new(:foo => 'foo').bar } - end - - test 'method access on an existing nested key returns the value' do - assert_equal 'bar', Hashr.new(:foo => { :bar => 'bar' }).foo.bar - end - - test 'method access on a non-existing nested key returns nil when raise_missing_keys is false' do - Hashr.raise_missing_keys = false - assert_nil Hashr.new(:foo => { :bar => 'bar' }).foo.baz - end - - test 'method access on a non-existing nested key raises an IndexError when raise_missing_keys is true' do - Hashr.raise_missing_keys = true - assert_raises(IndexError) { Hashr.new(:foo => { :bar => 'bar' }).foo.baz } - end - - test 'method access with a question mark returns true if the key has a value' do - assert_equal true, Hashr.new(:foo => { :bar => 'bar' }).foo.bar? - end - - test 'method access with a question mark returns false if the key does not have a value' do - assert_equal false, Hashr.new(:foo => { :bar => 'bar' }).foo.baz? - end - - test 'hash access is indifferent about symbols/strings (string data given, symbol keys used)' do - assert_equal 'bar', Hashr.new('foo' => { 'bar' => 'bar' })[:foo][:bar] - end - - test 'hash access is indifferent about symbols/strings (symbol data given, string keys used)' do - assert_equal 'bar', Hashr.new(:foo => { :bar => 'bar' })['foo']['bar'] - end - test 'mixing symbol and string keys in defaults and data' do Symbolized = Class.new(Hashr) { define :foo => 'foo' } Stringified = Class.new(Hashr) { define 'foo' => 'foo' } @@ -75,43 +23,6 @@ def teardown assert_equal 'foo', NoDefault.new('foo' => 'foo').foo end - test 'method assignment works' do - hashr = Hashr.new - hashr.foo = 'foo' - assert_equal 'foo', hashr.foo - end - - test 'method using a string key works' do - hashr = Hashr.new - hashr['foo'] = 'foo' - assert_equal 'foo', hashr.foo - end - - test 'using a symbol key works' do - hashr = Hashr.new - hashr[:foo] = 'foo' - assert_equal 'foo', hashr.foo - end - - test 'respond_to? returns true if raise_missing_keys is off' do - Hashr.raise_missing_keys = false - hashr = Hashr.new - assert hashr.respond_to?(:foo) - end - - test 'respond_to? returns false for missing keys if raise_missing_keys is on' do - Hashr.raise_missing_keys = true - hashr = Hashr.new - assert_equal false, hashr.respond_to?(:foo) - end - - test 'respond_to? returns true for extant keys if raise_missing_keys is on' do - Hashr.raise_missing_keys = true - hashr = Hashr.new - hashr[:foo] = 'bar' - assert hashr.respond_to?(:foo) - end - test 'defining defaults' do klass = Class.new(Hashr) do define :foo => 'foo', :bar => { :baz => 'baz' }