hashie 2.0.5 → 2.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 (46) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +36 -0
  3. data/.travis.yml +13 -6
  4. data/CHANGELOG.md +40 -21
  5. data/CONTRIBUTING.md +110 -19
  6. data/Gemfile +9 -0
  7. data/LICENSE +1 -1
  8. data/README.md +347 -0
  9. data/Rakefile +4 -2
  10. data/hashie.gemspec +4 -7
  11. data/lib/hashie.rb +3 -0
  12. data/lib/hashie/clash.rb +19 -19
  13. data/lib/hashie/dash.rb +47 -39
  14. data/lib/hashie/extensions/coercion.rb +10 -6
  15. data/lib/hashie/extensions/deep_fetch.rb +29 -0
  16. data/lib/hashie/extensions/deep_merge.rb +15 -6
  17. data/lib/hashie/extensions/ignore_undeclared.rb +41 -0
  18. data/lib/hashie/extensions/indifferent_access.rb +37 -10
  19. data/lib/hashie/extensions/key_conversion.rb +3 -3
  20. data/lib/hashie/extensions/method_access.rb +9 -9
  21. data/lib/hashie/hash.rb +7 -7
  22. data/lib/hashie/hash_extensions.rb +5 -7
  23. data/lib/hashie/mash.rb +38 -31
  24. data/lib/hashie/rash.rb +119 -0
  25. data/lib/hashie/trash.rb +31 -22
  26. data/lib/hashie/version.rb +1 -1
  27. data/spec/hashie/clash_spec.rb +43 -45
  28. data/spec/hashie/dash_spec.rb +115 -53
  29. data/spec/hashie/extensions/coercion_spec.rb +42 -37
  30. data/spec/hashie/extensions/deep_fetch_spec.rb +70 -0
  31. data/spec/hashie/extensions/deep_merge_spec.rb +11 -9
  32. data/spec/hashie/extensions/ignore_undeclared_spec.rb +23 -0
  33. data/spec/hashie/extensions/indifferent_access_spec.rb +117 -64
  34. data/spec/hashie/extensions/key_conversion_spec.rb +28 -27
  35. data/spec/hashie/extensions/merge_initializer_spec.rb +13 -10
  36. data/spec/hashie/extensions/method_access_spec.rb +49 -40
  37. data/spec/hashie/hash_spec.rb +25 -13
  38. data/spec/hashie/mash_spec.rb +243 -187
  39. data/spec/hashie/rash_spec.rb +44 -0
  40. data/spec/hashie/trash_spec.rb +81 -43
  41. data/spec/hashie/version_spec.rb +7 -0
  42. data/spec/spec_helper.rb +0 -4
  43. metadata +27 -78
  44. data/.document +0 -5
  45. data/README.markdown +0 -236
  46. data/lib/hashie/extensions/structure.rb +0 -47
@@ -10,7 +10,7 @@ module Hashie
10
10
  def stringify_keys!
11
11
  keys.each do |k|
12
12
  stringify_keys_recursively!(self[k])
13
- self[k.to_s] = self.delete(k)
13
+ self[k.to_s] = delete(k)
14
14
  end
15
15
  self
16
16
  end
@@ -51,7 +51,7 @@ module Hashie
51
51
  def symbolize_keys!
52
52
  keys.each do |k|
53
53
  symbolize_keys_recursively!(self[k])
54
- self[k.to_sym] = self.delete(k)
54
+ self[k.to_sym] = delete(k)
55
55
  end
56
56
  self
57
57
  end
@@ -81,7 +81,7 @@ module Hashie
81
81
  end
82
82
  end
83
83
  end
84
-
84
+
85
85
  module KeyConversion
86
86
  def self.included(base)
87
87
  base.send :include, SymbolizeKeys
@@ -5,8 +5,8 @@ module Hashie
5
5
  # to access your hash's keys. It will recognize keys
6
6
  # either as strings or symbols.
7
7
  #
8
- # Note that while nil keys will be returned as nil,
9
- # undefined keys will raise NoMethodErrors. Also note that
8
+ # Note that while nil keys will be returned as nil,
9
+ # undefined keys will raise NoMethodErrors. Also note that
10
10
  # #respond_to? has been patched to appropriately recognize
11
11
  # key methods.
12
12
  #
@@ -18,9 +18,9 @@ module Hashie
18
18
  # user = User.new
19
19
  # user['first_name'] = 'Michael'
20
20
  # user.first_name # => 'Michael'
21
- #
21
+ #
22
22
  # user[:last_name] = 'Bleigh'
23
- # user.last_name # => 'Bleigh'
23
+ # user.last_name # => 'Bleigh'
24
24
  #
25
25
  # user[:birthday] = nil
26
26
  # user.birthday # => nil
@@ -31,7 +31,7 @@ module Hashie
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
36
  return self[name.to_s] if key?(name.to_s)
37
37
  return self[name.to_sym] if key?(name.to_sym)
@@ -64,7 +64,7 @@ module Hashie
64
64
 
65
65
  def method_missing(name, *args)
66
66
  if args.size == 1 && name.to_s =~ /(.*)=$/
67
- return self[convert_key($1)] = args.first
67
+ return self[convert_key(Regexp.last_match[1])] = args.first
68
68
  end
69
69
 
70
70
  super
@@ -97,13 +97,13 @@ module Hashie
97
97
  # h.hji? # => NoMethodError
98
98
  module MethodQuery
99
99
  def respond_to?(name, include_private = false)
100
- return true if name.to_s =~ /(.*)\?$/ && (key?($1) || key?($1.to_sym))
100
+ return true if name.to_s =~ /(.*)\?$/ && (key?(Regexp.last_match[1]) || key?(Regexp.last_match[1].to_sym))
101
101
  super
102
102
  end
103
103
 
104
104
  def method_missing(name, *args)
105
- if args.empty? && name.to_s =~ /(.*)\?$/ && (key?($1) || key?($1.to_sym))
106
- return self[$1] || self[$1.to_sym]
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]
107
107
  end
108
108
 
109
109
  super
@@ -7,19 +7,19 @@ module Hashie
7
7
  class Hash < ::Hash
8
8
  include HashExtensions
9
9
 
10
- # Converts a mash back to a hash (with stringified keys)
11
- def to_hash(options={})
10
+ # Converts a mash back to a hash (with stringified or symbolized keys)
11
+ def to_hash(options = {})
12
12
  out = {}
13
13
  keys.each do |k|
14
+ assignment_key = k.to_s
15
+ assignment_key = assignment_key.to_sym if options[:symbolize_keys]
14
16
  if self[k].is_a?(Array)
15
- k = options[:symbolize_keys] ? k.to_sym : k.to_s
16
- out[k] ||= []
17
+ out[assignment_key] ||= []
17
18
  self[k].each do |array_object|
18
- out[k] << (Hash === array_object ? array_object.to_hash : array_object)
19
+ out[assignment_key] << (Hash === array_object ? array_object.to_hash(options) : array_object)
19
20
  end
20
21
  else
21
- k = options[:symbolize_keys] ? k.to_sym : k.to_s
22
- out[k] = Hash === self[k] ? self[k].to_hash : self[k]
22
+ out[assignment_key] = Hash === self[k] ? self[k].to_hash(options) : self[k]
23
23
  end
24
24
  end
25
25
  out
@@ -11,10 +11,8 @@ module Hashie
11
11
  # Destructively convert all of the keys of a Hash
12
12
  # to their string representations.
13
13
  def hashie_stringify_keys!
14
- self.keys.each do |k|
15
- unless String === k
16
- self[k.to_s] = self.delete(k)
17
- end
14
+ keys.each do |k|
15
+ self[k.to_s] = delete(k) unless String === k
18
16
  end
19
17
  self
20
18
  end
@@ -22,7 +20,7 @@ module Hashie
22
20
  # Convert all of the keys of a Hash
23
21
  # to their string representations.
24
22
  def hashie_stringify_keys
25
- self.dup.stringify_keys!
23
+ dup.stringify_keys!
26
24
  end
27
25
 
28
26
  # Convert this hash into a Mash
@@ -38,11 +36,11 @@ module Hashie
38
36
  end
39
37
 
40
38
  def hashie_inspect
41
- ret = "#<#{self.class.to_s}"
39
+ ret = "#<#{self.class}"
42
40
  stringify_keys.keys.sort.each do |key|
43
41
  ret << " #{key}=#{self[key].inspect}"
44
42
  end
45
- ret << ">"
43
+ ret << '>'
46
44
  ret
47
45
  end
48
46
  end
@@ -55,6 +55,7 @@ module Hashie
55
55
  # mash.author # => <Mash>
56
56
  #
57
57
  class Mash < Hash
58
+ ALLOWED_SUFFIXES = %w(? ! = _)
58
59
  include Hashie::PrettyInspect
59
60
  alias_method :to_s, :inspect
60
61
 
@@ -67,14 +68,14 @@ module Hashie
67
68
  default ? super(default) : super(&blk)
68
69
  end
69
70
 
70
- class << self; alias [] new; end
71
+ class << self; alias_method :[], :new; end
71
72
 
72
73
  def id #:nodoc:
73
- self["id"]
74
+ self['id']
74
75
  end
75
76
 
76
77
  def type #:nodoc:
77
- self["type"]
78
+ self['type']
78
79
  end
79
80
 
80
81
  alias_method :regular_reader, :[]
@@ -91,8 +92,8 @@ module Hashie
91
92
  # Sets an attribute in the Mash. Key will be converted to
92
93
  # a string before it is set, and Hashes will be converted
93
94
  # into Mashes for nesting purposes.
94
- def custom_writer(key,value) #:nodoc:
95
- regular_writer(convert_key(key), convert_value(value))
95
+ def custom_writer(key, value, convert = true) #:nodoc:
96
+ regular_writer(convert_key(key), convert ? convert_value(value) : value)
96
97
  end
97
98
 
98
99
  alias_method :[], :custom_reader
@@ -128,7 +129,7 @@ module Hashie
128
129
  alias_method :regular_dup, :dup
129
130
  # Duplicates the current mash as a new mash.
130
131
  def dup
131
- self.class.new(self, self.default)
132
+ self.class.new(self, default)
132
133
  end
133
134
 
134
135
  def key?(key)
@@ -148,14 +149,14 @@ module Hashie
148
149
  # Recursively merges this mash with the passed
149
150
  # in hash, merging each hash in the hierarchy.
150
151
  def deep_update(other_hash, &blk)
151
- other_hash.each_pair do |k,v|
152
+ other_hash.each_pair do |k, v|
152
153
  key = convert_key(k)
153
- if regular_reader(key).is_a?(Mash) and v.is_a?(::Hash)
154
+ if regular_reader(key).is_a?(Mash) && v.is_a?(::Hash)
154
155
  custom_reader(key).deep_update(v, &blk)
155
156
  else
156
157
  value = convert_value(v, true)
157
- value = blk.call(key, self[k], value) if blk
158
- custom_writer(key, value)
158
+ value = convert_value(blk.call(key, self[k], value), true) if blk
159
+ custom_writer(key, value, false)
159
160
  end
160
161
  end
161
162
  self
@@ -172,7 +173,7 @@ module Hashie
172
173
  # Merges (non-recursively) the hash from the argument,
173
174
  # changing the receiving hash
174
175
  def shallow_update(other_hash)
175
- other_hash.each_pair do |k,v|
176
+ other_hash.each_pair do |k, v|
176
177
  regular_writer(convert_key(k), convert_value(v, true))
177
178
  end
178
179
  self
@@ -186,25 +187,31 @@ module Hashie
186
187
 
187
188
  # Will return true if the Mash has had a key
188
189
  # set in addition to normal respond_to? functionality.
189
- def respond_to?(method_name, include_private=false)
190
- return true if key?(method_name) || method_name.to_s.slice(/[=?!_]\Z/)
190
+ def respond_to?(method_name, include_private = false)
191
+ return true if key?(method_name) || prefix_method?(method_name)
191
192
  super
192
193
  end
193
194
 
195
+ def prefix_method?(method_name)
196
+ method_name = method_name.to_s
197
+ method_name.end_with?(*ALLOWED_SUFFIXES) && key?(method_name.chop)
198
+ end
199
+
194
200
  def method_missing(method_name, *args, &blk)
195
201
  return self.[](method_name, &blk) if key?(method_name)
196
- match = method_name.to_s.match(/(.*?)([?=!_]?)$/)
202
+ suffixes_regex = ALLOWED_SUFFIXES.join
203
+ match = method_name.to_s.match(/(.*?)([#{suffixes_regex}]?)$/)
197
204
  case match[2]
198
- when "="
205
+ when '='
199
206
  self[match[1]] = args.first
200
- when "?"
207
+ when '?'
201
208
  !!self[match[1]]
202
- when "!"
209
+ when '!'
203
210
  initializing_reader(match[1])
204
- when "_"
211
+ when '_'
205
212
  underbang_reader(match[1])
206
213
  else
207
- default(method_name, *args, &blk)
214
+ default(method_name)
208
215
  end
209
216
  end
210
217
 
@@ -214,19 +221,19 @@ module Hashie
214
221
  key.to_s
215
222
  end
216
223
 
217
- def convert_value(val, duping=false) #:nodoc:
224
+ def convert_value(val, duping = false) #:nodoc:
218
225
  case val
219
- when self.class
220
- val.dup
221
- when Hash
222
- duping ? val.dup : val
223
- when ::Hash
224
- val = val.dup if duping
225
- self.class.new(val)
226
- when Array
227
- val.collect{ |e| convert_value(e) }
228
- else
229
- val
226
+ when self.class
227
+ val.dup
228
+ when Hash
229
+ duping ? val.dup : val
230
+ when ::Hash
231
+ val = val.dup if duping
232
+ self.class.new(val)
233
+ when Array
234
+ val.map { |e| convert_value(e) }
235
+ else
236
+ val
230
237
  end
231
238
  end
232
239
  end
@@ -0,0 +1,119 @@
1
+ module Hashie
2
+ #
3
+ # Rash is a Hash whose keys can be Regexps, or Ranges, which will
4
+ # match many input keys.
5
+ #
6
+ # A good use case for this class is routing URLs in a web framework.
7
+ # The Rash's keys match URL patterns, and the values specify actions
8
+ # which can handle the URL. When the Rash's value is proc, the proc
9
+ # will be automatically called with the regexp's matched groups as
10
+ # block arguments.
11
+ #
12
+ # Usage example:
13
+ #
14
+ # greeting = Hashie::Rash.new( /^Mr./ => "Hello sir!", /^Mrs./ => "Evening, madame." )
15
+ # greeting["Mr. Steve Austin"] #=> "Hello sir!"
16
+ # greeting["Mrs. Steve Austin"] #=> "Evening, madame."
17
+ #
18
+ # Note: The Rash is automatically optimized every 500 accesses
19
+ # (Regexps get sorted by how often they get matched).
20
+ # If this is too low or too high, you can tune it by
21
+ # setting: `rash.optimize_every = n`
22
+ #
23
+ class Rash
24
+ attr_accessor :optimize_every
25
+
26
+ def initialize(initial = {})
27
+ @hash = {}
28
+ @regexes = []
29
+ @ranges = []
30
+ @regex_counts = Hash.new(0)
31
+ @optimize_every = 500
32
+ @lookups = 0
33
+
34
+ update(initial)
35
+ end
36
+
37
+ def update(other)
38
+ other.each do |key, value|
39
+ self[key] = value
40
+ end
41
+
42
+ self
43
+ end
44
+
45
+ def []=(key, value)
46
+ case key
47
+ when Regexp
48
+ # key = normalize_regex(key) # this used to just do: /#{regexp}/
49
+ @regexes << key
50
+ when Range
51
+ @ranges << key
52
+ end
53
+ @hash[key] = value
54
+ end
55
+
56
+ #
57
+ # Return the first thing that matches the key.
58
+ #
59
+ def [](key)
60
+ all(key).first
61
+ end
62
+
63
+ #
64
+ # Return everything that matches the query.
65
+ #
66
+ def all(query)
67
+ return to_enum(:all, query) unless block_given?
68
+
69
+ if @hash.include? query
70
+ yield @hash[query]
71
+ return
72
+ end
73
+
74
+ case query
75
+ when String
76
+ optimize_if_necessary!
77
+
78
+ # see if any of the regexps match the string
79
+ @regexes.each do |regex|
80
+ match = regex.match(query)
81
+ if match
82
+ @regex_counts[regex] += 1
83
+ value = @hash[regex]
84
+ if value.respond_to? :call
85
+ yield value.call(match)
86
+ else
87
+ yield value
88
+ end
89
+ end
90
+ end
91
+
92
+ when Integer
93
+ # see if any of the ranges match the integer
94
+ @ranges.each do |range|
95
+ yield @hash[range] if range.include? query
96
+ end
97
+
98
+ when Regexp
99
+ # Reverse operation: `rash[/regexp/]` returns all the hash's string keys which match the regexp
100
+ @hash.each do |key, val|
101
+ yield val if key.is_a?(String) && query =~ key
102
+ end
103
+ end
104
+ end
105
+
106
+ def method_missing(*args, &block)
107
+ @hash.send(*args, &block)
108
+ end
109
+
110
+ private
111
+
112
+ def optimize_if_necessary!
113
+ if (@lookups += 1) >= @optimize_every
114
+ @regexes = @regex_counts.sort_by { |regex, count| -count }.map { |regex, count| regex }
115
+ @lookups = 0
116
+ end
117
+ end
118
+ end
119
+ end
@@ -8,7 +8,6 @@ module Hashie
8
8
  # such as a Java api, where the keys are named differently from how we would
9
9
  # in Ruby.
10
10
  class Trash < Dash
11
-
12
11
  # Defines a property on the Trash. Options are as follows:
13
12
  #
14
13
  # * <tt>:default</tt> - Specify a default value for this property, to be
@@ -20,33 +19,31 @@ module Hashie
20
19
  def self.property(property_name, options = {})
21
20
  super
22
21
 
22
+ options[:from] = options[:from].to_sym if options[:from]
23
+ property_name = property_name.to_sym
24
+
23
25
  if options[:from]
24
- if property_name.to_sym == options[:from].to_sym
25
- raise ArgumentError, "Property name (#{property_name}) and :from option must not be the same"
26
+ if property_name == options[:from]
27
+ fail ArgumentError, "Property name (#{property_name}) and :from option must not be the same"
28
+ end
29
+
30
+ translations[options[:from].to_sym] = property_name.to_sym
31
+
32
+ define_method "#{options[:from]}=" do |val|
33
+ with = options[:with] || options[:transform_with]
34
+ self[property_name.to_sym] = with.respond_to?(:call) ? with.call(val) : val
26
35
  end
27
- translations << options[:from].to_sym
28
- if options[:with].respond_to? :call
29
- class_eval do
30
- define_method "#{options[:from]}=" do |val|
31
- self[property_name.to_sym] = options[:with].call(val)
32
- end
33
- end
34
- else
35
- class_eval <<-RUBY
36
- def #{options[:from]}=(val)
37
- self[:#{property_name}] = val
38
- end
39
- RUBY
36
+ else
37
+ if options[:transform_with].respond_to? :call
38
+ transforms[property_name.to_sym] = options[:transform_with]
40
39
  end
41
- elsif options[:transform_with].respond_to? :call
42
- transforms[property_name.to_sym] = options[:transform_with]
43
40
  end
44
41
  end
45
42
 
46
43
  # Set a value on the Dash in a Hash-like way. Only works
47
44
  # on pre-existing properties.
48
45
  def []=(property, value)
49
- if self.class.translations.include? property.to_sym
46
+ if self.class.translations.key? property.to_sym
50
47
  send("#{property}=", value)
51
48
  elsif self.class.transforms.key? property.to_sym
52
49
  super property, self.class.transforms[property.to_sym].call(value)
@@ -55,10 +52,22 @@ module Hashie
55
52
  end
56
53
  end
57
54
 
55
+ def self.permitted_input_keys
56
+ @permitted_input_keys ||= properties.map { |property| inverse_translations.fetch property, property }
57
+ end
58
+
58
59
  private
59
60
 
61
+ def self.properties
62
+ @properties ||= []
63
+ end
64
+
60
65
  def self.translations
61
- @translations ||= []
66
+ @translations ||= {}
67
+ end
68
+
69
+ def self.inverse_translations
70
+ @inverse_translations ||= Hash[translations.map(&:reverse)]
62
71
  end
63
72
 
64
73
  def self.transforms
@@ -69,7 +78,7 @@ module Hashie
69
78
  #
70
79
  def property_exists?(property)
71
80
  unless self.class.property?(property.to_sym)
72
- raise NoMethodError, "The property '#{property}' is not defined for this Trash."
81
+ fail NoMethodError, "The property '#{property}' is not defined for this Trash."
73
82
  end
74
83
  true
75
84
  end
@@ -79,7 +88,7 @@ module Hashie
79
88
  # Deletes any keys that have a translation
80
89
  def initialize_attributes(attributes)
81
90
  return unless attributes
82
- attributes_copy = attributes.dup.delete_if do |k,v|
91
+ attributes_copy = attributes.dup.delete_if do |k, v|
83
92
  if self.class.translations.include?(k.to_sym)
84
93
  self[k] = v
85
94
  true