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
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) }