Allow options on Mash.load (#474)

`Mash.load` uses the Ruby standard library to load Yaml-serialized files
into a Mash. The original implementation used `YAML.load` for this
purpose. However, that method is inherently unsafe so we switched to
using `YAML.safe_load`.

Safely loading Yaml files has many different domain-specific
configuration flags that we did not, by default, expose. This change
introduces the ability to configure the safe loading of Yaml files so
that all types of Yaml can be loaded when necessary using the flags from
the standard library.

This implementation preserves the backwards-compatibility with the prior
implementation so that it should not require updates from users of the
current `Mash.load` behavior. For those who this change affects, we
included upgrading documentation to ease the transition.
This commit is contained in:
Daniel Doubrovkine (dB.) @dblockdotorg 2019-03-22 15:04:22 -04:00 committed by Michael Herold
parent dc64b1024c
commit 30ab2a3cb0
9 changed files with 102 additions and 15 deletions

View File

@ -1,6 +1,6 @@
# This configuration was generated by # This configuration was generated by
# `rubocop --auto-gen-config` # `rubocop --auto-gen-config`
# on 2018-09-30 14:46:03 -0500 using RuboCop version 0.52.1. # on 2019-03-22 11:21:24 -0400 using RuboCop version 0.52.1.
# The point is for the user to remove these configuration records # The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base. # one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new # Note that changes in the inspected code, or installation of new
@ -19,7 +19,7 @@ Metrics/ClassLength:
Metrics/CyclomaticComplexity: Metrics/CyclomaticComplexity:
Max: 11 Max: 11
# Offense count: 17 # Offense count: 19
# Configuration parameters: CountComments. # Configuration parameters: CountComments.
Metrics/MethodLength: Metrics/MethodLength:
Max: 28 Max: 28
@ -28,6 +28,6 @@ Metrics/MethodLength:
Metrics/PerceivedComplexity: Metrics/PerceivedComplexity:
Max: 10 Max: 10
# Offense count: 37 # Offense count: 39
Style/Documentation: Style/Documentation:
Enabled: false Enabled: false

View File

@ -13,6 +13,7 @@ scheme are considered to be bugs.
### Added ### Added
* [#323](https://github.com/intridea/hashie/pull/323): Added `Hashie::Extensions::Mash::DefineAccessors` - [@marshall-lee](https://github.com/marshall-lee). * [#323](https://github.com/intridea/hashie/pull/323): Added `Hashie::Extensions::Mash::DefineAccessors` - [@marshall-lee](https://github.com/marshall-lee).
* [#474](https://github.com/intridea/hashie/pull/474): Expose `YAML#safe_load` options in `Mash#load` - [@riouruma](https://github.com/riouruma), [@dblock](https://github.com/dblock).
### Changed ### Changed

View File

@ -566,7 +566,7 @@ Twitter.extend mash.to_module # NOTE: if you want another name than settings, ca
Twitter.settings.api_key # => 'abcd' Twitter.settings.api_key # => 'abcd'
``` ```
You can use another parser (by default: YamlErbParser): You can use another parser (by default: [YamlErbParser](lib/hashie/extensions/parsers/yaml_erb_parser.rb)):
``` ```
#/etc/data/user.csv #/etc/data/user.csv
@ -582,6 +582,14 @@ mash = Mash.load('data/user.csv', parser: MyCustomCsvParser)
mash[1] #=> { name: 'John', lastname: 'Doe' } mash[1] #=> { name: 'John', lastname: 'Doe' }
``` ```
The `Mash#load` method calls `YAML.safe_load(path, [], [], true)`.
Specify `whitelist_symbols`, `whitelist_classes` and `aliases` options as needed.
```ruby
Mash.load('data/user.csv', whitelist_classes: [Symbol], whitelist_symbols: [], aliases: false)
```
### Mash Extension: KeepOriginalKeys ### Mash Extension: KeepOriginalKeys
This extension can be mixed into a Mash to keep the form of any keys passed directly into the Mash. By default, Mash converts keys to strings to give indifferent access. This extension still allows indifferent access, but keeps the form of the keys to eliminate confusion when you're not expecting the keys to change. This extension can be mixed into a Mash to keep the form of any keys passed directly into the Mash. By default, Mash converts keys to strings to give indifferent access. This extension still allows indifferent access, but keeps the form of the keys to eliminate confusion when you're not expecting the keys to change.

View File

@ -1,6 +1,41 @@
Upgrading Hashie Upgrading Hashie
================ ================
### Upgrading to 3.7.0
#### Mash#load takes options
The `Hashie::Mash#load` method now accepts options, changing the interface of `Parser#initialize`. If you have a custom parser, you must update its `initialize` method.
For example, `Hashie::Extensions::Parsers::YamlErbParser` now accepts `whitelist_classes`, `whitelist_symbols` and `aliases` options.
Before:
```ruby
class Hashie::Extensions::Parsers::YamlErbParser
def initialize(file_path)
@file_path = file_path
end
end
```
After:
```ruby
class Hashie::Extensions::Parsers::YamlErbParser
def initialize(file_path, options = {})
@file_path = file_path
@options = options
end
end
```
Options can now be passed into `Mash#load`.
```ruby
Mash.load(filename, whitelist_classes: [])
```
### Upgrading to 3.5.2 ### Upgrading to 3.5.2
#### Disable logging in Mash subclasses #### Disable logging in Mash subclasses

View File

@ -6,19 +6,23 @@ module Hashie
module Extensions module Extensions
module Parsers module Parsers
class YamlErbParser class YamlErbParser
def initialize(file_path) def initialize(file_path, options = {})
@content = File.read(file_path) @content = File.read(file_path)
@file_path = file_path.is_a?(Pathname) ? file_path.to_s : file_path @file_path = file_path.is_a?(Pathname) ? file_path.to_s : file_path
@options = options
end end
def perform def perform
template = ERB.new(@content) template = ERB.new(@content)
template.filename = @file_path template.filename = @file_path
YAML.safe_load template.result, [], [], true whitelist_classes = @options.fetch(:whitelist_classes) { [] }
whitelist_symbols = @options.fetch(:whitelist_symbols) { [] }
aliases = @options.fetch(:aliases) { true }
YAML.safe_load template.result, whitelist_classes, whitelist_symbols, aliases
end end
def self.perform(file_path) def self.perform(file_path, options = {})
new(file_path).perform new(file_path, options).perform
end end
end end
end end

View File

@ -107,7 +107,7 @@ module Hashie
raise ArgumentError, "The following file doesn't exist: #{path}" unless File.file?(path) raise ArgumentError, "The following file doesn't exist: #{path}" unless File.file?(path)
parser = options.fetch(:parser) { Hashie::Extensions::Parsers::YamlErbParser } parser = options.fetch(:parser) { Hashie::Extensions::Parsers::YamlErbParser }
@_mashes[path] = new(parser.perform(path)).freeze @_mashes[path] = new(parser.perform(path, options.except(:parser))).freeze
end end
def to_module(mash_method_name = :settings) def to_module(mash_method_name = :settings)

View File

@ -1,3 +1,3 @@
module Hashie module Hashie
VERSION = '3.6.1'.freeze VERSION = '3.7.0'.freeze
end end

3
spec/fixtures/yaml_with_symbols.yml vendored Normal file
View File

@ -0,0 +1,3 @@
:user_icon:
:width: 200
:height: 200

View File

@ -645,7 +645,7 @@ describe Hashie::Mash do
context 'if the file exists' do context 'if the file exists' do
before do before do
expect(File).to receive(:file?).with(path).and_return(true) expect(File).to receive(:file?).with(path).and_return(true)
expect(parser).to receive(:perform).with(path).and_return(config) expect(parser).to receive(:perform).with(path, {}).and_return(config)
end end
it { is_expected.to be_a(Hashie::Mash) } it { is_expected.to be_a(Hashie::Mash) }
@ -677,7 +677,7 @@ describe Hashie::Mash do
before do before do
expect(File).to receive(:file?).with(path).and_return(true) expect(File).to receive(:file?).with(path).and_return(true)
expect(parser).to receive(:perform).with(path).and_return(config) expect(parser).to receive(:perform).with(path, {}).and_return(config)
end end
it 'return a Mash from a file' do it 'return a Mash from a file' do
@ -693,8 +693,8 @@ describe Hashie::Mash do
before do before do
expect(File).to receive(:file?).with(path).and_return(true) expect(File).to receive(:file?).with(path).and_return(true)
expect(File).to receive(:file?).with("#{path}+1").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, {}).and_return(config)
expect(parser).to receive(:perform).once.with("#{path}+1").and_return(config) expect(parser).to receive(:perform).once.with("#{path}+1", {}).and_return(config)
end end
it 'cache the loaded yml file', :test_cache do it 'cache the loaded yml file', :test_cache do
@ -710,9 +710,45 @@ describe Hashie::Mash do
context 'when the file has aliases in it' do context 'when the file has aliases in it' do
it 'can use the aliases and does not raise an error' do it 'can use the aliases and does not raise an error' do
mash = Hashie::Mash.load('spec/fixtures/yaml_with_aliases.yml') mash = Hashie::Mash.load('spec/fixtures/yaml_with_aliases.yml')
expect(mash.company_a.accounts.admin.password).to eq('secret') expect(mash.company_a.accounts.admin.password).to eq('secret')
end end
it 'can override the value of aliases' do
expect do
Hashie::Mash.load('spec/fixtures/yaml_with_aliases.yml', aliases: false)
end.to raise_error Psych::BadAlias, /base_accounts/
end
end
context 'when the file has symbols' do
it 'can override the value of whitelist_classes' do
mash = Hashie::Mash.load('spec/fixtures/yaml_with_symbols.yml', whitelist_classes: [Symbol])
expect(mash.user_icon.width).to eq(200)
end
it 'uses defaults for whitelist_classes' do
expect do
Hashie::Mash.load('spec/fixtures/yaml_with_symbols.yml')
end.to raise_error Psych::DisallowedClass, /Symbol/
end
it 'can override the value of whitelist_symbols' do
mash = Hashie::Mash.load('spec/fixtures/yaml_with_symbols.yml',
whitelist_classes: [Symbol],
whitelist_symbols: %i[
user_icon
width
height
])
expect(mash.user_icon.width).to eq(200)
end
it 'raises an error on insufficient whitelist_symbols' do
expect do
Hashie::Mash.load('spec/fixtures/yaml_with_symbols.yml',
whitelist_classes: [Symbol],
whitelist_symbols: %i[
user_icon
width
])
end.to raise_error Psych::DisallowedClass, /Symbol/
end
end end
end end