Skip to content

Commit

Permalink
Rewrite
Browse files Browse the repository at this point in the history
Simplify Hashr:

* use BasicObject
* remove unused features like :_access and :_include
* remove raise_missing_keys
* rename :define to :default
  • Loading branch information
svenfuchs committed Oct 4, 2015
1 parent 2d29a3d commit 13edf19
Show file tree
Hide file tree
Showing 21 changed files with 430 additions and 590 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Gemfile.lock
33 changes: 0 additions & 33 deletions Gemfile.lock

This file was deleted.

101 changes: 64 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,80 +2,107 @@

# Hashr

Hashr is a very simple and tiny class derived from Ruby's core Hash class which makes using nested hashes for configuration (and other purposes) easier and less repetive and error prone.
Hashr is a very simple and tiny class which makes using nested hashes for
configuration (and other purposes) easier.

It supports the following features:

* method read and write access
* automatic predicate (boolean, i.e. `?`) methods
* easy defaults
* easy inclusion of modules into nested hashes
* automatic symbolized keys
* indifferent (strings vs symbols) keys

## Usage

Directly use Hashr instances like this:

config = Hashr.new(foo: { bar: 'bar' })
```ruby
config = Hashr.new(foo: { bar: 'bar' })

config.foo? # => true
config.foo # => { bar: 'bar' }
config.foo? # => true
config.foo # => { bar: 'bar' }

config.foo.bar? # => true
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'
config.foo.baz = 'baz'
config.foo.baz # => 'baz'
```

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'
```ruby
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)"
```ruby
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:
Missing keys won't raise an exception but instead behave like Hash access:

config = Hashr.new
config.foo? # => false
config.foo # => nil
```ruby
config = Hashr.new
config.foo? # => false
config.foo # => nil
```

You can make Hashr raise an `IndexError` though like this:
## Defaults

Hashr.raise_missing_keys = true
config = Hashr.new
config.foo? # => false
config.foo # => raises an IndexError "Key :foo is not defined."
Defaults can be defined per class:

```ruby
class Config < Hashr
default boxes: { memory: '1024' }
end

config = Config.new
config.boxes.memory # => 1024
```

Or passed to the instance:

```ruby
data = {}
defaults = { boxes: { memory: '1024' } }

config = Hashr.new(data, defaults)
config.boxes.memory # => 1024
```

## Environment defaults

Hashr includes a simple module that makes it easy to overwrite configuration defaults from environment variables:
Hashr includes a simple module that makes it easy to overwrite configuration
defaults from environment variables:

class Config < Hashr
extend Hashr::EnvDefaults
```ruby
class Config < Hashr
extend Hashr::EnvDefaults

self.env_namespace = 'foo'
self.env_namespace = 'foo'

define boxes: { memory: '1024' }
end
default boxes: { memory: '1024' }
end
```

Now when an environment variable is defined then it will overwrite the default:

ENV['FOO_BOXES_MEMORY'] = '2048'
config = Config.new
config.boxes.memory # => '2048'
```ruby
ENV['FOO_BOXES_MEMORY'] = '2048'
config = Config.new
config.boxes.memory # => '2048'
```

## Other libraries

Expand Down
12 changes: 0 additions & 12 deletions Rakefile

This file was deleted.

6 changes: 3 additions & 3 deletions hashr.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ Gem::Specification.new do |s|
s.name = "hashr"
s.version = Hashr::VERSION
s.authors = ["Sven Fuchs"]
s.email = "[email protected]"
s.email = "[email protected]"
s.homepage = "http://github.com/svenfuchs/hashr"
s.summary = "Simple Hash extension to make working with nested hashes (e.g. for configuration) easier and less error-prone"
s.description = "Simple Hash extension to make working with nested hashes (e.g. for configuration) easier and less error-prone."
s.description = "#{s.summary}."

s.files = Dir['{lib/**/*,test/**/*,[A-Z]*}']
s.files = Dir['{lib/**/*,test/**/*,MIT-LICENSE,README.md,Gemfile}']
s.platform = Gem::Platform::RUBY
s.require_path = 'lib'
s.rubyforge_project = '[none]'
Expand Down
138 changes: 50 additions & 88 deletions lib/hashr.rb
Original file line number Diff line number Diff line change
@@ -1,132 +1,94 @@
require 'hashr/core_ext/ruby/hash'

class Hashr < Hash
autoload :EnvDefaults, 'hashr/env_defaults'

TEMPLATE = new
class Hashr < BasicObject
require 'hashr/core_ext/ruby/hash'
require 'hashr/env_defaults'

class << self
attr_accessor :raise_missing_keys
attr_reader :defaults

def define(definition)
@definition = deep_accessorize(definition.deep_symbolize_keys)
def inherited(other)
other.default(defaults)
end

def definition
@definition ||= {}
def new(*args)
super(self, *args)
end

def default(defaults)
@defaults = deep_accessorize(defaults)
@defaults = (self.defaults || {}).deep_merge(defaults || {})
end
alias :define :default
end

def defaults
@defaults ||= {}
end
attr_reader :class

def deep_accessorize(hash)
hash.each do |key, value|
next unless value.is_a?(Hash)
value[:_access] ||= []
value[:_access] = Array(value[:_access])
value.keys.each { |key| value[:_access] << key if value.respond_to?(key) }
deep_accessorize(value)
end
def initialize(klass, data = nil, defaults = nil, &block)
::Kernel.fail ::ArgumentError.new("Invalid input #{data.inspect}") unless data.nil? || data.is_a?(::Hash)

data = (data || {}).deep_symbolize_keys
defaults = (defaults || klass.defaults || {}).deep_symbolize_keys

@class = klass
@data = defaults.deep_merge(data).inject({}) do |result, (key, value)|
result.merge(key => value.is_a?(::Hash) ? ::Hashr.new(value, {}) : value)
end
end

undef :id if method_defined?(:id) # undefine deprecated method #id on 1.8.x
singleton_class.class_eval(&block) if block_given?
end

def initialize(data = {}, definition = self.class.definition, &block)
raise(ArgumentError.new("Invalid input #{data.inspect}")) unless data.nil? || data.is_a?(Hash)
replace((deep_hashrize(definition.deep_merge((data || {}).deep_symbolize_keys))))
deep_defaultize(self)
(class << self; self; end).class_eval(&block) if block_given?
def defined?(key)
@data.key?(key.respond_to?(:to_sym) ? key.to_sym : key)
end

def [](key, default = nil)
store(key.to_sym, Hashr.new(default)) if default && !key?(key)
super(key.to_sym)
def [](key)
@data[key.to_s] || @data[key.to_sym]
end

def []=(key, value)
super(key.to_sym, value.is_a?(Hash) ? self.class.new(value, {}) : value)
@data.store(key, value.is_a?(::Hash) ? self.class.new(value, {}) : value)
end

def set(path, value, stack = [])
tokens = path.to_s.split('.')
tokens.size == 1 ? self[path] = value : self[tokens.shift, Hashr.new].set(tokens.join('.'), value, stack)
def values_at(*keys)
keys.map { |key| self[key] }
end

def respond_to?(method)
if self.class.raise_missing_keys
key?(method)
else
true
end
true
end

def method_missing(name, *args, &block)
case name.to_s[-1, 1]
when '?'
!!self[name.to_s[0..-2].to_sym]
!!self[name.to_s[0..-2]]
when '='
self[name.to_s[0..-2].to_sym] = args.first
self[name.to_s[0..-2]] = args.first
else
raise(IndexError.new("Key #{name.inspect} is not defined.")) if !key?(name) && self.class.raise_missing_keys
self[name]
end
end

def include_modules(modules)
Array(modules).each { |mod| meta_class.send(:include, mod) } if modules
def to_h
@data.inject({}) do |hash, (key, value)|
hash.merge(key => value.respond_to?(:to_h) ? value.to_h : value)
end
end
alias to_hash to_h

def include_accessors(accessors)
Array(accessors).each { |accessor| meta_class.send(:define_method, accessor) { self[accessor] } } if accessors
def inspect
"<Hashr #{@data.inspect}>"
end

def meta_class
class << self; self end
def ==(other)
to_h == other.to_h if other.respond_to?(:to_h)
end

def to_hash
inject({}) do |hash, (key, value)|
hash[key] = value.is_a?(Hashr) ? value.to_hash : value
hash
end
def instance_of?(const)
self.class == const
end

protected

def deep_hashrize(hash)
hash.inject(TEMPLATE.dup) do |result, (key, value)|
case key.to_sym
when :_include
result.include_modules(value)
when :_access
result.include_accessors(value)
else
result.store(key.to_sym, value.is_a?(Hash) ? deep_hashrize(value) : value)
end
result
end
end

def deep_defaultize(hash)
self.class.defaults.each do |key, value|
case key.to_sym
when :_include
hash.include_modules(value)
when :_access
hash.include_accessors(value)
end
end

hash.each do |key, value|
deep_defaultize(value) if value.is_a?(Hash)
end

hash
end
def is_a?(const)
consts = [self.class]
consts << consts.last.superclass while consts.last.superclass
consts.include?(const)
end
alias kind_of? is_a?
end
Loading

0 comments on commit 13edf19

Please sign in to comment.