hashie 2.0.5 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
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