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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +138 -53
  3. data/CONTRIBUTING.md +38 -1
  4. data/LICENSE +1 -1
  5. data/README.md +920 -205
  6. data/Rakefile +32 -1
  7. data/UPGRADING.md +56 -0
  8. data/hashie.gemspec +10 -5
  9. data/lib/hashie/array.rb +15 -12
  10. data/lib/hashie/clash.rb +59 -4
  11. data/lib/hashie/dash.rb +160 -32
  12. data/lib/hashie/extensions/active_support/core_ext/hash.rb +38 -0
  13. data/lib/hashie/extensions/array/pretty_inspect.rb +20 -0
  14. data/lib/hashie/extensions/block_access.rb +71 -0
  15. data/lib/hashie/extensions/coercion.rb +180 -26
  16. data/lib/hashie/extensions/dash/coercion.rb +34 -3
  17. data/lib/hashie/extensions/dash/indifferent_access.rb +90 -3
  18. data/lib/hashie/extensions/dash/lazy.rb +170 -0
  19. data/lib/hashie/extensions/dash/predefined_values.rb +148 -0
  20. data/lib/hashie/extensions/dash/property_translation.rb +138 -15
  21. data/lib/hashie/extensions/deep_fetch.rb +21 -9
  22. data/lib/hashie/extensions/deep_find.rb +20 -0
  23. data/lib/hashie/extensions/deep_grep.rb +69 -0
  24. data/lib/hashie/extensions/deep_locate.rb +16 -29
  25. data/lib/hashie/extensions/deep_merge.rb +16 -0
  26. data/lib/hashie/extensions/deep_merge_initializer.rb +42 -0
  27. data/lib/hashie/extensions/ignore_undeclared.rb +24 -7
  28. data/lib/hashie/extensions/indifferent_access.rb +124 -5
  29. data/lib/hashie/extensions/kernel_coercion.rb +58 -0
  30. data/lib/hashie/extensions/key_conflict_warning.rb +39 -4
  31. data/lib/hashie/extensions/key_conversion.rb +9 -0
  32. data/lib/hashie/extensions/mash/define_accessors.rb +71 -8
  33. data/lib/hashie/extensions/mash/keep_original_keys.rb +13 -9
  34. data/lib/hashie/extensions/mash/permissive_respond_to.rb +2 -0
  35. data/lib/hashie/extensions/mash/required_keys.rb +111 -0
  36. data/lib/hashie/extensions/mash/safe_assignment.rb +22 -4
  37. data/lib/hashie/extensions/mash/strict_key_access.rb +65 -0
  38. data/lib/hashie/extensions/mash/symbolize_keys.rb +11 -7
  39. data/lib/hashie/extensions/mash/transform_keys.rb +86 -0
  40. data/lib/hashie/extensions/mash/underscore_keys.rb +32 -0
  41. data/lib/hashie/extensions/merge_initializer.rb +9 -0
  42. data/lib/hashie/extensions/method_access.rb +94 -8
  43. data/lib/hashie/extensions/parsers/yaml_erb_parser.rb +28 -35
  44. data/lib/hashie/extensions/parsers/yaml_parser.rb +81 -0
  45. data/lib/hashie/extensions/persistable.rb +137 -0
  46. data/lib/hashie/extensions/pretty_inspect.rb +14 -0
  47. data/lib/hashie/extensions/ruby_version.rb +18 -1
  48. data/lib/hashie/extensions/ruby_version_check.rb +17 -0
  49. data/lib/hashie/extensions/strict_key_access.rb +59 -5
  50. data/lib/hashie/extensions/stringify_keys.rb +72 -17
  51. data/lib/hashie/extensions/symbolize_keys.rb +84 -17
  52. data/lib/hashie/hash.rb +18 -2
  53. data/lib/hashie/logger.rb +18 -1
  54. data/lib/hashie/mash.rb +314 -82
  55. data/lib/hashie/railtie.rb +27 -1
  56. data/lib/hashie/rash.rb +64 -4
  57. data/lib/hashie/trash.rb +8 -0
  58. data/lib/hashie/utils.rb +17 -6
  59. data/lib/hashie/version.rb +3 -1
  60. data/lib/hashie.rb +46 -0
  61. 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) #:nodoc:
6
- if !key?(key) && respond_to?(key, true)
7
- raise ArgumentError, "The property #{key} clashes with an existing method."
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::SymbolizedKeys
10
+ # include Hashie::Extensions::Mash::SymbolizeKeys
9
11
  # end
10
12
  #
11
13
  # response = LazyResponse.new("id" => 123, "name" => "Rey").to_h
12
- # #=> {id: 123, name: "Rey"}
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
- raise ArgumentError, "#{base} must descent from Hashie::Mash" unless base <= Hashie::Mash
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 [String, Symbol] key the key to convert to a symbol
31
- # @return [void]
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 # => NoMethodError
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? # => NoMethodError
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 overriden
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
- def initialize(hash = {})
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
- require 'yaml'
1
+ # frozen_string_literal: true
2
+
2
3
  require 'erb'
3
- require 'pathname'
4
+ require 'hashie/extensions/parsers/yaml_parser'
4
5
 
5
6
  module Hashie
6
7
  module Extensions
7
8
  module Parsers
8
- class YamlErbParser
9
- def initialize(file_path, options = {})
10
- @content = File.read(file_path)
11
- @file_path = file_path.is_a?(Pathname) ? file_path.to_s : file_path
12
- @options = options
13
- end
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
- permitted_classes = @options.fetch(:permitted_classes) { [] }
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