hashie 4.1.0 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +138 -53
- data/CONTRIBUTING.md +38 -1
- data/LICENSE +1 -1
- data/README.md +920 -205
- data/Rakefile +32 -1
- data/UPGRADING.md +56 -0
- data/hashie.gemspec +10 -5
- data/lib/hashie/array.rb +15 -12
- data/lib/hashie/clash.rb +59 -4
- data/lib/hashie/dash.rb +160 -32
- data/lib/hashie/extensions/active_support/core_ext/hash.rb +38 -0
- data/lib/hashie/extensions/array/pretty_inspect.rb +20 -0
- data/lib/hashie/extensions/block_access.rb +71 -0
- data/lib/hashie/extensions/coercion.rb +180 -26
- data/lib/hashie/extensions/dash/coercion.rb +34 -3
- data/lib/hashie/extensions/dash/indifferent_access.rb +90 -3
- data/lib/hashie/extensions/dash/lazy.rb +170 -0
- data/lib/hashie/extensions/dash/predefined_values.rb +148 -0
- data/lib/hashie/extensions/dash/property_translation.rb +138 -15
- data/lib/hashie/extensions/deep_fetch.rb +21 -9
- data/lib/hashie/extensions/deep_find.rb +20 -0
- data/lib/hashie/extensions/deep_grep.rb +69 -0
- data/lib/hashie/extensions/deep_locate.rb +16 -29
- data/lib/hashie/extensions/deep_merge.rb +16 -0
- data/lib/hashie/extensions/deep_merge_initializer.rb +42 -0
- data/lib/hashie/extensions/ignore_undeclared.rb +24 -7
- data/lib/hashie/extensions/indifferent_access.rb +124 -5
- data/lib/hashie/extensions/kernel_coercion.rb +58 -0
- data/lib/hashie/extensions/key_conflict_warning.rb +39 -4
- data/lib/hashie/extensions/key_conversion.rb +9 -0
- data/lib/hashie/extensions/mash/define_accessors.rb +71 -8
- data/lib/hashie/extensions/mash/keep_original_keys.rb +13 -9
- data/lib/hashie/extensions/mash/permissive_respond_to.rb +2 -0
- data/lib/hashie/extensions/mash/required_keys.rb +111 -0
- data/lib/hashie/extensions/mash/safe_assignment.rb +22 -4
- data/lib/hashie/extensions/mash/strict_key_access.rb +65 -0
- data/lib/hashie/extensions/mash/symbolize_keys.rb +11 -7
- data/lib/hashie/extensions/mash/transform_keys.rb +86 -0
- data/lib/hashie/extensions/mash/underscore_keys.rb +32 -0
- data/lib/hashie/extensions/merge_initializer.rb +9 -0
- data/lib/hashie/extensions/method_access.rb +94 -8
- data/lib/hashie/extensions/parsers/yaml_erb_parser.rb +28 -35
- data/lib/hashie/extensions/parsers/yaml_parser.rb +81 -0
- data/lib/hashie/extensions/persistable.rb +137 -0
- data/lib/hashie/extensions/pretty_inspect.rb +14 -0
- data/lib/hashie/extensions/ruby_version.rb +18 -1
- data/lib/hashie/extensions/ruby_version_check.rb +17 -0
- data/lib/hashie/extensions/strict_key_access.rb +59 -5
- data/lib/hashie/extensions/stringify_keys.rb +72 -17
- data/lib/hashie/extensions/symbolize_keys.rb +84 -17
- data/lib/hashie/hash.rb +18 -2
- data/lib/hashie/logger.rb +18 -1
- data/lib/hashie/mash.rb +314 -82
- data/lib/hashie/railtie.rb +27 -1
- data/lib/hashie/rash.rb +64 -4
- data/lib/hashie/trash.rb +8 -0
- data/lib/hashie/utils.rb +17 -6
- data/lib/hashie/version.rb +3 -1
- data/lib/hashie.rb +46 -0
- metadata +22 -9
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hashie
|
|
4
|
+
module Extensions
|
|
5
|
+
module Mash
|
|
6
|
+
# Extends a Mash with the ability to require specific keys to be
|
|
7
|
+
# set (non-nil) when constructed, while still allowing arbitrary,
|
|
8
|
+
# undeclared keys to be assigned - unlike Hashie::Dash, which
|
|
9
|
+
# restricts assignment to only pre-declared properties.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
#
|
|
13
|
+
# class PersonMash < Hashie::Mash
|
|
14
|
+
# include Hashie::Extensions::Mash::RequiredKeys
|
|
15
|
+
#
|
|
16
|
+
# required_keys :name, :email
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# expect { PersonMash.new(email: 'bob@example.com') }
|
|
20
|
+
# .to raise_error(ArgumentError, 'the following keys are required: `name`')
|
|
21
|
+
#
|
|
22
|
+
# person = PersonMash.new(name: 'Bob', email: 'bob@example.com')
|
|
23
|
+
# person.age = 42 # arbitrary, undeclared keys are still allowed
|
|
24
|
+
# person.age # => 42
|
|
25
|
+
module RequiredKeys
|
|
26
|
+
# @example Install required-key declarations
|
|
27
|
+
# klass = Class.new(Hashie::Mash) do
|
|
28
|
+
# include Hashie::Extensions::Mash::RequiredKeys
|
|
29
|
+
# end
|
|
30
|
+
# klass.required_keys # => []
|
|
31
|
+
def self.included(base)
|
|
32
|
+
base.instance_variable_set(:@required_keys, [])
|
|
33
|
+
base.extend(ClassMethods)
|
|
34
|
+
base.include(InstanceMethods)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Class-level required-key declarations inherited by subclasses.
|
|
38
|
+
#
|
|
39
|
+
# @example
|
|
40
|
+
# klass = Class.new(Hashie::Mash) do
|
|
41
|
+
# include Hashie::Extensions::Mash::RequiredKeys
|
|
42
|
+
# required_keys :name
|
|
43
|
+
# end
|
|
44
|
+
# klass.required_keys # => ['name']
|
|
45
|
+
module ClassMethods
|
|
46
|
+
# @example Copy required keys when subclassing
|
|
47
|
+
# parent = Class.new(Hashie::Mash) do
|
|
48
|
+
# include Hashie::Extensions::Mash::RequiredKeys
|
|
49
|
+
# required_keys :name
|
|
50
|
+
# end
|
|
51
|
+
# child = Class.new(parent)
|
|
52
|
+
# child.required_keys :email
|
|
53
|
+
# child.required_keys # => ['name', 'email']
|
|
54
|
+
# parent.required_keys # => ['name']
|
|
55
|
+
def inherited(klass)
|
|
56
|
+
super
|
|
57
|
+
klass.instance_variable_set(:@required_keys, required_keys.dup)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# With no arguments, returns the keys currently declared as
|
|
61
|
+
# required. With one or more arguments, declares those keys
|
|
62
|
+
# as required. Keys are stored and compared as strings, so
|
|
63
|
+
# both symbol and string keys are accepted.
|
|
64
|
+
#
|
|
65
|
+
# @example
|
|
66
|
+
# klass = Class.new(Hashie::Mash) do
|
|
67
|
+
# include Hashie::Extensions::Mash::RequiredKeys
|
|
68
|
+
# required_keys :name, 'email', :name
|
|
69
|
+
# end
|
|
70
|
+
# klass.required_keys # => ['name', 'email']
|
|
71
|
+
def required_keys(*keys)
|
|
72
|
+
return @required_keys if keys.empty?
|
|
73
|
+
|
|
74
|
+
@required_keys |= keys.map(&:to_s)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Checks required values when constructing a Mash.
|
|
79
|
+
#
|
|
80
|
+
# @example
|
|
81
|
+
# klass = Class.new(Hashie::Mash) do
|
|
82
|
+
# include Hashie::Extensions::Mash::RequiredKeys
|
|
83
|
+
# required_keys :name
|
|
84
|
+
# end
|
|
85
|
+
# klass.new(name: 'Ada').name # => 'Ada'
|
|
86
|
+
module InstanceMethods
|
|
87
|
+
# @example Reject nil or missing required values
|
|
88
|
+
# klass = Class.new(Hashie::Mash) do
|
|
89
|
+
# include Hashie::Extensions::Mash::RequiredKeys
|
|
90
|
+
# required_keys :name
|
|
91
|
+
# end
|
|
92
|
+
# expect { klass.new(name: nil) }.to raise_error(ArgumentError, /name/)
|
|
93
|
+
# klass.new(name: 'Ada', age: 37).age # => 37
|
|
94
|
+
def initialize(*args, &block)
|
|
95
|
+
super
|
|
96
|
+
assert_required_keys_set!
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def assert_required_keys_set!
|
|
102
|
+
missing_keys = self.class.required_keys.select { |key| self[key].nil? }
|
|
103
|
+
return if missing_keys.empty?
|
|
104
|
+
|
|
105
|
+
raise ArgumentError, "the following keys are required: `#{missing_keys.join(', ')}`"
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -1,14 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Hashie
|
|
2
4
|
module Extensions
|
|
3
5
|
module Mash
|
|
6
|
+
# Rejects assignments that would shadow existing Mash methods.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# klass = Class.new(Hashie::Mash) do
|
|
10
|
+
# include Hashie::Extensions::Mash::SafeAssignment
|
|
11
|
+
# end
|
|
12
|
+
# mash = klass.new
|
|
13
|
+
# expect { mash[:size] = 2 }.to raise_error(ArgumentError, /clashes/)
|
|
14
|
+
# mash.empty? # => true
|
|
4
15
|
module SafeAssignment
|
|
5
|
-
def custom_writer(key, *args)
|
|
6
|
-
if !key?(key) && respond_to?(key, true)
|
|
7
|
-
|
|
8
|
-
end
|
|
16
|
+
def custom_writer(key, *args) # :nodoc:
|
|
17
|
+
raise ArgumentError, "the property `#{key}` clashes with an existing method" if !key?(key) && respond_to?(key, true)
|
|
18
|
+
|
|
9
19
|
super
|
|
10
20
|
end
|
|
11
21
|
|
|
22
|
+
# @example
|
|
23
|
+
# klass = Class.new(Hashie::Mash) do
|
|
24
|
+
# include Hashie::Extensions::Mash::SafeAssignment
|
|
25
|
+
# end
|
|
26
|
+
# mash = klass.new
|
|
27
|
+
# mash[:name] = 'Ada'
|
|
28
|
+
# mash.name # => 'Ada'
|
|
29
|
+
# expect { mash[:size] = 2 }.to raise_error(ArgumentError, /clashes/)
|
|
12
30
|
def []=(*args)
|
|
13
31
|
custom_writer(*args)
|
|
14
32
|
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hashie
|
|
4
|
+
module Extensions
|
|
5
|
+
module Mash
|
|
6
|
+
# Overrides Mash's default behavior of returning nil for keys that
|
|
7
|
+
# aren't set, raising a KeyError instead.
|
|
8
|
+
#
|
|
9
|
+
# Unlike Hashie::Extensions::StrictKeyAccess, this extension is aware
|
|
10
|
+
# of Mash's special suffixed methods (e.g. +?+, +!+, +_+ and +=+) so
|
|
11
|
+
# that they continue to work as expected instead of always raising.
|
|
12
|
+
#
|
|
13
|
+
# Because nested Hashes are converted into instances of +self.class+,
|
|
14
|
+
# this extension is automatically applied to any nested Mash as well.
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# class StrictMash < Hashie::Mash
|
|
18
|
+
# include Hashie::Extensions::Mash::StrictKeyAccess
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# mash = StrictMash.new(name: 'Bob')
|
|
22
|
+
# mash.name #=> 'Bob'
|
|
23
|
+
# expect { mash.age }.to raise_error(KeyError, 'key not found: `age`')
|
|
24
|
+
#
|
|
25
|
+
# mash.name? #=> true
|
|
26
|
+
# mash.age? #=> false
|
|
27
|
+
#
|
|
28
|
+
# mash.address!.city = 'Springfield'
|
|
29
|
+
# mash.address.city #=> 'Springfield'
|
|
30
|
+
#
|
|
31
|
+
# @api public
|
|
32
|
+
module StrictKeyAccess
|
|
33
|
+
# Hook for being included in a class
|
|
34
|
+
#
|
|
35
|
+
# @api private
|
|
36
|
+
# @return [void]
|
|
37
|
+
# @raise [ArgumentError] when the base class isn't a Mash
|
|
38
|
+
def self.included(descendant)
|
|
39
|
+
error_message = "#{descendant} is not a kind of Hashie::Mash"
|
|
40
|
+
raise ArgumentError, error_message unless descendant <= Hashie::Mash
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @api private
|
|
44
|
+
def method_missing(method_name, *args, &blk)
|
|
45
|
+
return super if key?(method_name)
|
|
46
|
+
|
|
47
|
+
name, suffix = method_name_and_suffix(method_name)
|
|
48
|
+
raise KeyError, "key not found: `#{name}`" if !key?(name) && !suffix
|
|
49
|
+
|
|
50
|
+
super
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @api private
|
|
54
|
+
def respond_to_missing?(method_name, *args)
|
|
55
|
+
return true if key?(method_name)
|
|
56
|
+
|
|
57
|
+
_, suffix = method_name_and_suffix(method_name)
|
|
58
|
+
return false unless suffix
|
|
59
|
+
|
|
60
|
+
super
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Hashie
|
|
2
4
|
module Extensions
|
|
3
5
|
module Mash
|
|
@@ -5,11 +7,11 @@ module Hashie
|
|
|
5
7
|
#
|
|
6
8
|
# @example
|
|
7
9
|
# class LazyResponse < Hashie::Mash
|
|
8
|
-
# include Hashie::Extensions::Mash::
|
|
10
|
+
# include Hashie::Extensions::Mash::SymbolizeKeys
|
|
9
11
|
# end
|
|
10
12
|
#
|
|
11
13
|
# response = LazyResponse.new("id" => 123, "name" => "Rey").to_h
|
|
12
|
-
#
|
|
14
|
+
# response # => {id: 123, name: "Rey"}
|
|
13
15
|
#
|
|
14
16
|
# @api public
|
|
15
17
|
module SymbolizeKeys
|
|
@@ -19,18 +21,20 @@ module Hashie
|
|
|
19
21
|
# @return [void]
|
|
20
22
|
# @raise [ArgumentError] when the base class isn't a Mash
|
|
21
23
|
def self.included(base)
|
|
22
|
-
|
|
24
|
+
return if base <= Hashie::Mash
|
|
25
|
+
|
|
26
|
+
raise ArgumentError, "`#{base}` must descend from Hashie::Mash"
|
|
23
27
|
end
|
|
24
28
|
|
|
25
29
|
private
|
|
26
30
|
|
|
27
|
-
# Converts a key to a symbol
|
|
31
|
+
# Converts a key to a symbol, if possible
|
|
28
32
|
#
|
|
29
33
|
# @api private
|
|
30
|
-
# @param [
|
|
31
|
-
# @return [
|
|
34
|
+
# @param [<K>] key the key to attempt convert to a symbol
|
|
35
|
+
# @return [Symbol, K]
|
|
32
36
|
def convert_key(key)
|
|
33
|
-
key.to_sym
|
|
37
|
+
key.respond_to?(:to_sym) ? key.to_sym : key
|
|
34
38
|
end
|
|
35
39
|
end
|
|
36
40
|
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hashie
|
|
4
|
+
module Extensions
|
|
5
|
+
module Mash
|
|
6
|
+
# Applies a class-configured normalization to string and symbol Mash keys.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# klass = Class.new(Hashie::Mash) do
|
|
10
|
+
# include Hashie::Extensions::Mash::TransformKeys
|
|
11
|
+
# transform_keys_with(&:downcase)
|
|
12
|
+
# end
|
|
13
|
+
# klass.new('NAME' => 'Ada').name # => 'Ada'
|
|
14
|
+
module TransformKeys
|
|
15
|
+
# @example Install class-level transformation configuration
|
|
16
|
+
# klass = Class.new(Hashie::Mash) do
|
|
17
|
+
# include Hashie::Extensions::Mash::TransformKeys
|
|
18
|
+
# end
|
|
19
|
+
# klass.key_transformer # => nil
|
|
20
|
+
def self.included(base)
|
|
21
|
+
raise ArgumentError, "`#{base}` must descend from Hashie::Mash" unless base <= Hashie::Mash
|
|
22
|
+
|
|
23
|
+
base.extend(ClassMethods)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Configures key normalization independently for each subclass.
|
|
27
|
+
#
|
|
28
|
+
# @example
|
|
29
|
+
# klass = Class.new(Hashie::Mash) do
|
|
30
|
+
# include Hashie::Extensions::Mash::TransformKeys
|
|
31
|
+
# transform_keys_with(&:downcase)
|
|
32
|
+
# end
|
|
33
|
+
# klass.new('NAME' => 'Ada')['name'] # => 'Ada'
|
|
34
|
+
module ClassMethods
|
|
35
|
+
# @example Inherit a transformer without sharing later configuration
|
|
36
|
+
# parent = Class.new(Hashie::Mash) do
|
|
37
|
+
# include Hashie::Extensions::Mash::TransformKeys
|
|
38
|
+
# transform_keys_with(&:downcase)
|
|
39
|
+
# end
|
|
40
|
+
# child = Class.new(parent)
|
|
41
|
+
# child.new('NAME' => 'Ada').name # => 'Ada'
|
|
42
|
+
# child.transform_keys_with(&:upcase)
|
|
43
|
+
# parent.new('NAME' => 'Ada').keys # => ['name']
|
|
44
|
+
def inherited(klass)
|
|
45
|
+
super
|
|
46
|
+
klass.instance_variable_set(:@key_transformer, key_transformer)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @example Configure with a callable or a block
|
|
50
|
+
# klass = Class.new(Hashie::Mash) do
|
|
51
|
+
# include Hashie::Extensions::Mash::TransformKeys
|
|
52
|
+
# transform_keys_with(->(key) { key.downcase })
|
|
53
|
+
# end
|
|
54
|
+
# klass.new('NAME' => 'Ada').keys # => ['name']
|
|
55
|
+
# klass.transform_keys_with { |key| key.upcase }
|
|
56
|
+
# klass.new(name: 'Ada').keys # => ['NAME']
|
|
57
|
+
def transform_keys_with(callable = nil, &block)
|
|
58
|
+
raise ArgumentError, 'provide one callable or block' if (callable && block) || !(callable || block).respond_to?(:call)
|
|
59
|
+
|
|
60
|
+
@key_transformer = callable || block
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# @example Read the configured callable
|
|
64
|
+
# klass = Class.new(Hashie::Mash) do
|
|
65
|
+
# include Hashie::Extensions::Mash::TransformKeys
|
|
66
|
+
# transform_keys_with(&:downcase)
|
|
67
|
+
# end
|
|
68
|
+
# klass.key_transformer.call('NAME') # => 'name'
|
|
69
|
+
attr_reader :key_transformer
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def convert_key(key)
|
|
75
|
+
transformer = self.class.key_transformer
|
|
76
|
+
return super unless transformer && (key.is_a?(String) || key.is_a?(Symbol))
|
|
77
|
+
|
|
78
|
+
transformed = transformer.call(key.to_s)
|
|
79
|
+
raise TypeError, 'key transformer must return a String' unless transformed.is_a?(String)
|
|
80
|
+
|
|
81
|
+
super(transformed)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'hashie/extensions/mash/transform_keys'
|
|
4
|
+
|
|
5
|
+
module Hashie
|
|
6
|
+
module Extensions
|
|
7
|
+
module Mash
|
|
8
|
+
# Normalizes camel-case and hyphenated Mash keys to lowercase underscores.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# klass = Class.new(Hashie::Mash) do
|
|
12
|
+
# include Hashie::Extensions::Mash::UnderscoreKeys
|
|
13
|
+
# end
|
|
14
|
+
# klass.new('firstName' => 'Ada', 'last-name' => 'Lovelace').to_h # => {'first_name' => 'Ada', 'last_name' => 'Lovelace'}
|
|
15
|
+
module UnderscoreKeys
|
|
16
|
+
# @example Configure underscore normalization on inclusion
|
|
17
|
+
# klass = Class.new(Hashie::Mash) do
|
|
18
|
+
# include Hashie::Extensions::Mash::UnderscoreKeys
|
|
19
|
+
# end
|
|
20
|
+
# klass.key_transformer.call('HTTPStatus') # => 'http_status'
|
|
21
|
+
def self.included(base)
|
|
22
|
+
base.include(TransformKeys) unless base < TransformKeys
|
|
23
|
+
base.transform_keys_with do |key|
|
|
24
|
+
key.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
25
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
26
|
+
.tr('-', '_').downcase
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Hashie
|
|
2
4
|
module Extensions
|
|
3
5
|
# The MergeInitializer is a super-simple mixin that allows
|
|
@@ -15,6 +17,13 @@ module Hashie
|
|
|
15
17
|
# h[:abc] # => 'def'
|
|
16
18
|
#
|
|
17
19
|
module MergeInitializer
|
|
20
|
+
# @example Initialize entries and a default value
|
|
21
|
+
# class InitializedHash < Hash
|
|
22
|
+
# include Hashie::Extensions::MergeInitializer
|
|
23
|
+
# end
|
|
24
|
+
# hash = InitializedHash.new({ name: 'Ada' }, 'unknown')
|
|
25
|
+
# hash[:name] # => 'Ada'
|
|
26
|
+
# hash[:missing] # => 'unknown'
|
|
18
27
|
def initialize(hash = {}, default = nil, &block)
|
|
19
28
|
default ? super(default) : super(&block)
|
|
20
29
|
hash.each do |key, value|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Hashie
|
|
2
4
|
module Extensions
|
|
3
5
|
# MethodReader allows you to access keys of the hash
|
|
@@ -25,13 +27,22 @@ module Hashie
|
|
|
25
27
|
# user[:birthday] = nil
|
|
26
28
|
# user.birthday # => nil
|
|
27
29
|
#
|
|
28
|
-
# user.not_declared
|
|
30
|
+
# expect { user.not_declared }.to raise_error(NoMethodError)
|
|
29
31
|
module MethodReader
|
|
32
|
+
# @example Advertise readers for existing keys
|
|
33
|
+
# hash = { name: 'Ada' }.extend(Hashie::Extensions::MethodReader)
|
|
34
|
+
# hash.respond_to?(:name) # => true
|
|
35
|
+
# hash.respond_to?(:missing) # => false
|
|
30
36
|
def respond_to_missing?(name, include_private = false)
|
|
31
37
|
return true if key?(name.to_s) || key?(name.to_sym)
|
|
38
|
+
|
|
32
39
|
super
|
|
33
40
|
end
|
|
34
41
|
|
|
42
|
+
# @example Read both symbol and string keys
|
|
43
|
+
# hash = { name: 'Ada', 'age' => 42 }.extend(Hashie::Extensions::MethodReader)
|
|
44
|
+
# hash.name # => 'Ada'
|
|
45
|
+
# hash.age # => 42
|
|
35
46
|
def method_missing(name, *args)
|
|
36
47
|
if key?(name)
|
|
37
48
|
self[name]
|
|
@@ -67,19 +78,29 @@ module Hashie
|
|
|
67
78
|
# h['awesome'] # => 'sauce'
|
|
68
79
|
#
|
|
69
80
|
module MethodWriter
|
|
81
|
+
# @example Advertise arbitrary writers
|
|
82
|
+
# hash = {}.extend(Hashie::Extensions::MethodWriter)
|
|
83
|
+
# hash.respond_to?(:name=) # => true
|
|
84
|
+
# hash.respond_to?(:name) # => false
|
|
70
85
|
def respond_to_missing?(name, include_private = false)
|
|
71
86
|
return true if name.to_s =~ /=$/
|
|
87
|
+
|
|
72
88
|
super
|
|
73
89
|
end
|
|
74
90
|
|
|
91
|
+
# @example Write string keys with assignment syntax
|
|
92
|
+
# hash = {}.extend(Hashie::Extensions::MethodWriter)
|
|
93
|
+
# hash.name = 'Ada'
|
|
94
|
+
# hash # => { 'name' => 'Ada' }
|
|
75
95
|
def method_missing(name, *args)
|
|
76
|
-
if args.size == 1 && name.to_s =~ /(.*)=$/
|
|
77
|
-
return self[convert_key(Regexp.last_match[1])] = args.first
|
|
78
|
-
end
|
|
96
|
+
return self[convert_key(Regexp.last_match[1])] = args.first if args.size == 1 && name.to_s =~ /(.*)=$/
|
|
79
97
|
|
|
80
98
|
super
|
|
81
99
|
end
|
|
82
100
|
|
|
101
|
+
# @example Normalize a writer key
|
|
102
|
+
# hash = {}.extend(Hashie::Extensions::MethodWriter)
|
|
103
|
+
# hash.convert_key(:name) # => 'name'
|
|
83
104
|
def convert_key(key)
|
|
84
105
|
key.to_s
|
|
85
106
|
end
|
|
@@ -104,8 +125,12 @@ module Hashie
|
|
|
104
125
|
# h.abc? # => true
|
|
105
126
|
# h['def'] = nil
|
|
106
127
|
# h.def? # => false
|
|
107
|
-
# h.hji?
|
|
128
|
+
# expect { h.hji? }.to raise_error(NoMethodError)
|
|
108
129
|
module MethodQuery
|
|
130
|
+
# @example Advertise queries for present keys, including nil values
|
|
131
|
+
# hash = { name: nil }.extend(Hashie::Extensions::MethodQuery)
|
|
132
|
+
# hash.respond_to?(:name?) # => true
|
|
133
|
+
# hash.respond_to?(:missing?) # => false
|
|
109
134
|
def respond_to_missing?(name, include_private = false)
|
|
110
135
|
if query_method?(name) && indifferent_key?(key_from_query_method(name))
|
|
111
136
|
true
|
|
@@ -114,6 +139,10 @@ module Hashie
|
|
|
114
139
|
end
|
|
115
140
|
end
|
|
116
141
|
|
|
142
|
+
# @example Query the truthiness of values
|
|
143
|
+
# hash = { active: true, disabled: nil }.extend(Hashie::Extensions::MethodQuery)
|
|
144
|
+
# hash.active? # => true
|
|
145
|
+
# hash.disabled? # => false
|
|
117
146
|
def method_missing(name, *args)
|
|
118
147
|
return super unless args.empty?
|
|
119
148
|
|
|
@@ -148,6 +177,12 @@ module Hashie
|
|
|
148
177
|
# A macro module that will automatically include MethodReader,
|
|
149
178
|
# MethodWriter, and MethodQuery, giving you the ability to read,
|
|
150
179
|
# write, and query keys in a hash using method call shortcuts.
|
|
180
|
+
#
|
|
181
|
+
# @example Read, write, and query through methods
|
|
182
|
+
# hash = {}.extend(Module.new { include Hashie::Extensions::MethodAccess })
|
|
183
|
+
# hash.name = 'Ada'
|
|
184
|
+
# hash.name # => 'Ada'
|
|
185
|
+
# hash.name? # => true
|
|
151
186
|
module MethodAccess
|
|
152
187
|
def self.included(base)
|
|
153
188
|
[MethodReader, MethodWriter, MethodQuery].each do |mod|
|
|
@@ -158,6 +193,16 @@ module Hashie
|
|
|
158
193
|
|
|
159
194
|
# A module shared between MethodOverridingWriter and MethodOverridingInitializer
|
|
160
195
|
# to contained shared logic. This module aids in redefining existing hash methods.
|
|
196
|
+
#
|
|
197
|
+
# @example Preserve an overridden method under a double-underscore alias
|
|
198
|
+
# require 'hashie/extensions/method_access'
|
|
199
|
+
# class OverridingHash < Hash
|
|
200
|
+
# include Hashie::Extensions::MethodOverridingWriter
|
|
201
|
+
# end
|
|
202
|
+
# hash = OverridingHash.new
|
|
203
|
+
# hash.size = 'large'
|
|
204
|
+
# hash.size # => 'large'
|
|
205
|
+
# hash.__size # => 1
|
|
161
206
|
module RedefineMethod
|
|
162
207
|
protected
|
|
163
208
|
|
|
@@ -185,6 +230,7 @@ module Hashie
|
|
|
185
230
|
# appropriately as true.
|
|
186
231
|
#
|
|
187
232
|
# @example
|
|
233
|
+
# require 'hashie/extensions/method_access'
|
|
188
234
|
# class MyHash < Hash
|
|
189
235
|
# include Hashie::Extensions::MethodOverridingWriter
|
|
190
236
|
# end
|
|
@@ -194,15 +240,25 @@ module Hashie
|
|
|
194
240
|
# h['awesome'] # => 'sauce'
|
|
195
241
|
# h.zip = 'a-dee-doo-dah'
|
|
196
242
|
# h.zip # => 'a-dee-doo-dah'
|
|
197
|
-
# h.__zip # => [[['awesome', 'sauce'], ['zip', 'a-dee-doo-dah']]]
|
|
243
|
+
# h.__zip # => [[['awesome', 'sauce']], [['zip', 'a-dee-doo-dah']]]
|
|
198
244
|
#
|
|
199
245
|
module MethodOverridingWriter
|
|
200
246
|
include RedefineMethod
|
|
201
247
|
|
|
248
|
+
# @example Normalize keys before storage
|
|
249
|
+
# require 'hashie/extensions/method_access'
|
|
250
|
+
# hash = {}.extend(Hashie::Extensions::MethodOverridingWriter)
|
|
251
|
+
# hash.convert_key(:size) # => 'size'
|
|
202
252
|
def convert_key(key)
|
|
203
253
|
key.to_s
|
|
204
254
|
end
|
|
205
255
|
|
|
256
|
+
# @example Override a Hash method and keep access to the original
|
|
257
|
+
# require 'hashie/extensions/method_access'
|
|
258
|
+
# hash = {}.extend(Hashie::Extensions::MethodOverridingWriter)
|
|
259
|
+
# hash.size = 'large'
|
|
260
|
+
# hash.size # => 'large'
|
|
261
|
+
# hash.__size # => 1
|
|
206
262
|
def method_missing(name, *args)
|
|
207
263
|
if args.size == 1 && name.to_s =~ /(.*)=$/
|
|
208
264
|
key = Regexp.last_match[1]
|
|
@@ -213,8 +269,13 @@ module Hashie
|
|
|
213
269
|
super
|
|
214
270
|
end
|
|
215
271
|
|
|
272
|
+
# @example Advertise method-overriding writers
|
|
273
|
+
# require 'hashie/extensions/method_access'
|
|
274
|
+
# hash = {}.extend(Hashie::Extensions::MethodOverridingWriter)
|
|
275
|
+
# hash.respond_to?(:size=) # => true
|
|
216
276
|
def respond_to_missing?(name, include_private = false)
|
|
217
277
|
return true if name.to_s.end_with?('=')
|
|
278
|
+
|
|
218
279
|
super
|
|
219
280
|
end
|
|
220
281
|
|
|
@@ -231,6 +292,16 @@ module Hashie
|
|
|
231
292
|
# shortcuts that can override object methods. Any overridden
|
|
232
293
|
# object method is automatically aliased with two leading
|
|
233
294
|
# underscores.
|
|
295
|
+
#
|
|
296
|
+
# @example Initialize, read, write, and query a conflicting key
|
|
297
|
+
# class OverridingHash < Hash
|
|
298
|
+
# include Hashie::Extensions::MethodAccessWithOverride
|
|
299
|
+
# end
|
|
300
|
+
# hash = OverridingHash.new(size: 'large')
|
|
301
|
+
# hash.size # => 'large'
|
|
302
|
+
# hash.size = 'small'
|
|
303
|
+
# hash.size? # => true
|
|
304
|
+
# hash.__size # => 1
|
|
234
305
|
module MethodAccessWithOverride
|
|
235
306
|
def self.included(base)
|
|
236
307
|
[MethodReader, MethodOverridingWriter,
|
|
@@ -241,10 +312,11 @@ module Hashie
|
|
|
241
312
|
end
|
|
242
313
|
|
|
243
314
|
# MethodOverridingInitializer allows you to override default hash
|
|
244
|
-
# methods when passing in values from an existing hash. The
|
|
315
|
+
# methods when passing in values from an existing hash. The overridden
|
|
245
316
|
# methods are aliased with two leading underscores.
|
|
246
317
|
#
|
|
247
318
|
# @example
|
|
319
|
+
# require 'hashie/extensions/method_access'
|
|
248
320
|
# class MyHash < Hash
|
|
249
321
|
# include Hashie::Extensions::MethodOverridingInitializer
|
|
250
322
|
# end
|
|
@@ -255,7 +327,21 @@ module Hashie
|
|
|
255
327
|
module MethodOverridingInitializer
|
|
256
328
|
include RedefineMethod
|
|
257
329
|
|
|
258
|
-
|
|
330
|
+
# @example Initialize conflicting keys and preserve a default
|
|
331
|
+
# require 'hashie/extensions/method_access'
|
|
332
|
+
# class OverridingHash < Hash
|
|
333
|
+
# include Hashie::Extensions::MethodOverridingInitializer
|
|
334
|
+
# end
|
|
335
|
+
# hash = OverridingHash.new({ size: 'large' }, false)
|
|
336
|
+
# hash.size # => 'large'
|
|
337
|
+
# hash.__size # => 1
|
|
338
|
+
# hash[:missing] # => false
|
|
339
|
+
def initialize(hash = {}, default = nil, &block)
|
|
340
|
+
raise ArgumentError, 'cannot specify both a default value and a default block' if default && block
|
|
341
|
+
|
|
342
|
+
self.default = default unless default.nil?
|
|
343
|
+
self.default_proc = block if block
|
|
344
|
+
|
|
259
345
|
hash.each do |key, value|
|
|
260
346
|
skey = key.to_s
|
|
261
347
|
redefine_method(skey) if method?(skey)
|
|
@@ -1,46 +1,39 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
require 'erb'
|
|
3
|
-
require '
|
|
4
|
+
require 'hashie/extensions/parsers/yaml_parser'
|
|
4
5
|
|
|
5
6
|
module Hashie
|
|
6
7
|
module Extensions
|
|
7
8
|
module Parsers
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
# Parses YAML after evaluating ERB templates.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# require 'tempfile'
|
|
13
|
+
# file = Tempfile.new(['hashie-example', '.yml'], '.')
|
|
14
|
+
# begin
|
|
15
|
+
# file.write("count: <%= 1 + 2 %>\n")
|
|
16
|
+
# file.close
|
|
17
|
+
# Hashie::Extensions::Parsers::YamlErbParser.perform(file.path) # => {'count' => 3}
|
|
18
|
+
# ensure
|
|
19
|
+
# file.close!
|
|
20
|
+
# end
|
|
21
|
+
class YamlErbParser < YamlParser
|
|
22
|
+
# @example Evaluate ERB before safely loading YAML
|
|
23
|
+
# require 'tempfile'
|
|
24
|
+
# file = Tempfile.new(['hashie-example', '.yml'], '.')
|
|
25
|
+
# begin
|
|
26
|
+
# file.write("count: <%= 2 * 3 %>\n")
|
|
27
|
+
# file.close
|
|
28
|
+
# parser = Hashie::Extensions::Parsers::YamlErbParser.new(file.path)
|
|
29
|
+
# parser.perform # => {'count' => 6}
|
|
30
|
+
# ensure
|
|
31
|
+
# file.close!
|
|
32
|
+
# end
|
|
15
33
|
def perform
|
|
16
34
|
template = ERB.new(@content)
|
|
17
35
|
template.filename = @file_path
|
|
18
|
-
|
|
19
|
-
permitted_symbols = @options.fetch(:permitted_symbols) { [] }
|
|
20
|
-
aliases = @options.fetch(:aliases) { true }
|
|
21
|
-
|
|
22
|
-
yaml_safe_load(template, permitted_classes, permitted_symbols, aliases)
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def self.perform(file_path, options = {})
|
|
26
|
-
new(file_path, options).perform
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
private
|
|
30
|
-
|
|
31
|
-
if Gem::Version.new(Psych::VERSION) >= Gem::Version.new('3.1.0') # Ruby 2.6+
|
|
32
|
-
def yaml_safe_load(template, permitted_classes, permitted_symbols, aliases)
|
|
33
|
-
YAML.safe_load(
|
|
34
|
-
template.result,
|
|
35
|
-
permitted_classes: permitted_classes,
|
|
36
|
-
permitted_symbols: permitted_symbols,
|
|
37
|
-
aliases: aliases
|
|
38
|
-
)
|
|
39
|
-
end
|
|
40
|
-
else
|
|
41
|
-
def yaml_safe_load(template, permitted_classes, permitted_symbols, aliases)
|
|
42
|
-
YAML.safe_load(template.result, permitted_classes, permitted_symbols, aliases)
|
|
43
|
-
end
|
|
36
|
+
yaml_safe_load(template.result)
|
|
44
37
|
end
|
|
45
38
|
end
|
|
46
39
|
end
|