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
@@ -13,8 +13,9 @@ module Hashie
13
13
  # It is preferrable to a Struct because of the in-class
14
14
  # API for defining properties as well as per-property defaults.
15
15
  class Dash < Hash
16
- include PrettyInspect
17
- alias_method :to_s, :inspect
16
+ include Hashie::Extensions::PrettyInspect
17
+
18
+ alias to_s inspect
18
19
 
19
20
  # Defines a property on the Dash. Options are
20
21
  # as follows:
@@ -25,11 +26,14 @@ module Hashie
25
26
  #
26
27
  # * <tt>:required</tt> - Specify the value as required for this
27
28
  # property, to raise an error if a value is unset in a new or
28
- # existing Dash.
29
+ # existing Dash. If a Proc is provided, it will be run in the
30
+ # context of the Dash instance. If a Symbol is provided, the
31
+ # property it represents must not be nil. The property is only
32
+ # required if the value is truthy.
33
+ #
34
+ # * <tt>:message</tt> - Specify custom error message for required property
29
35
  #
30
36
  def self.property(property_name, options = {})
31
- property_name = property_name.to_sym
32
-
33
37
  properties << property_name
34
38
 
35
39
  if options.key?(:default)
@@ -38,30 +42,35 @@ module Hashie
38
42
  defaults.delete property_name
39
43
  end
40
44
 
41
- unless instance_methods.map { |m| m.to_s }.include?("#{property_name}=")
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) }
45
- end
45
+ define_getter_for(property_name)
46
+ define_setter_for(property_name)
47
+
48
+ @subclasses.each { |klass| klass.property(property_name, options) } if defined? @subclasses
46
49
 
47
- if defined? @subclasses
48
- @subclasses.each { |klass| klass.property(property_name, options) }
50
+ condition = options.delete(:required)
51
+ if condition
52
+ message = options.delete(:message) || "is required for #{name}."
53
+ required_properties[property_name] = { condition: condition, message: message }
54
+ elsif options.key?(:message)
55
+ raise ArgumentError, 'The :message option should be used with :required option.'
49
56
  end
50
- required_properties << property_name if options.delete(:required)
51
57
  end
52
58
 
53
59
  class << self
54
60
  attr_reader :properties, :defaults
61
+ attr_reader :getters
55
62
  attr_reader :required_properties
56
63
  end
57
64
  instance_variable_set('@properties', Set.new)
65
+ instance_variable_set('@getters', Set.new)
58
66
  instance_variable_set('@defaults', {})
59
- instance_variable_set('@required_properties', Set.new)
67
+ instance_variable_set('@required_properties', {})
60
68
 
61
69
  def self.inherited(klass)
62
70
  super
63
71
  (@subclasses ||= Set.new) << klass
64
72
  klass.instance_variable_set('@properties', properties.dup)
73
+ klass.instance_variable_set('@getters', getters.dup)
65
74
  klass.instance_variable_set('@defaults', defaults.dup)
66
75
  klass.instance_variable_set('@required_properties', required_properties.dup)
67
76
  end
@@ -69,13 +78,25 @@ module Hashie
69
78
  # Check to see if the specified property has already been
70
79
  # defined.
71
80
  def self.property?(name)
72
- properties.include? name.to_sym
81
+ properties.include? name
73
82
  end
74
83
 
75
84
  # Check to see if the specified property is
76
85
  # required.
77
86
  def self.required?(name)
78
- required_properties.include? name.to_sym
87
+ required_properties.key? name
88
+ end
89
+
90
+ private_class_method def self.define_getter_for(property_name)
91
+ return if getters.include?(property_name)
92
+ define_method(property_name) { |&block| self.[](property_name, &block) }
93
+ getters << property_name
94
+ end
95
+
96
+ private_class_method def self.define_setter_for(property_name)
97
+ setter = :"#{property_name}="
98
+ return if instance_methods.include?(setter)
99
+ define_method(setter) { |value| self.[]=(property_name, value) }
79
100
  end
80
101
 
81
102
  # You may initialize a Dash with an attributes hash
@@ -85,25 +106,30 @@ module Hashie
85
106
 
86
107
  self.class.defaults.each_pair do |prop, value|
87
108
  self[prop] = begin
88
- value.dup
109
+ val = value.dup
110
+ if val.is_a?(Proc)
111
+ val.arity == 1 ? val.call(self) : val.call
112
+ else
113
+ val
114
+ end
89
115
  rescue TypeError
90
116
  value
91
117
  end
92
118
  end
93
119
 
94
120
  initialize_attributes(attributes)
95
- assert_required_properties_set!
121
+ assert_required_attributes_set!
96
122
  end
97
123
 
98
- alias_method :_regular_reader, :[]
99
- alias_method :_regular_writer, :[]=
124
+ alias _regular_reader []
125
+ alias _regular_writer []=
100
126
  private :_regular_reader, :_regular_writer
101
127
 
102
128
  # Retrieve a value from the Dash (will return the
103
129
  # property's default value if it hasn't been set).
104
130
  def [](property)
105
131
  assert_property_exists! property
106
- value = super(property.to_s)
132
+ value = super(property)
107
133
  # If the value is a lambda, proc, or whatever answers to call, eval the thing!
108
134
  if value.is_a? Proc
109
135
  self[property] = value.call # Set the result of the call as a value
@@ -118,7 +144,7 @@ module Hashie
118
144
  def []=(property, value)
119
145
  assert_property_required! property, value
120
146
  assert_property_exists! property
121
- super(property.to_s, value)
147
+ super(property, value)
122
148
  end
123
149
 
124
150
  def merge(other_hash)
@@ -143,35 +169,72 @@ module Hashie
143
169
  self
144
170
  end
145
171
 
172
+ def update_attributes!(attributes)
173
+ update_attributes(attributes)
174
+
175
+ self.class.defaults.each_pair do |prop, value|
176
+ next unless self[prop].nil?
177
+ self[prop] = begin
178
+ value.dup
179
+ rescue TypeError
180
+ value
181
+ end
182
+ end
183
+ assert_required_attributes_set!
184
+ end
185
+
146
186
  private
147
187
 
148
188
  def initialize_attributes(attributes)
189
+ return unless attributes
190
+
191
+ cleaned_attributes = attributes.reject { |_attr, value| value.nil? }
192
+ update_attributes(cleaned_attributes)
193
+ end
194
+
195
+ def update_attributes(attributes)
196
+ return unless attributes
197
+
149
198
  attributes.each_pair do |att, value|
150
199
  self[att] = value
151
- end if attributes
200
+ end
152
201
  end
153
202
 
154
203
  def assert_property_exists!(property)
155
- unless self.class.property?(property)
156
- fail NoMethodError, "The property '#{property}' is not defined for this Dash."
157
- end
204
+ fail_no_property_error!(property) unless self.class.property?(property)
158
205
  end
159
206
 
160
- def assert_required_properties_set!
161
- self.class.required_properties.each do |required_property|
207
+ def assert_required_attributes_set!
208
+ self.class.required_properties.each_key do |required_property|
162
209
  assert_property_set!(required_property)
163
210
  end
164
211
  end
165
212
 
166
213
  def assert_property_set!(property)
167
- if send(property).nil?
168
- fail ArgumentError, "The property '#{property}' is required for this Dash."
169
- end
214
+ fail_property_required_error!(property) if send(property).nil? && required?(property)
170
215
  end
171
216
 
172
217
  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."
218
+ fail_property_required_error!(property) if value.nil? && required?(property)
219
+ end
220
+
221
+ def fail_property_required_error!(property)
222
+ raise ArgumentError,
223
+ "The property '#{property}' #{self.class.required_properties[property][:message]}"
224
+ end
225
+
226
+ def fail_no_property_error!(property)
227
+ raise NoMethodError, "The property '#{property}' is not defined for #{self.class.name}."
228
+ end
229
+
230
+ def required?(property)
231
+ return false unless self.class.required?(property)
232
+
233
+ condition = self.class.required_properties[property][:condition]
234
+ case condition
235
+ when Proc then !!instance_exec(&condition)
236
+ when Symbol then !!send(condition)
237
+ else !!condition
175
238
  end
176
239
  end
177
240
  end
@@ -0,0 +1,14 @@
1
+ module Hashie
2
+ module Extensions
3
+ module ActiveSupport
4
+ module CoreExt
5
+ module Hash
6
+ def except(*keys)
7
+ string_keys = keys.map { |key| convert_key(key) }
8
+ super(*string_keys)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,19 @@
1
+ module Hashie
2
+ module Extensions
3
+ module Array
4
+ module PrettyInspect
5
+ def self.included(base)
6
+ base.send :alias_method, :array_inspect, :inspect
7
+ base.send :alias_method, :inspect, :hashie_inspect
8
+ end
9
+
10
+ def hashie_inspect
11
+ ret = "#<#{self.class} ["
12
+ ret << to_a.map(&:inspect).join(', ')
13
+ ret << ']>'
14
+ ret
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,27 +1,56 @@
1
1
  module Hashie
2
+ class CoercionError < StandardError
3
+ def initialize(key, value, into, message)
4
+ super("Cannot coerce property #{key.inspect} from #{value.class} to #{into}: #{message}")
5
+ end
6
+ end
7
+
2
8
  module Extensions
3
9
  module Coercion
10
+ CORE_TYPES = {
11
+ Integer => :to_i,
12
+ Float => :to_f,
13
+ Complex => :to_c,
14
+ Rational => :to_r,
15
+ String => :to_s,
16
+ Symbol => :to_sym
17
+ }.freeze
18
+
19
+ ABSTRACT_CORE_TYPES =
20
+ if RubyVersion.new(RUBY_VERSION) >= RubyVersion.new('2.4.0')
21
+ { Numeric => [Integer, Float, Complex, Rational] }
22
+ else
23
+ {
24
+ Integer => [Fixnum, Bignum],
25
+ Numeric => [Fixnum, Bignum, Float, Complex, Rational]
26
+ }
27
+ end
28
+
4
29
  def self.included(base)
5
- base.extend ClassMethods
6
30
  base.send :include, InstanceMethods
31
+ base.extend ClassMethods
32
+ unless base.method_defined?(:set_value_without_coercion)
33
+ base.send :alias_method, :set_value_without_coercion, :[]=
34
+ end
35
+ base.send :alias_method, :[]=, :set_value_with_coercion
7
36
  end
8
37
 
9
38
  module InstanceMethods
10
- def []=(key, value)
39
+ def set_value_with_coercion(key, value)
11
40
  into = self.class.key_coercion(key) || self.class.value_coercion(value)
12
41
 
13
- if value && into
14
- if into.respond_to?(:coerce)
15
- value = into.coerce(value)
16
- else
17
- value = into.new(value)
42
+ unless value.nil? || into.nil?
43
+ begin
44
+ value = self.class.fetch_coercion(into).call(value)
45
+ rescue NoMethodError, TypeError => e
46
+ raise CoercionError.new(key, value, into, e.message)
18
47
  end
19
48
  end
20
49
 
21
- super(key, value)
50
+ set_value_without_coercion(key, value)
22
51
  end
23
52
 
24
- def custom_writer(key, value, convert = true)
53
+ def custom_writer(key, value, _convert = true)
25
54
  self[key] = value
26
55
  end
27
56
 
@@ -33,6 +62,9 @@ module Hashie
33
62
  end
34
63
 
35
64
  module ClassMethods
65
+ attr_writer :key_coercions
66
+ protected :key_coercions=
67
+
36
68
  # Set up a coercion rule such that any time the specified
37
69
  # key is set it will be coerced into the specified class.
38
70
  # Coercion will occur by first attempting to call Class.coerce
@@ -48,16 +80,15 @@ module Hashie
48
80
  # coerce_key :user, User
49
81
  # end
50
82
  def coerce_key(*attrs)
51
- @key_coercions ||= {}
52
83
  into = attrs.pop
53
- attrs.each { |key| @key_coercions[key] = into }
84
+ attrs.each { |key| key_coercions[key] = into }
54
85
  end
55
86
 
56
- alias_method :coerce_keys, :coerce_key
87
+ alias coerce_keys coerce_key
57
88
 
58
89
  # Returns a hash of any existing key coercions.
59
90
  def key_coercions
60
- @key_coercions || {}
91
+ @key_coercions ||= {}
61
92
  end
62
93
 
63
94
  # Returns the specific key coercion for the specified key,
@@ -72,7 +103,8 @@ module Hashie
72
103
  #
73
104
  # @param [Class] from the type you would like coerced.
74
105
  # @param [Class] into the class into which you would like the value coerced.
75
- # @option options [Boolean] :strict (true) whether use exact source class only or include ancestors
106
+ # @option options [Boolean] :strict (true) whether use exact source class
107
+ # only or include ancestors
76
108
  #
77
109
  # @example Coerce all hashes into this special type of hash
78
110
  # class SpecialHash < Hash
@@ -89,11 +121,17 @@ module Hashie
89
121
  def coerce_value(from, into, options = {})
90
122
  options = { strict: true }.merge(options)
91
123
 
124
+ if ABSTRACT_CORE_TYPES.key? from
125
+ ABSTRACT_CORE_TYPES[from].each do |type|
126
+ coerce_value type, into, options
127
+ end
128
+ end
129
+
92
130
  if options[:strict]
93
- (@strict_value_coercions ||= {})[from] = into
131
+ strict_value_coercions[from] = into
94
132
  else
95
133
  while from.superclass && from.superclass != Object
96
- (@lenient_value_coercions ||= {})[from] = into
134
+ lenient_value_coercions[from] = into
97
135
  from = from.superclass
98
136
  end
99
137
  end
@@ -101,11 +139,12 @@ module Hashie
101
139
 
102
140
  # Return all value coercions that have the :strict rule as true.
103
141
  def strict_value_coercions
104
- @strict_value_coercions || {}
142
+ @strict_value_coercions ||= {}
105
143
  end
144
+
106
145
  # Return all value coercions that have the :strict rule as false.
107
146
  def lenient_value_coercions
108
- @value_coercions || {}
147
+ @lenient_value_coercions ||= {}
109
148
  end
110
149
 
111
150
  # Fetch the value coercion, if any, for the specified object.
@@ -113,6 +152,73 @@ module Hashie
113
152
  from = value.class
114
153
  strict_value_coercions[from] || lenient_value_coercions[from]
115
154
  end
155
+
156
+ def fetch_coercion(type)
157
+ return type if type.is_a? Proc
158
+ coercion_cache[type]
159
+ end
160
+
161
+ def coercion_cache
162
+ @coercion_cache ||= ::Hash.new do |hash, type|
163
+ hash[type] = build_coercion(type)
164
+ end
165
+ end
166
+
167
+ def build_coercion(type)
168
+ if type.is_a? Enumerable
169
+ if type.class == ::Hash
170
+ type, key_type, value_type = type.class, *type.first
171
+ build_hash_coercion(type, key_type, value_type)
172
+ else
173
+ value_type = type.first
174
+ type = type.class
175
+ build_container_coercion(type, value_type)
176
+ end
177
+ elsif CORE_TYPES.key? type
178
+ build_core_type_coercion(type)
179
+ elsif type.respond_to? :coerce
180
+ lambda do |value|
181
+ return value if value.is_a? type
182
+ type.coerce(value)
183
+ end
184
+ elsif type.respond_to? :new
185
+ lambda do |value|
186
+ return value if value.is_a? type
187
+ type.new(value)
188
+ end
189
+ else
190
+ raise TypeError, "#{type} is not a coercable type"
191
+ end
192
+ end
193
+
194
+ def build_hash_coercion(type, key_type, value_type)
195
+ key_coerce = fetch_coercion(key_type)
196
+ value_coerce = fetch_coercion(value_type)
197
+ lambda do |value|
198
+ type[value.map { |k, v| [key_coerce.call(k), value_coerce.call(v)] }]
199
+ end
200
+ end
201
+
202
+ def build_container_coercion(type, value_type)
203
+ value_coerce = fetch_coercion(value_type)
204
+ lambda do |value|
205
+ type.new(value.map { |v| value_coerce.call(v) })
206
+ end
207
+ end
208
+
209
+ def build_core_type_coercion(type)
210
+ name = CORE_TYPES[type]
211
+ lambda do |value|
212
+ return value if value.is_a? type
213
+ return value.send(name)
214
+ end
215
+ end
216
+
217
+ def inherited(klass)
218
+ super
219
+
220
+ klass.key_coercions = key_coercions.dup
221
+ end
116
222
  end
117
223
  end
118
224
  end