Skip to content

Commit

Permalink
Added Mash#load with YAML file support.
Browse files Browse the repository at this point in the history
  • Loading branch information
gregory authored and dblock committed Jul 14, 2014
1 parent bff2a89 commit 59069f1
Show file tree
Hide file tree
Showing 6 changed files with 180 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## 3.2.1 (Next)

* [#183](https://github.com/intridea/hashie/pull/183) Added Mash#load with YAML file support - [@gregory](https://github.com/gregory).
* Your contribution here.

## 3.2.0 (7/10/2014)
Expand Down
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,49 @@ mash.inspect # => <Hashie::Mash>

**Note:** The `?` method will return false if a key has been set to false or nil. In order to check if a key has been set at all, use the `mash.key?('some_key')` method instead.

Mash allows you also to transform any files into a Mash objects.

### Example:

```yml
#/etc/config/settings/twitter.yml
development:
api_key: 'api_key'
production:
api_key: <%= ENV['API_KEY'] %> #let's say that ENV['API_KEY'] is set to 'abcd'
```

```ruby
mash = Mash.load('settings/twitter.yml')
mash.development.api_key # => 'localhost'
mash.development.api_key = "foo" # => <# RuntimeError can't modify frozen ...>
mash.development.api_key? # => true
```

You can access a Mash from another class:

```ruby
mash = Mash.load('settings/twitter.yml')[ENV['RACK_ENV']]
Twitter.extend mash.to_module # NOTE: if you want another name than settings, call: to_module('my_settings')
Twitter.settings.api_key # => 'abcd'
```

You can use another parser (by default: YamlErbParser):

```
#/etc/data/user.csv
id | name | lastname
---|------------- | -------------
1 |John | Doe
2 |Laurent | Garnier
```

```ruby
mash = Mash.load('data/user.csv', parser: MyCustomCsvParser)
# => { 1 => { name: 'John', lastname: 'Doe'}, 2 => { name: 'Laurent', lastname: 'Garnier' } }
mash[1] #=> { name: 'John', lastname: 'Doe' }
```

## Dash

Dash is an extended Hash that has a discrete set of defined properties and only those properties may be set on the hash. Additionally, you can set defaults for each property. You can also flag a property as required. Required properties will raise an exception if unset.
Expand Down
4 changes: 4 additions & 0 deletions lib/hashie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ module Extensions
autoload :PrettyInspect, 'hashie/extensions/pretty_inspect'
autoload :KeyConversion, 'hashie/extensions/key_conversion'

module Parsers
autoload :YamlErbParser, 'hashie/extensions/parsers/yaml_erb_parser'
end

module Dash
autoload :IndifferentAccess, 'hashie/extensions/dash/indifferent_access'
end
Expand Down
21 changes: 21 additions & 0 deletions lib/hashie/extensions/parsers/yaml_erb_parser.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
require 'yaml'
require 'erb'
module Hashie
module Extensions
module Parsers
class YamlErbParser
def initialize(file_path)
@content = File.read(file_path)
end

def perform
YAML.load ERB.new(@content).result
end

def self.perform(file_path)
new(file_path).perform
end
end
end
end
end
19 changes: 19 additions & 0 deletions lib/hashie/mash.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,25 @@ class Mash < Hash

ALLOWED_SUFFIXES = %w(? ! = _)

def self.load(path, options = {})
@_mashes ||= new do |h, file_path|
fail ArgumentError, "The following file doesn't exist: #{file_path}" unless File.file?(file_path)

parser = options.fetch(:parser) { Hashie::Extensions::Parsers::YamlErbParser }
h[file_path] = new(parser.perform(file_path)).freeze
end
@_mashes[path]
end

def to_module(mash_method_name = :settings)
mash = self
Module.new do |m|
m.send :define_method, mash_method_name.to_sym do
mash
end
end
end

alias_method :to_s, :inspect

# If you pass in an existing hash, it will
Expand Down
92 changes: 92 additions & 0 deletions spec/hashie/mash_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -499,4 +499,96 @@ class SubMash < Hashie::Mash
end
end
end

describe '.load(filename, options = {})' do
let(:config) do
{
'production' => {
'foo' => 'production_foo'
}
}
end
let(:path) { 'database.yml' }
let(:parser) { double(:parser) }

subject { described_class.load(path, parser: parser) }

before do |ex|
unless ex.metadata == :test_cache
described_class.instance_variable_set('@_mashes', nil) # clean the cached mashes
end
end

context 'if the file exists' do
before do
expect(File).to receive(:file?).with(path).and_return(true)
expect(parser).to receive(:perform).with(path).and_return(config)
end

it { is_expected.to be_a(Hashie::Mash) }

it 'return a Mash from a file' do
expect(subject.production).not_to be_nil
expect(subject.production.keys).to eq config['production'].keys
expect(subject.production.foo).to eq config['production']['foo']
end

it 'freeze the attribtues' do
expect { subject.production = {} }.to raise_exception(RuntimeError, /can't modify frozen/)
end
end

context 'if the fils does not exists' do
before do
expect(File).to receive(:file?).with(path).and_return(false)
end

it 'raise an ArgumentError' do
expect { subject }.to raise_exception(ArgumentError)
end
end

describe 'results are cached' do
let(:parser) { double(:parser) }

subject { described_class.load(path, parser: parser) }

before do
expect(File).to receive(:file?).with(path).and_return(true)
expect(File).to receive(:file?).with("#{path}+1").and_return(true)
expect(parser).to receive(:perform).once.with(path).and_return(config)
expect(parser).to receive(:perform).once.with("#{path}+1").and_return(config)
end

it 'cache the loaded yml file', :test_cache do
2.times do
expect(subject).to be_a(described_class)
expect(described_class.load("#{path}+1", parser: parser)).to be_a(described_class)
end

expect(subject.object_id).to eq subject.object_id
end
end
end

describe '#to_module(mash_method_name)' do
let(:mash) { described_class.new }
subject { Class.new.extend mash.to_module }

it 'defines a settings method on the klass class that extends the module' do
expect(subject).to respond_to(:settings)
expect(subject.settings).to eq mash
end

context 'when a settings_method_name is set' do
let(:mash_method_name) { 'config' }

subject { Class.new.extend mash.to_module(mash_method_name) }

it 'defines a settings method on the klass class that extends the module' do
expect(subject).to respond_to(mash_method_name.to_sym)
expect(subject.send(mash_method_name.to_sym)).to eq mash
end
end
end
end

0 comments on commit 59069f1

Please sign in to comment.