hashie 3.6.0 → 4.1.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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +228 -174
  3. data/CONTRIBUTING.md +13 -6
  4. data/README.md +127 -15
  5. data/UPGRADING.md +83 -7
  6. data/hashie.gemspec +13 -7
  7. data/lib/hashie.rb +21 -19
  8. data/lib/hashie/dash.rb +2 -1
  9. data/lib/hashie/extensions/active_support/core_ext/hash.rb +14 -0
  10. data/lib/hashie/extensions/coercion.rb +23 -16
  11. data/lib/hashie/extensions/dash/indifferent_access.rb +20 -1
  12. data/lib/hashie/extensions/dash/property_translation.rb +6 -3
  13. data/lib/hashie/extensions/deep_fetch.rb +4 -2
  14. data/lib/hashie/extensions/deep_find.rb +12 -3
  15. data/lib/hashie/extensions/deep_locate.rb +22 -7
  16. data/lib/hashie/extensions/deep_merge.rb +18 -1
  17. data/lib/hashie/extensions/indifferent_access.rb +1 -3
  18. data/lib/hashie/extensions/key_conflict_warning.rb +55 -0
  19. data/lib/hashie/extensions/mash/define_accessors.rb +90 -0
  20. data/lib/hashie/extensions/mash/keep_original_keys.rb +2 -1
  21. data/lib/hashie/extensions/mash/permissive_respond_to.rb +61 -0
  22. data/lib/hashie/extensions/mash/safe_assignment.rb +3 -1
  23. data/lib/hashie/extensions/method_access.rb +5 -2
  24. data/lib/hashie/extensions/parsers/yaml_erb_parser.rb +26 -4
  25. data/lib/hashie/extensions/ruby_version_check.rb +5 -1
  26. data/lib/hashie/extensions/strict_key_access.rb +8 -4
  27. data/lib/hashie/hash.rb +16 -9
  28. data/lib/hashie/mash.rb +114 -53
  29. data/lib/hashie/railtie.rb +7 -0
  30. data/lib/hashie/rash.rb +1 -1
  31. data/lib/hashie/utils.rb +28 -0
  32. data/lib/hashie/version.rb +1 -1
  33. metadata +19 -130
  34. data/spec/hashie/array_spec.rb +0 -29
  35. data/spec/hashie/clash_spec.rb +0 -70
  36. data/spec/hashie/dash_spec.rb +0 -598
  37. data/spec/hashie/extensions/autoload_spec.rb +0 -24
  38. data/spec/hashie/extensions/coercion_spec.rb +0 -639
  39. data/spec/hashie/extensions/dash/coercion_spec.rb +0 -13
  40. data/spec/hashie/extensions/dash/indifferent_access_spec.rb +0 -84
  41. data/spec/hashie/extensions/deep_fetch_spec.rb +0 -97
  42. data/spec/hashie/extensions/deep_find_spec.rb +0 -138
  43. data/spec/hashie/extensions/deep_locate_spec.rb +0 -137
  44. data/spec/hashie/extensions/deep_merge_spec.rb +0 -70
  45. data/spec/hashie/extensions/ignore_undeclared_spec.rb +0 -47
  46. data/spec/hashie/extensions/indifferent_access_spec.rb +0 -295
  47. data/spec/hashie/extensions/indifferent_access_with_rails_hwia_spec.rb +0 -208
  48. data/spec/hashie/extensions/key_conversion_spec.rb +0 -12
  49. data/spec/hashie/extensions/mash/keep_original_keys_spec.rb +0 -46
  50. data/spec/hashie/extensions/mash/safe_assignment_spec.rb +0 -50
  51. data/spec/hashie/extensions/mash/symbolize_keys_spec.rb +0 -39
  52. data/spec/hashie/extensions/merge_initializer_spec.rb +0 -23
  53. data/spec/hashie/extensions/method_access_spec.rb +0 -226
  54. data/spec/hashie/extensions/strict_key_access_spec.rb +0 -110
  55. data/spec/hashie/extensions/stringify_keys_spec.rb +0 -124
  56. data/spec/hashie/extensions/symbolize_keys_spec.rb +0 -129
  57. data/spec/hashie/hash_spec.rb +0 -84
  58. data/spec/hashie/mash_spec.rb +0 -771
  59. data/spec/hashie/parsers/yaml_erb_parser_spec.rb +0 -46
  60. data/spec/hashie/rash_spec.rb +0 -83
  61. data/spec/hashie/trash_spec.rb +0 -328
  62. data/spec/hashie/utils_spec.rb +0 -25
  63. data/spec/hashie/version_spec.rb +0 -7
  64. data/spec/hashie_spec.rb +0 -13
  65. data/spec/integration/elasticsearch/integration_spec.rb +0 -40
  66. data/spec/integration/omniauth-oauth2/app.rb +0 -52
  67. data/spec/integration/omniauth-oauth2/integration_spec.rb +0 -26
  68. data/spec/integration/omniauth-oauth2/some_site.rb +0 -38
  69. data/spec/integration/omniauth/app.rb +0 -11
  70. data/spec/integration/omniauth/integration_spec.rb +0 -38
  71. data/spec/integration/rails-without-dependency/integration_spec.rb +0 -15
  72. data/spec/integration/rails/app.rb +0 -47
  73. data/spec/integration/rails/integration_spec.rb +0 -26
  74. data/spec/spec_helper.rb +0 -23
  75. data/spec/support/integration_specs.rb +0 -36
  76. data/spec/support/logger.rb +0 -24
  77. data/spec/support/module_context.rb +0 -11
  78. data/spec/support/ruby_version_check.rb +0 -6
data/lib/hashie/dash.rb CHANGED
@@ -219,7 +219,8 @@ module Hashie
219
219
  end
220
220
 
221
221
  def fail_property_required_error!(property)
222
- raise ArgumentError, "The property '#{property}' #{self.class.required_properties[property][:message]}"
222
+ raise ArgumentError,
223
+ "The property '#{property}' #{self.class.required_properties[property][:message]}"
223
224
  end
224
225
 
225
226
  def fail_no_property_error!(property)
@@ -0,0 +1,14 @@
1
+ module Hashie
2
+ module Extensions
3
+ module ActiveSupport
4
+ module CoreExt
5
+ module Hash
6
+ def except(*keys)
7
+ string_keys = keys.map { |key| convert_key(key) }
8
+ super(*string_keys)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -1,5 +1,9 @@
1
1
  module Hashie
2
- class CoercionError < StandardError; end
2
+ class CoercionError < StandardError
3
+ def initialize(key, value, into, message)
4
+ super("Cannot coerce property #{key.inspect} from #{value.class} to #{into}: #{message}")
5
+ end
6
+ end
3
7
 
4
8
  module Extensions
5
9
  module Coercion
@@ -12,20 +16,22 @@ module Hashie
12
16
  Symbol => :to_sym
13
17
  }.freeze
14
18
 
15
- ABSTRACT_CORE_TYPES = if RubyVersion.new(RUBY_VERSION) >= RubyVersion.new('2.4.0')
16
- { Numeric => [Integer, Float, Complex, Rational] }
17
- else
18
- {
19
- Integer => [Fixnum, Bignum], # rubocop:disable Lint/UnifiedInteger
20
- Numeric => [Fixnum, Bignum, Float, Complex, Rational] # rubocop:disable Lint/UnifiedInteger
21
- }
22
- end
19
+ ABSTRACT_CORE_TYPES =
20
+ if RubyVersion.new(RUBY_VERSION) >= RubyVersion.new('2.4.0')
21
+ { Numeric => [Integer, Float, Complex, Rational] }
22
+ else
23
+ {
24
+ Integer => [Fixnum, Bignum],
25
+ Numeric => [Fixnum, Bignum, Float, Complex, Rational]
26
+ }
27
+ end
23
28
 
24
29
  def self.included(base)
25
30
  base.send :include, InstanceMethods
26
- base.extend ClassMethods # NOTE: we wanna make sure we first define set_value_with_coercion before extending
27
-
28
- base.send :alias_method, :set_value_without_coercion, :[]= unless base.method_defined?(:set_value_without_coercion)
31
+ base.extend ClassMethods
32
+ unless base.method_defined?(:set_value_without_coercion)
33
+ base.send :alias_method, :set_value_without_coercion, :[]=
34
+ end
29
35
  base.send :alias_method, :[]=, :set_value_with_coercion
30
36
  end
31
37
 
@@ -37,7 +43,7 @@ module Hashie
37
43
  begin
38
44
  value = self.class.fetch_coercion(into).call(value)
39
45
  rescue NoMethodError, TypeError => e
40
- raise CoercionError, "Cannot coerce property #{key.inspect} from #{value.class} to #{into}: #{e.message}"
46
+ raise CoercionError.new(key, value, into, e.message)
41
47
  end
42
48
  end
43
49
 
@@ -97,7 +103,8 @@ module Hashie
97
103
  #
98
104
  # @param [Class] from the type you would like coerced.
99
105
  # @param [Class] into the class into which you would like the value coerced.
100
- # @option options [Boolean] :strict (true) whether use exact source class only or include ancestors
106
+ # @option options [Boolean] :strict (true) whether use exact source class
107
+ # only or include ancestors
101
108
  #
102
109
  # @example Coerce all hashes into this special type of hash
103
110
  # class SpecialHash < Hash
@@ -159,10 +166,10 @@ module Hashie
159
166
 
160
167
  def build_coercion(type)
161
168
  if type.is_a? Enumerable
162
- if type.class <= ::Hash
169
+ if type.class == ::Hash
163
170
  type, key_type, value_type = type.class, *type.first
164
171
  build_hash_coercion(type, key_type, value_type)
165
- else # Enumerable but not Hash: Array, Set
172
+ else
166
173
  value_type = type.first
167
174
  type = type.class
168
175
  build_container_coercion(type, value_type)
@@ -7,11 +7,23 @@ module Hashie
7
7
  base.send :include, Hashie::Extensions::IndifferentAccess
8
8
  end
9
9
 
10
+ def self.maybe_extend(base)
11
+ return unless requires_class_methods?(base)
12
+
13
+ base.extend(ClassMethods)
14
+ end
15
+
16
+ def self.requires_class_methods?(klass)
17
+ klass <= Hashie::Dash &&
18
+ !klass.singleton_class.included_modules.include?(ClassMethods)
19
+ end
20
+ private_class_method :requires_class_methods?
21
+
10
22
  module ClassMethods
11
23
  # Check to see if the specified property has already been
12
24
  # defined.
13
25
  def property?(name)
14
- name = translations[name.to_sym] if included_modules.include?(Hashie::Extensions::Dash::PropertyTranslation) && translation_exists?(name)
26
+ name = translations[name.to_sym] if translation_for?(name)
15
27
  name = name.to_s
16
28
  !!properties.find { |property| property.to_s == name }
17
29
  end
@@ -30,6 +42,13 @@ module Hashie
30
42
  name = name.to_s
31
43
  !!transforms.keys.find { |key| key.to_s == name }
32
44
  end
45
+
46
+ private
47
+
48
+ def translation_for?(name)
49
+ included_modules.include?(Hashie::Extensions::Dash::PropertyTranslation) &&
50
+ translation_exists?(name)
51
+ end
33
52
  end
34
53
  end
35
54
  end
@@ -35,7 +35,7 @@ module Hashie
35
35
  # end
36
36
  #
37
37
  # model = DataModelHash.new(id: '123', created: '2014-04-25 22:35:28')
38
- # model.id.class #=> Fixnum
38
+ # model.id.class #=> Integer (Fixnum if you are using Ruby 2.3 or lower)
39
39
  # model.created_at.class #=> Time
40
40
  module PropertyTranslation
41
41
  def self.included(base)
@@ -58,7 +58,9 @@ module Hashie
58
58
  end
59
59
 
60
60
  def permitted_input_keys
61
- @permitted_input_keys ||= properties.map { |property| inverse_translations.fetch property, property }
61
+ @permitted_input_keys ||=
62
+ properties
63
+ .map { |property| inverse_translations.fetch property, property }
62
64
  end
63
65
 
64
66
  # Defines a property on the Trash. Options are as follows:
@@ -135,7 +137,8 @@ module Hashie
135
137
  end
136
138
 
137
139
  def fail_self_transformation_error!(property_name)
138
- raise ArgumentError, "Property name (#{property_name}) and :from option must not be the same"
140
+ raise ArgumentError,
141
+ "Property name (#{property_name}) and :from option must not be the same"
139
142
  end
140
143
 
141
144
  def valid_transformer?(transformer)
@@ -9,7 +9,8 @@ module Hashie
9
9
  #
10
10
  # options.deep_fetch(:user, :non_existent_key) { 'a value' } #=> 'a value'
11
11
  #
12
- # This is particularly useful for fetching values from deeply nested api responses or params hashes.
12
+ # This is particularly useful for fetching values from deeply nested api responses
13
+ # or params hashes.
13
14
  module DeepFetch
14
15
  class UndefinedPathError < StandardError; end
15
16
 
@@ -20,7 +21,8 @@ module Hashie
20
21
  obj.fetch(arg)
21
22
  rescue ArgumentError, IndexError, NoMethodError => e
22
23
  break yield(arg) if block
23
- raise UndefinedPathError, "Could not fetch path (#{args.join(' > ')}) at #{arg}", e.backtrace
24
+ raise UndefinedPathError,
25
+ "Could not fetch path (#{args.join(' > ')}) at #{arg}", e.backtrace
24
26
  end
25
27
  end
26
28
  end
@@ -1,3 +1,4 @@
1
+ require 'hashie/extensions/deep_locate'
1
2
  module Hashie
2
3
  module Extensions
3
4
  module DeepFind
@@ -24,7 +25,12 @@ module Hashie
24
25
  # Performs a depth-first search on deeply nested data structures for
25
26
  # a key and returns all occurrences of the key.
26
27
  #
27
- # options = {users: [{location: {address: '123 Street'}}, {location: {address: '234 Street'}}]}
28
+ # options = {
29
+ # users: [
30
+ # { location: {address: '123 Street'} },
31
+ # { location: {address: '234 Street'}}
32
+ # ]
33
+ # }
28
34
  # options.extend(Hashie::Extensions::DeepFind)
29
35
  # options.deep_find_all(:address) # => ['123 Street', '234 Street']
30
36
  #
@@ -33,7 +39,10 @@ module Hashie
33
39
  # end
34
40
  #
35
41
  # my_hash = MyHash.new
36
- # my_hash[:users] = [{location: {address: '123 Street'}}, {location: {address: '234 Street'}}]
42
+ # my_hash[:users] = [
43
+ # {location: {address: '123 Street'}},
44
+ # {location: {address: '234 Street'}}
45
+ # ]
37
46
  # my_hash.deep_find_all(:address) # => ['123 Street', '234 Street']
38
47
  def deep_find_all(key)
39
48
  matches = _deep_find_all(key)
@@ -49,7 +58,7 @@ module Hashie
49
58
  end
50
59
 
51
60
  def _deep_find_all(key, object = self, matches = [])
52
- deep_locate_result = Hashie::Extensions::DeepLocate.deep_locate(key, object).tap do |result|
61
+ deep_locate_result = DeepLocate.deep_locate(key, object).tap do |result|
53
62
  result.map! { |element| element[key] }
54
63
  end
55
64
 
@@ -14,10 +14,12 @@ module Hashie
14
14
  # ...
15
15
  # ]
16
16
  #
17
- # Hashie::Extensions::DeepLocate.deep_locate -> (key, value, object) { key == :title }, books
17
+ # DeepLocate.deep_locate -> (key, value, object) { key == :title }, books
18
18
  # # => [{:title=>"Ruby for beginners", :pages=>120}, ...]
19
19
  def self.deep_locate(comparator, object)
20
- comparator = _construct_key_comparator(comparator, object) unless comparator.respond_to?(:call)
20
+ unless comparator.respond_to?(:call)
21
+ comparator = _construct_key_comparator(comparator, object)
22
+ end
21
23
 
22
24
  _deep_locate(comparator, object)
23
25
  end
@@ -53,17 +55,21 @@ module Hashie
53
55
  # # http://ruby-journal.com/becareful-with-space-in-lambda-hash-rocket-syntax-between-ruby-1-dot-9-and-2-dot-0/
54
56
  #
55
57
  # books.deep_locate -> (key, value, object) { key == :title && value.include?("Ruby") }
56
- # # => [{:title=>"Ruby for beginners", :pages=>120}, {:title=>"Ruby for the rest of us", :pages=>576}]
58
+ # # => [{:title=>"Ruby for beginners", :pages=>120},
59
+ # # {:title=>"Ruby for the rest of us", :pages=>576}]
57
60
  #
58
61
  # books.deep_locate -> (key, value, object) { key == :pages && value <= 120 }
59
- # # => [{:title=>"Ruby for beginners", :pages=>120}, {:title=>"CSS for intermediates", :pages=>80}]
62
+ # # => [{:title=>"Ruby for beginners", :pages=>120},
63
+ # # {:title=>"CSS for intermediates", :pages=>80}]
60
64
  def deep_locate(comparator)
61
65
  Hashie::Extensions::DeepLocate.deep_locate(comparator, self)
62
66
  end
63
67
 
64
68
  def self._construct_key_comparator(search_key, object)
65
- search_key = search_key.to_s if defined?(::ActiveSupport::HashWithIndifferentAccess) && object.is_a?(::ActiveSupport::HashWithIndifferentAccess)
66
- search_key = search_key.to_s if object.respond_to?(:indifferent_access?) && object.indifferent_access?
69
+ if object.respond_to?(:indifferent_access?) && object.indifferent_access? ||
70
+ activesupport_indifferent?(object)
71
+ search_key = search_key.to_s
72
+ end
67
73
 
68
74
  lambda do |non_callable_object|
69
75
  ->(key, _, _) { key == non_callable_object }
@@ -73,7 +79,10 @@ module Hashie
73
79
 
74
80
  def self._deep_locate(comparator, object, result = [])
75
81
  if object.is_a?(::Enumerable)
76
- result.push object if object.any? { |value| _match_comparator?(value, comparator, object) }
82
+ if object.any? { |value| _match_comparator?(value, comparator, object) }
83
+ result.push object
84
+ end
85
+
77
86
  (object.respond_to?(:values) ? object.values : object.entries).each do |value|
78
87
  _deep_locate(comparator, value, result)
79
88
  end
@@ -93,6 +102,12 @@ module Hashie
93
102
  comparator.call(key, value, object)
94
103
  end
95
104
  private_class_method :_match_comparator?
105
+
106
+ def self.activesupport_indifferent?(object)
107
+ defined?(::ActiveSupport::HashWithIndifferentAccess) &&
108
+ object.is_a?(::ActiveSupport::HashWithIndifferentAccess)
109
+ end
110
+ private_class_method :activesupport_indifferent?
96
111
  end
97
112
  end
98
113
  end
@@ -3,7 +3,7 @@ module Hashie
3
3
  module DeepMerge
4
4
  # Returns a new hash with +self+ and +other_hash+ merged recursively.
5
5
  def deep_merge(other_hash, &block)
6
- copy = dup
6
+ copy = _deep_dup(self)
7
7
  copy.extend(Hashie::Extensions::DeepMerge) unless copy.respond_to?(:deep_merge!)
8
8
  copy.deep_merge!(other_hash, &block)
9
9
  end
@@ -18,11 +18,28 @@ module Hashie
18
18
 
19
19
  private
20
20
 
21
+ def _deep_dup(hash)
22
+ copy = hash.dup
23
+
24
+ copy.each do |key, value|
25
+ copy[key] =
26
+ if value.is_a?(::Hash)
27
+ _deep_dup(value)
28
+ else
29
+ Hashie::Utils.safe_dup(value)
30
+ end
31
+ end
32
+
33
+ copy
34
+ end
35
+
21
36
  def _recursive_merge(hash, other_hash, &block)
22
37
  other_hash.each do |k, v|
23
38
  hash[k] =
24
39
  if hash.key?(k) && hash[k].is_a?(::Hash) && v.is_a?(::Hash)
25
40
  _recursive_merge(hash[k], v, &block)
41
+ elsif v.is_a?(::Hash)
42
+ _recursive_merge({}, v, &block)
26
43
  elsif hash.key?(k) && block_given?
27
44
  yield(k, hash[k], v)
28
45
  else
@@ -24,9 +24,7 @@ module Hashie
24
24
  #
25
25
  module IndifferentAccess
26
26
  def self.included(base)
27
- Hashie::Extensions::Dash::IndifferentAccess::ClassMethods.tap do |extension|
28
- base.extend(extension) if base <= Hashie::Dash && !base.singleton_class.included_modules.include?(extension)
29
- end
27
+ Hashie::Extensions::Dash::IndifferentAccess.maybe_extend(base)
30
28
 
31
29
  base.class_eval do
32
30
  alias_method :regular_writer, :[]= unless method_defined?(:regular_writer)
@@ -0,0 +1,55 @@
1
+ module Hashie
2
+ module Extensions
3
+ module KeyConflictWarning
4
+ class CannotDisableMashWarnings < StandardError
5
+ def initialize
6
+ super(
7
+ 'You cannot disable warnings on the base Mash class. ' \
8
+ 'Please subclass the Mash and disable it in the subclass.'
9
+ )
10
+ end
11
+ end
12
+
13
+ # Disable the logging of warnings based on keys conflicting keys/methods
14
+ #
15
+ # @api semipublic
16
+ # @return [void]
17
+ def disable_warnings(*method_keys)
18
+ raise CannotDisableMashWarnings if self == Hashie::Mash
19
+ if method_keys.any?
20
+ disabled_warnings.concat(method_keys).tap(&:flatten!).uniq!
21
+ else
22
+ disabled_warnings.clear
23
+ end
24
+
25
+ @disable_warnings = true
26
+ end
27
+
28
+ # Checks whether this class disables warnings for conflicting keys/methods
29
+ #
30
+ # @api semipublic
31
+ # @return [Boolean]
32
+ def disable_warnings?(method_key = nil)
33
+ return disabled_warnings.include?(method_key) if disabled_warnings.any? && method_key
34
+ @disable_warnings ||= false
35
+ end
36
+
37
+ # Returns an array of methods that this class disables warnings for.
38
+ #
39
+ # @api semipublic
40
+ # @return [Boolean]
41
+ def disabled_warnings
42
+ @_disabled_warnings ||= []
43
+ end
44
+
45
+ # Inheritance hook that sets class configuration when inherited.
46
+ #
47
+ # @api semipublic
48
+ # @return [void]
49
+ def inherited(subclass)
50
+ super
51
+ subclass.disable_warnings(disabled_warnings) if disable_warnings?
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,90 @@
1
+ module Hashie
2
+ module Extensions
3
+ module Mash
4
+ module DefineAccessors
5
+ def self.included(klass)
6
+ klass.class_eval do
7
+ mod = Ext.new
8
+ include mod
9
+ end
10
+ end
11
+
12
+ def self.extended(obj)
13
+ included(obj.singleton_class)
14
+ end
15
+
16
+ class Ext < Module
17
+ def initialize
18
+ mod = self
19
+ define_method(:method_missing) do |method_name, *args, &block|
20
+ key, suffix = method_name_and_suffix(method_name)
21
+ case suffix
22
+ when '='.freeze
23
+ mod.define_writer(key, method_name)
24
+ when '?'.freeze
25
+ mod.define_predicate(key, method_name)
26
+ when '!'.freeze
27
+ mod.define_initializing_reader(key, method_name)
28
+ when '_'.freeze
29
+ mod.define_underbang_reader(key, method_name)
30
+ else
31
+ mod.define_reader(key, method_name)
32
+ end
33
+ send(method_name, *args, &block)
34
+ end
35
+ end
36
+
37
+ def define_reader(key, method_name)
38
+ define_method(method_name) do |&block|
39
+ if key? method_name
40
+ self.[](method_name, &block)
41
+ else
42
+ self.[](key, &block)
43
+ end
44
+ end
45
+ end
46
+
47
+ def define_writer(key, method_name)
48
+ define_method(method_name) do |value = nil|
49
+ if key? method_name
50
+ self.[](method_name, &proc)
51
+ else
52
+ assign_property(key, value)
53
+ end
54
+ end
55
+ end
56
+
57
+ def define_predicate(key, method_name)
58
+ define_method(method_name) do
59
+ if key? method_name
60
+ self.[](method_name, &proc)
61
+ else
62
+ !!self[key]
63
+ end
64
+ end
65
+ end
66
+
67
+ def define_initializing_reader(key, method_name)
68
+ define_method(method_name) do
69
+ if key? method_name
70
+ self.[](method_name, &proc)
71
+ else
72
+ initializing_reader(key)
73
+ end
74
+ end
75
+ end
76
+
77
+ def define_underbang_reader(key, method_name)
78
+ define_method(method_name) do
79
+ if key? method_name
80
+ self.[](key, &proc)
81
+ else
82
+ underbang_reader(key)
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end