hashie 2.1.2 → 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 (71) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +524 -59
  3. data/CONTRIBUTING.md +24 -7
  4. data/README.md +781 -90
  5. data/Rakefile +19 -2
  6. data/UPGRADING.md +245 -0
  7. data/hashie.gemspec +21 -13
  8. data/lib/hashie.rb +60 -21
  9. data/lib/hashie/array.rb +21 -0
  10. data/lib/hashie/clash.rb +24 -12
  11. data/lib/hashie/dash.rb +96 -33
  12. data/lib/hashie/extensions/active_support/core_ext/hash.rb +14 -0
  13. data/lib/hashie/extensions/array/pretty_inspect.rb +19 -0
  14. data/lib/hashie/extensions/coercion.rb +124 -18
  15. data/lib/hashie/extensions/dash/coercion.rb +25 -0
  16. data/lib/hashie/extensions/dash/indifferent_access.rb +56 -0
  17. data/lib/hashie/extensions/dash/property_translation.rb +191 -0
  18. data/lib/hashie/extensions/deep_fetch.rb +7 -5
  19. data/lib/hashie/extensions/deep_find.rb +69 -0
  20. data/lib/hashie/extensions/deep_locate.rb +113 -0
  21. data/lib/hashie/extensions/deep_merge.rb +35 -12
  22. data/lib/hashie/extensions/ignore_undeclared.rb +11 -5
  23. data/lib/hashie/extensions/indifferent_access.rb +28 -16
  24. data/lib/hashie/extensions/key_conflict_warning.rb +55 -0
  25. data/lib/hashie/extensions/key_conversion.rb +0 -82
  26. data/lib/hashie/extensions/mash/define_accessors.rb +90 -0
  27. data/lib/hashie/extensions/mash/keep_original_keys.rb +53 -0
  28. data/lib/hashie/extensions/mash/permissive_respond_to.rb +61 -0
  29. data/lib/hashie/extensions/mash/safe_assignment.rb +18 -0
  30. data/lib/hashie/extensions/mash/symbolize_keys.rb +38 -0
  31. data/lib/hashie/extensions/method_access.rb +154 -11
  32. data/lib/hashie/extensions/parsers/yaml_erb_parser.rb +48 -0
  33. data/lib/hashie/extensions/pretty_inspect.rb +19 -0
  34. data/lib/hashie/extensions/ruby_version.rb +60 -0
  35. data/lib/hashie/extensions/ruby_version_check.rb +21 -0
  36. data/lib/hashie/extensions/strict_key_access.rb +77 -0
  37. data/lib/hashie/extensions/stringify_keys.rb +71 -0
  38. data/lib/hashie/extensions/symbolize_keys.rb +71 -0
  39. data/lib/hashie/hash.rb +27 -8
  40. data/lib/hashie/logger.rb +18 -0
  41. data/lib/hashie/mash.rb +235 -57
  42. data/lib/hashie/railtie.rb +21 -0
  43. data/lib/hashie/rash.rb +40 -16
  44. data/lib/hashie/trash.rb +2 -88
  45. data/lib/hashie/utils.rb +44 -0
  46. data/lib/hashie/version.rb +1 -1
  47. metadata +42 -81
  48. data/.gitignore +0 -9
  49. data/.rspec +0 -2
  50. data/.rubocop.yml +0 -36
  51. data/.travis.yml +0 -15
  52. data/Gemfile +0 -11
  53. data/Guardfile +0 -5
  54. data/lib/hashie/hash_extensions.rb +0 -47
  55. data/spec/hashie/clash_spec.rb +0 -48
  56. data/spec/hashie/dash_spec.rb +0 -338
  57. data/spec/hashie/extensions/coercion_spec.rb +0 -156
  58. data/spec/hashie/extensions/deep_fetch_spec.rb +0 -70
  59. data/spec/hashie/extensions/deep_merge_spec.rb +0 -22
  60. data/spec/hashie/extensions/ignore_undeclared_spec.rb +0 -23
  61. data/spec/hashie/extensions/indifferent_access_spec.rb +0 -152
  62. data/spec/hashie/extensions/key_conversion_spec.rb +0 -103
  63. data/spec/hashie/extensions/merge_initializer_spec.rb +0 -23
  64. data/spec/hashie/extensions/method_access_spec.rb +0 -121
  65. data/spec/hashie/hash_spec.rb +0 -66
  66. data/spec/hashie/mash_spec.rb +0 -467
  67. data/spec/hashie/rash_spec.rb +0 -44
  68. data/spec/hashie/trash_spec.rb +0 -193
  69. data/spec/hashie/version_spec.rb +0 -7
  70. data/spec/spec.opts +0 -3
  71. data/spec/spec_helper.rb +0 -8
@@ -0,0 +1,61 @@
1
+ module Hashie
2
+ module Extensions
3
+ module Mash
4
+ # Allow a Mash to properly respond to everything
5
+ #
6
+ # By default, Mashes only say they respond to methods for keys that exist
7
+ # in their key set or any of the affix methods (e.g. setter, underbang,
8
+ # etc.). This causes issues when you try to use them within a
9
+ # SimpleDelegator or bind to a method for a key that is unset.
10
+ #
11
+ # This extension allows a Mash to properly respond to `respond_to?` and
12
+ # `method` for keys that have not yet been set. This enables full
13
+ # compatibility with SimpleDelegator and thunk-oriented programming.
14
+ #
15
+ # There is a trade-off with this extension: it will run slower than a
16
+ # regular Mash; insertions and initializations with keys run approximately
17
+ # 20% slower and cost approximately 19KB of memory per class that you
18
+ # make permissive.
19
+ #
20
+ # @api public
21
+ # @example Make a new, permissively responding Mash subclass
22
+ # class PermissiveMash < Hashie::Mash
23
+ # include Hashie::Extensions::Mash::PermissiveRespondTo
24
+ # end
25
+ #
26
+ # mash = PermissiveMash.new(a: 1)
27
+ # mash.respond_to? :b #=> true
28
+ module PermissiveRespondTo
29
+ # The Ruby hook for behavior when including the module
30
+ #
31
+ # @api private
32
+ # @private
33
+ # @return void
34
+ def self.included(base)
35
+ base.instance_variable_set :@_method_cache, base.instance_methods
36
+ base.define_singleton_method(:method_cache) { @_method_cache }
37
+ end
38
+
39
+ # The Ruby hook for determining what messages a class might respond to
40
+ #
41
+ # @api private
42
+ # @private
43
+ def respond_to_missing?(_method_name, _include_private = false)
44
+ true
45
+ end
46
+
47
+ private
48
+
49
+ # Override the Mash logging behavior to account for permissiveness
50
+ #
51
+ # @api private
52
+ # @private
53
+ def log_collision?(method_key)
54
+ self.class.method_cache.include?(method_key) &&
55
+ !self.class.disable_warnings?(method_key) &&
56
+ !(regular_key?(method_key) || regular_key?(method_key.to_s))
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,18 @@
1
+ module Hashie
2
+ module Extensions
3
+ module Mash
4
+ 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
9
+ super
10
+ end
11
+
12
+ def []=(*args)
13
+ custom_writer(*args)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,38 @@
1
+ module Hashie
2
+ module Extensions
3
+ module Mash
4
+ # Overrides Mash's default behavior of converting keys to strings
5
+ #
6
+ # @example
7
+ # class LazyResponse < Hashie::Mash
8
+ # include Hashie::Extensions::Mash::SymbolizedKeys
9
+ # end
10
+ #
11
+ # response = LazyResponse.new("id" => 123, "name" => "Rey").to_h
12
+ # #=> {id: 123, name: "Rey"}
13
+ #
14
+ # @api public
15
+ module SymbolizeKeys
16
+ # Hook for being included in a class
17
+ #
18
+ # @api private
19
+ # @return [void]
20
+ # @raise [ArgumentError] when the base class isn't a Mash
21
+ def self.included(base)
22
+ raise ArgumentError, "#{base} must descent from Hashie::Mash" unless base <= Hashie::Mash
23
+ end
24
+
25
+ private
26
+
27
+ # Converts a key to a symbol
28
+ #
29
+ # @api private
30
+ # @param [String, Symbol] key the key to convert to a symbol
31
+ # @return [void]
32
+ def convert_key(key)
33
+ key.to_sym
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -27,15 +27,25 @@ module Hashie
27
27
  #
28
28
  # user.not_declared # => NoMethodError
29
29
  module MethodReader
30
- def respond_to?(name, include_private = false)
30
+ def respond_to_missing?(name, include_private = false)
31
31
  return true if key?(name.to_s) || key?(name.to_sym)
32
32
  super
33
33
  end
34
34
 
35
35
  def method_missing(name, *args)
36
- return self[name.to_s] if key?(name.to_s)
37
- return self[name.to_sym] if key?(name.to_sym)
38
- super
36
+ if key?(name)
37
+ self[name]
38
+ else
39
+ sname = name.to_s
40
+ if key?(sname)
41
+ self[sname]
42
+ elsif sname[-1] == '?'
43
+ kname = sname[0..-2]
44
+ key?(kname) || key?(kname.to_sym)
45
+ else
46
+ super
47
+ end
48
+ end
39
49
  end
40
50
  end
41
51
 
@@ -57,7 +67,7 @@ module Hashie
57
67
  # h['awesome'] # => 'sauce'
58
68
  #
59
69
  module MethodWriter
60
- def respond_to?(name, include_private = false)
70
+ def respond_to_missing?(name, include_private = false)
61
71
  return true if name.to_s =~ /=$/
62
72
  super
63
73
  end
@@ -96,17 +106,42 @@ module Hashie
96
106
  # h.def? # => false
97
107
  # h.hji? # => NoMethodError
98
108
  module MethodQuery
99
- def respond_to?(name, include_private = false)
100
- return true if name.to_s =~ /(.*)\?$/ && (key?(Regexp.last_match[1]) || key?(Regexp.last_match[1].to_sym))
101
- super
109
+ def respond_to_missing?(name, include_private = false)
110
+ if query_method?(name) && indifferent_key?(key_from_query_method(name))
111
+ true
112
+ else
113
+ super
114
+ end
102
115
  end
103
116
 
104
117
  def method_missing(name, *args)
105
- if args.empty? && name.to_s =~ /(.*)\?$/ && (key?(Regexp.last_match[1]) || key?(Regexp.last_match[1].to_sym))
106
- return self[Regexp.last_match[1]] || self[Regexp.last_match[1].to_sym]
118
+ return super unless args.empty?
119
+
120
+ if query_method?(name)
121
+ key = key_from_query_method(name)
122
+ if indifferent_key?(key)
123
+ !!(self[key] || self[key.to_sym])
124
+ else
125
+ super
126
+ end
127
+ else
128
+ super
107
129
  end
130
+ end
108
131
 
109
- super
132
+ private
133
+
134
+ def indifferent_key?(name)
135
+ name = name.to_s
136
+ key?(name) || key?(name.to_sym)
137
+ end
138
+
139
+ def key_from_query_method(query_method)
140
+ query_method.to_s[0..-2]
141
+ end
142
+
143
+ def query_method?(name)
144
+ name.to_s.end_with?('?')
110
145
  end
111
146
  end
112
147
 
@@ -120,5 +155,113 @@ module Hashie
120
155
  end
121
156
  end
122
157
  end
158
+
159
+ # A module shared between MethodOverridingWriter and MethodOverridingInitializer
160
+ # to contained shared logic. This module aids in redefining existing hash methods.
161
+ module RedefineMethod
162
+ protected
163
+
164
+ def method?(name)
165
+ methods.map(&:to_s).include?(name)
166
+ end
167
+
168
+ def redefine_method(method_name)
169
+ eigenclass = class << self; self; end
170
+ eigenclass.__send__(:alias_method, "__#{method_name}", method_name)
171
+ eigenclass.__send__(:define_method, method_name, -> { self[method_name] })
172
+ end
173
+ end
174
+
175
+ # MethodOverridingWriter gives you #key_name= shortcuts for
176
+ # writing to your hash. It allows methods to be overridden by
177
+ # #key_name= shortcuts and aliases those methods with two
178
+ # leading underscores.
179
+ #
180
+ # Keys are written as strings. Override #convert_key if you
181
+ # would like to have symbols or something else.
182
+ #
183
+ # Note that MethodOverridingWriter also overrides
184
+ # #respond_to_missing? such that any #method_name= will respond
185
+ # appropriately as true.
186
+ #
187
+ # @example
188
+ # class MyHash < Hash
189
+ # include Hashie::Extensions::MethodOverridingWriter
190
+ # end
191
+ #
192
+ # h = MyHash.new
193
+ # h.awesome = 'sauce'
194
+ # h['awesome'] # => 'sauce'
195
+ # h.zip = 'a-dee-doo-dah'
196
+ # h.zip # => 'a-dee-doo-dah'
197
+ # h.__zip # => [[['awesome', 'sauce'], ['zip', 'a-dee-doo-dah']]]
198
+ #
199
+ module MethodOverridingWriter
200
+ include RedefineMethod
201
+
202
+ def convert_key(key)
203
+ key.to_s
204
+ end
205
+
206
+ def method_missing(name, *args)
207
+ if args.size == 1 && name.to_s =~ /(.*)=$/
208
+ key = Regexp.last_match[1]
209
+ redefine_method(key) if method?(key) && !already_overridden?(key)
210
+ return self[convert_key(key)] = args.first
211
+ end
212
+
213
+ super
214
+ end
215
+
216
+ def respond_to_missing?(name, include_private = false)
217
+ return true if name.to_s.end_with?('=')
218
+ super
219
+ end
220
+
221
+ protected
222
+
223
+ def already_overridden?(name)
224
+ method?("__#{name}")
225
+ end
226
+ end
227
+
228
+ # A macro module that will automatically include MethodReader,
229
+ # MethodOverridingWriter, and MethodQuery, giving you the ability
230
+ # to read, write, and query keys in a hash using method call
231
+ # shortcuts that can override object methods. Any overridden
232
+ # object method is automatically aliased with two leading
233
+ # underscores.
234
+ module MethodAccessWithOverride
235
+ def self.included(base)
236
+ [MethodReader, MethodOverridingWriter,
237
+ MethodQuery, MethodOverridingInitializer].each do |mod|
238
+ base.send :include, mod
239
+ end
240
+ end
241
+ end
242
+
243
+ # MethodOverridingInitializer allows you to override default hash
244
+ # methods when passing in values from an existing hash. The overriden
245
+ # methods are aliased with two leading underscores.
246
+ #
247
+ # @example
248
+ # class MyHash < Hash
249
+ # include Hashie::Extensions::MethodOverridingInitializer
250
+ # end
251
+ #
252
+ # h = MyHash.new(zip: 'a-dee-doo-dah')
253
+ # h.zip # => 'a-dee-doo-dah'
254
+ # h.__zip # => [[['zip', 'a-dee-doo-dah']]]
255
+ module MethodOverridingInitializer
256
+ include RedefineMethod
257
+
258
+ def initialize(hash = {})
259
+ hash.each do |key, value|
260
+ skey = key.to_s
261
+ redefine_method(skey) if method?(skey)
262
+ self[skey] = value
263
+ end
264
+ end
265
+ end
123
266
  end
124
267
  end
@@ -0,0 +1,48 @@
1
+ require 'yaml'
2
+ require 'erb'
3
+ require 'pathname'
4
+
5
+ module Hashie
6
+ module Extensions
7
+ 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
+
15
+ def perform
16
+ template = ERB.new(@content)
17
+ 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
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,19 @@
1
+ module Hashie
2
+ module Extensions
3
+ module PrettyInspect
4
+ def self.included(base)
5
+ base.send :alias_method, :hash_inspect, :inspect
6
+ base.send :alias_method, :inspect, :hashie_inspect
7
+ end
8
+
9
+ def hashie_inspect
10
+ ret = "#<#{self.class}"
11
+ keys.sort_by(&:to_s).each do |key|
12
+ ret << " #{key}=#{self[key].inspect}"
13
+ end
14
+ ret << '>'
15
+ ret
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,60 @@
1
+ # Copyright (c) Chad Fowler, Rich Kilmer, Jim Weirich and others.
2
+ # Portions copyright (c) Engine Yard and Andre Arko
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # 'Software'), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+
15
+ module Hashie
16
+ module Extensions
17
+ class RubyVersion
18
+ include Comparable
19
+
20
+ attr_accessor :segments
21
+
22
+ def initialize(version)
23
+ @segments = split_to_segments(version)
24
+ end
25
+
26
+ def <=>(other)
27
+ lhsegments = segments
28
+ rhsegments = other.segments
29
+
30
+ lhsize = lhsegments.size
31
+ rhsize = rhsegments.size
32
+ limit = (lhsize > rhsize ? lhsize : rhsize) - 1
33
+
34
+ i = 0
35
+
36
+ while i <= limit
37
+ lhs = lhsegments[i] || 0
38
+ rhs = rhsegments[i] || 0
39
+ i += 1
40
+
41
+ next if lhs == rhs
42
+ return -1 if lhs.is_a?(String) && rhs.is_a?(Numeric)
43
+ return 1 if lhs.is_a?(Numeric) && rhs.is_a?(String)
44
+
45
+ return lhs <=> rhs
46
+ end
47
+
48
+ 0
49
+ end
50
+
51
+ private
52
+
53
+ def split_to_segments(version)
54
+ version.scan(/[0-9]+|[a-z]+/i).map do |segment|
55
+ /^\d+$/ =~ segment ? segment.to_i : segment
56
+ end.freeze
57
+ end
58
+ end
59
+ end
60
+ end