hashie 2.1.2 → 4.1.0

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