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
data/Rakefile CHANGED
@@ -6,8 +6,10 @@ Bundler::GemHelper.install_tasks
6
6
 
7
7
  require 'rspec/core/rake_task'
8
8
  RSpec::Core::RakeTask.new do |spec|
9
- # spec.libs << 'lib' << 'spec'
10
9
  spec.pattern = 'spec/**/*_spec.rb'
11
10
  end
12
11
 
13
- task :default => :spec
12
+ require 'rubocop/rake_task'
13
+ Rubocop::RakeTask.new(:rubocop)
14
+
15
+ task default: [:rubocop, :spec]
@@ -3,8 +3,8 @@ require File.expand_path('../lib/hashie/version', __FILE__)
3
3
  Gem::Specification.new do |gem|
4
4
  gem.authors = ["Michael Bleigh", "Jerry Cheung"]
5
5
  gem.email = ["michael@intridea.com", "jollyjerry@gmail.com"]
6
- gem.description = %q{Hashie is a small collection of tools that make hashes more powerful. Currently includes Mash (Mocking Hash) and Dash (Discrete Hash).}
7
- gem.summary = %q{Your friendly neighborhood hash toolkit.}
6
+ gem.description = %q{Hashie is a collection of classes and mixins that make hashes more powerful.}
7
+ gem.summary = %q{Your friendly neighborhood hash library.}
8
8
  gem.homepage = 'https://github.com/intridea/hashie'
9
9
 
10
10
  gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
@@ -15,9 +15,6 @@ Gem::Specification.new do |gem|
15
15
  gem.version = Hashie::VERSION
16
16
  gem.license = "MIT"
17
17
 
18
- gem.add_development_dependency 'rake', '~> 0.9.2'
19
- gem.add_development_dependency 'rspec', '~> 2.5'
20
- gem.add_development_dependency 'guard'
21
- gem.add_development_dependency 'guard-rspec'
22
- gem.add_development_dependency 'growl'
18
+ gem.add_development_dependency 'rake'
19
+ gem.add_development_dependency 'rspec'
23
20
  end
@@ -6,11 +6,13 @@ module Hashie
6
6
  autoload :Mash, 'hashie/mash'
7
7
  autoload :PrettyInspect, 'hashie/hash_extensions'
8
8
  autoload :Trash, 'hashie/trash'
9
+ autoload :Rash, 'hashie/rash'
9
10
 
10
11
  module Extensions
11
12
  autoload :Coercion, 'hashie/extensions/coercion'
12
13
  autoload :DeepMerge, 'hashie/extensions/deep_merge'
13
14
  autoload :KeyConversion, 'hashie/extensions/key_conversion'
15
+ autoload :IgnoreUndeclared, 'hashie/extensions/ignore_undeclared'
14
16
  autoload :IndifferentAccess, 'hashie/extensions/indifferent_access'
15
17
  autoload :MergeInitializer, 'hashie/extensions/merge_initializer'
16
18
  autoload :MethodAccess, 'hashie/extensions/method_access'
@@ -19,5 +21,6 @@ module Hashie
19
21
  autoload :MethodWriter, 'hashie/extensions/method_access'
20
22
  autoload :StringifyKeys, 'hashie/extensions/key_conversion'
21
23
  autoload :SymbolizeKeys, 'hashie/extensions/key_conversion'
24
+ autoload :DeepFetch, 'hashie/extensions/deep_fetch'
22
25
  end
23
26
  end
@@ -3,10 +3,10 @@ require 'hashie/hash'
3
3
  module Hashie
4
4
  #
5
5
  # A Clash is a "Chainable Lazy Hash". Inspired by libraries such as Arel,
6
- # a Clash allows you to chain together method arguments to build a
6
+ # a Clash allows you to chain together method arguments to build a
7
7
  # hash, something that's especially useful if you're doing something
8
8
  # like constructing a complex options hash. Here's a basic example:
9
- #
9
+ #
10
10
  # c = Hashie::Clash.new.conditions(:foo => 'bar').order(:created_at)
11
11
  # c # => {:conditions => {:foo => 'bar'}, :order => :created_at}
12
12
  #
@@ -15,7 +15,7 @@ module Hashie
15
15
  # back out again with the _end! method. Example:
16
16
  #
17
17
  # c = Hashie::Clash.new.conditions!.foo('bar').baz(123)._end!.order(:created_at)
18
- # c # => {:conditions => {:foo => 'bar', :baz => 123}, :order => :created_at}
18
+ # c # => { conditions: { foo: 'bar', baz: 123 }, order: :created_at}
19
19
  #
20
20
  # Because the primary functionality of Clash is to build options objects,
21
21
  # all keys are converted to symbols since many libraries expect symbols explicitly
@@ -25,7 +25,7 @@ module Hashie
25
25
  class ChainError < ::StandardError; end
26
26
  # The parent Clash if this Clash was created via chaining.
27
27
  attr_reader :_parent
28
-
28
+
29
29
  # Initialize a new clash by passing in a Hash to
30
30
  # convert and, optionally, the parent to which this
31
31
  # Clash is chained.
@@ -35,7 +35,7 @@ module Hashie
35
35
  self[k.to_sym] = v
36
36
  end
37
37
  end
38
-
38
+
39
39
  # Jump back up a level if you are using bang method
40
40
  # chaining. For example:
41
41
  #
@@ -43,44 +43,44 @@ module Hashie
43
43
  # c.baz!.foo(123) # => c[:baz]
44
44
  # c.baz!._end! # => c
45
45
  def _end!
46
- self._parent
46
+ _parent
47
47
  end
48
-
48
+
49
49
  def id(*args) #:nodoc:
50
50
  method_missing(:id, *args)
51
51
  end
52
-
52
+
53
53
  def merge_store(key, *args) #:nodoc:
54
54
  case args.length
55
- when 1
56
- val = args.first
57
- val = self[key].merge(val) if self[key].is_a?(::Hash) && val.is_a?(::Hash)
58
- else
59
- val = args
55
+ when 1
56
+ val = args.first
57
+ val = self[key].merge(val) if self[key].is_a?(::Hash) && val.is_a?(::Hash)
58
+ else
59
+ val = args
60
60
  end
61
61
 
62
62
  self[key.to_sym] = val
63
63
  self
64
64
  end
65
-
65
+
66
66
  def method_missing(name, *args) #:nodoc:
67
67
  name = name.to_s
68
68
  if name.match(/!$/) && args.empty?
69
69
  key = name[0...-1].to_sym
70
-
70
+
71
71
  if self[key].nil?
72
72
  self[key] = Clash.new({}, self)
73
73
  elsif self[key].is_a?(::Hash) && !self[key].is_a?(Clash)
74
74
  self[key] = Clash.new(self[key], self)
75
75
  else
76
- raise ChainError, "Tried to chain into a non-hash key."
76
+ fail ChainError, 'Tried to chain into a non-hash key.'
77
77
  end
78
-
78
+
79
79
  self[key]
80
80
  elsif args.any?
81
81
  key = name.to_sym
82
- self.merge_store(key, *args)
82
+ merge_store(key, *args)
83
83
  end
84
84
  end
85
85
  end
86
- end
86
+ end
@@ -30,24 +30,18 @@ module Hashie
30
30
  def self.property(property_name, options = {})
31
31
  property_name = property_name.to_sym
32
32
 
33
- self.properties << property_name
33
+ properties << property_name
34
34
 
35
- if options.has_key?(:default)
36
- self.defaults[property_name] = options[:default]
37
- elsif self.defaults.has_key?(property_name)
38
- self.defaults.delete property_name
35
+ if options.key?(:default)
36
+ defaults[property_name] = options[:default]
37
+ elsif defaults.key?(property_name)
38
+ defaults.delete property_name
39
39
  end
40
40
 
41
41
  unless instance_methods.map { |m| m.to_s }.include?("#{property_name}=")
42
- class_eval <<-ACCESSORS
43
- def #{property_name}(&block)
44
- self.[](#{property_name.to_s.inspect}, &block)
45
- end
46
-
47
- def #{property_name}=(value)
48
- self.[]=(#{property_name.to_s.inspect}, value)
49
- end
50
- ACCESSORS
42
+ define_method(property_name) { |&block| self.[](property_name.to_s, &block) }
43
+ property_assignment = property_name.to_s.concat('=').to_sym
44
+ define_method(property_assignment) { |value| self.[]=(property_name.to_s, value) }
51
45
  end
52
46
 
53
47
  if defined? @subclasses
@@ -67,9 +61,9 @@ module Hashie
67
61
  def self.inherited(klass)
68
62
  super
69
63
  (@subclasses ||= Set.new) << klass
70
- klass.instance_variable_set('@properties', self.properties.dup)
71
- klass.instance_variable_set('@defaults', self.defaults.dup)
72
- klass.instance_variable_set('@required_properties', self.required_properties.dup)
64
+ klass.instance_variable_set('@properties', properties.dup)
65
+ klass.instance_variable_set('@defaults', defaults.dup)
66
+ klass.instance_variable_set('@required_properties', required_properties.dup)
73
67
  end
74
68
 
75
69
  # Check to see if the specified property has already been
@@ -127,6 +121,21 @@ module Hashie
127
121
  super(property.to_s, value)
128
122
  end
129
123
 
124
+ def merge(other_hash)
125
+ new_dash = dup
126
+ other_hash.each do |k, v|
127
+ new_dash[k] = block_given? ? yield(k, self[k], v) : v
128
+ end
129
+ new_dash
130
+ end
131
+
132
+ def merge!(other_hash)
133
+ other_hash.each do |k, v|
134
+ self[k] = block_given? ? yield(k, self[k], v) : v
135
+ end
136
+ self
137
+ end
138
+
130
139
  def replace(other_hash)
131
140
  other_hash = self.class.defaults.merge(other_hash)
132
141
  (keys - other_hash.keys).each { |key| delete(key) }
@@ -136,35 +145,34 @@ module Hashie
136
145
 
137
146
  private
138
147
 
139
- def initialize_attributes(attributes)
140
- attributes.each_pair do |att, value|
141
- self[att] = value
142
- end if attributes
143
- end
148
+ def initialize_attributes(attributes)
149
+ attributes.each_pair do |att, value|
150
+ self[att] = value
151
+ end if attributes
152
+ end
144
153
 
145
- def assert_property_exists!(property)
146
- unless self.class.property?(property)
147
- raise NoMethodError, "The property '#{property}' is not defined for this Dash."
148
- end
154
+ def assert_property_exists!(property)
155
+ unless self.class.property?(property)
156
+ fail NoMethodError, "The property '#{property}' is not defined for this Dash."
149
157
  end
158
+ end
150
159
 
151
- def assert_required_properties_set!
152
- self.class.required_properties.each do |required_property|
153
- assert_property_set!(required_property)
154
- end
160
+ def assert_required_properties_set!
161
+ self.class.required_properties.each do |required_property|
162
+ assert_property_set!(required_property)
155
163
  end
164
+ end
156
165
 
157
- def assert_property_set!(property)
158
- if send(property).nil?
159
- raise ArgumentError, "The property '#{property}' is required for this Dash."
160
- end
166
+ def assert_property_set!(property)
167
+ if send(property).nil?
168
+ fail ArgumentError, "The property '#{property}' is required for this Dash."
161
169
  end
170
+ end
162
171
 
163
- def assert_property_required!(property, value)
164
- if self.class.required?(property) && value.nil?
165
- raise ArgumentError, "The property '#{property}' is required for this Dash."
166
- end
172
+ def assert_property_required!(property, value)
173
+ if self.class.required?(property) && value.nil?
174
+ fail ArgumentError, "The property '#{property}' is required for this Dash."
167
175
  end
168
-
176
+ end
169
177
  end
170
178
  end
@@ -2,7 +2,7 @@ module Hashie
2
2
  module Extensions
3
3
  module Coercion
4
4
  def self.included(base)
5
- base.send :extend, ClassMethods
5
+ base.extend ClassMethods
6
6
  base.send :include, InstanceMethods
7
7
  end
8
8
 
@@ -21,7 +21,7 @@ module Hashie
21
21
  super(key, value)
22
22
  end
23
23
 
24
- def custom_writer(key, value)
24
+ def custom_writer(key, value, convert = true)
25
25
  self[key] = value
26
26
  end
27
27
 
@@ -53,7 +53,7 @@ module Hashie
53
53
  attrs.each { |key| @key_coercions[key] = into }
54
54
  end
55
55
 
56
- alias :coerce_keys :coerce_key
56
+ alias_method :coerce_keys, :coerce_key
57
57
 
58
58
  # Returns a hash of any existing key coercions.
59
59
  def key_coercions
@@ -87,7 +87,7 @@ module Hashie
87
87
  # end
88
88
  # end
89
89
  def coerce_value(from, into, options = {})
90
- options = {:strict => true}.merge(options)
90
+ options = { strict: true }.merge(options)
91
91
 
92
92
  if options[:strict]
93
93
  (@strict_value_coercions ||= {})[from] = into
@@ -100,9 +100,13 @@ module Hashie
100
100
  end
101
101
 
102
102
  # Return all value coercions that have the :strict rule as true.
103
- def strict_value_coercions; @strict_value_coercions || {} end
103
+ def strict_value_coercions
104
+ @strict_value_coercions || {}
105
+ end
104
106
  # Return all value coercions that have the :strict rule as false.
105
- def lenient_value_coercions; @value_coercions || {} end
107
+ def lenient_value_coercions
108
+ @value_coercions || {}
109
+ end
106
110
 
107
111
  # Fetch the value coercion, if any, for the specified object.
108
112
  def value_coercion(value)
@@ -0,0 +1,29 @@
1
+ module Hashie
2
+ module Extensions
3
+ # Searches a deeply nested datastructure for a key path, and returns the associated value.
4
+ #
5
+ # options = { user: { location: { address: '123 Street' } } }
6
+ # options.deep_fetch :user, :location, :address #=> '123 Street'
7
+ #
8
+ # If a block is provided its value will be returned if the key does not exist.
9
+ #
10
+ # options.deep_fetch(:user, :non_existent_key) { 'a value' } #=> 'a value'
11
+ #
12
+ # This is particularly useful for fetching values from deeply nested api responses or params hashes.
13
+ module DeepFetch
14
+ class UndefinedPathError < StandardError; end
15
+
16
+ def deep_fetch(*args, &block)
17
+ args.reduce(self) do |obj, arg|
18
+ begin
19
+ arg = Integer(arg) if obj.kind_of? Array
20
+ obj.fetch(arg)
21
+ rescue ArgumentError, IndexError => e
22
+ break block.call(arg) if block
23
+ raise UndefinedPathError, "Could not fetch path (#{args.join(' > ')}) at #{arg}", e.backtrace
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -3,19 +3,28 @@ 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)
6
- (class << (h = dup); self; end).send :include, Hashie::Extensions::DeepMerge
7
- h.deep_merge!(other_hash)
6
+ dup.deep_merge!(other_hash)
8
7
  end
9
8
 
10
9
  # Returns a new hash with +self+ and +other_hash+ merged recursively.
11
10
  # Modifies the receiver in place.
12
11
  def deep_merge!(other_hash)
13
- other_hash.each do |k,v|
14
- (class << (tv = self[k]); self; end).send :include, Hashie::Extensions::DeepMerge
15
- self[k] = tv.is_a?(::Hash) && v.is_a?(::Hash) ? tv.deep_merge(v) : v
16
- end
12
+ _recursive_merge(self, other_hash)
17
13
  self
18
14
  end
15
+
16
+ private
17
+
18
+ def _recursive_merge(hash, other_hash)
19
+ if other_hash.is_a?(::Hash) && hash.is_a?(::Hash)
20
+ other_hash.each do |k, v|
21
+ hash[k] = hash.key?(k) ? _recursive_merge(hash[k], v) : v
22
+ end
23
+ hash
24
+ else
25
+ other_hash
26
+ end
27
+ end
19
28
  end
20
29
  end
21
30
  end
@@ -0,0 +1,41 @@
1
+ module Hashie
2
+ module Extensions
3
+ # IgnoreUndeclared is a simple mixin that silently ignores
4
+ # undeclared properties on initialization instead of
5
+ # raising an error. This is useful when using a Trash to
6
+ # capture a subset of a larger hash.
7
+ #
8
+ # Note that attempting to retrieve an undeclared property
9
+ # will still raise a NoMethodError, even if a value for
10
+ # that property was provided at initialization.
11
+ #
12
+ # @example
13
+ # class Person < Trash
14
+ # include Hashie::Extensions::IgnoreUndeclared
15
+ #
16
+ # property :first_name
17
+ # property :last_name
18
+ # end
19
+ #
20
+ # user_data = {
21
+ # :first_name => 'Freddy',
22
+ # :last_name => 'Nostrils',
23
+ # :email => 'freddy@example.com'
24
+ # }
25
+ #
26
+ # p = Person.new(user_data) # 'email' is silently ignored
27
+ #
28
+ # p.first_name # => 'Freddy'
29
+ # p.last_name # => 'Nostrils'
30
+ # p.email # => NoMethodError
31
+ module IgnoreUndeclared
32
+ def initialize_attributes(attributes)
33
+ attributes.each_pair do |att, value|
34
+ if self.class.property?(att) || (self.class.respond_to?(:translations) && self.class.translations.include?(att.to_sym))
35
+ self[att] = value
36
+ end
37
+ end if attributes
38
+ end
39
+ end
40
+ end
41
+ end
@@ -27,6 +27,7 @@ module Hashie
27
27
  base.class_eval do
28
28
  alias_method :regular_writer, :[]=
29
29
  alias_method :[]=, :indifferent_writer
30
+ alias_method :store, :indifferent_writer
30
31
  %w(default update replace fetch delete key? values_at).each do |m|
31
32
  alias_method "regular_#{m}", m
32
33
  alias_method m, "indifferent_#{m}"
@@ -35,6 +36,16 @@ module Hashie
35
36
  %w(include? member? has_key?).each do |key_alias|
36
37
  alias_method key_alias, :indifferent_key?
37
38
  end
39
+
40
+ class << self
41
+ def [](*)
42
+ super.convert!
43
+ end
44
+
45
+ def try_convert(*)
46
+ (hash = super) && self[hash]
47
+ end
48
+ end
38
49
  end
39
50
  end
40
51
 
@@ -61,7 +72,7 @@ module Hashie
61
72
  # is injecting itself into member hashes.
62
73
  def convert!
63
74
  keys.each do |k|
64
- regular_writer convert_key(k), convert_value(self.regular_delete(k))
75
+ regular_writer convert_key(k), convert_value(regular_delete(k))
65
76
  end
66
77
  self
67
78
  end
@@ -75,7 +86,7 @@ module Hashie
75
86
  value
76
87
  end
77
88
  end
78
-
89
+
79
90
  def indifferent_default(key = nil)
80
91
  return self[convert_key(key)] if key?(key)
81
92
  regular_default(key)
@@ -83,18 +94,34 @@ module Hashie
83
94
 
84
95
  def indifferent_update(other_hash)
85
96
  return regular_update(other_hash) if hash_with_indifference?(other_hash)
86
- other_hash.each_pair do |k,v|
97
+ other_hash.each_pair do |k, v|
87
98
  self[k] = v
88
99
  end
89
100
  end
90
-
91
- def indifferent_writer(key, value); regular_writer convert_key(key), convert_value(value) end
92
- def indifferent_fetch(key, *args); regular_fetch convert_key(key), *args end
93
- def indifferent_delete(key); regular_delete convert_key(key) end
94
- def indifferent_key?(key); regular_key? convert_key(key) end
95
- def indifferent_values_at(*indices); indices.map{|i| self[i] } end
96
101
 
97
- def indifferent_access?; true end
102
+ def indifferent_writer(key, value)
103
+ regular_writer convert_key(key), convert_value(value)
104
+ end
105
+
106
+ def indifferent_fetch(key, *args)
107
+ regular_fetch convert_key(key), *args
108
+ end
109
+
110
+ def indifferent_delete(key)
111
+ regular_delete convert_key(key)
112
+ end
113
+
114
+ def indifferent_key?(key)
115
+ regular_key? convert_key(key)
116
+ end
117
+
118
+ def indifferent_values_at(*indices)
119
+ indices.map { |i| self[i] }
120
+ end
121
+
122
+ def indifferent_access?
123
+ true
124
+ end
98
125
 
99
126
  def indifferent_replace(other_hash)
100
127
  (keys - other_hash.keys).each { |key| delete(key) }