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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +524 -59
- data/CONTRIBUTING.md +24 -7
- data/README.md +781 -90
- data/Rakefile +19 -2
- data/UPGRADING.md +245 -0
- data/hashie.gemspec +21 -13
- data/lib/hashie.rb +60 -21
- data/lib/hashie/array.rb +21 -0
- data/lib/hashie/clash.rb +24 -12
- data/lib/hashie/dash.rb +96 -33
- data/lib/hashie/extensions/active_support/core_ext/hash.rb +14 -0
- data/lib/hashie/extensions/array/pretty_inspect.rb +19 -0
- data/lib/hashie/extensions/coercion.rb +124 -18
- data/lib/hashie/extensions/dash/coercion.rb +25 -0
- data/lib/hashie/extensions/dash/indifferent_access.rb +56 -0
- data/lib/hashie/extensions/dash/property_translation.rb +191 -0
- data/lib/hashie/extensions/deep_fetch.rb +7 -5
- data/lib/hashie/extensions/deep_find.rb +69 -0
- data/lib/hashie/extensions/deep_locate.rb +113 -0
- data/lib/hashie/extensions/deep_merge.rb +35 -12
- data/lib/hashie/extensions/ignore_undeclared.rb +11 -5
- data/lib/hashie/extensions/indifferent_access.rb +28 -16
- data/lib/hashie/extensions/key_conflict_warning.rb +55 -0
- data/lib/hashie/extensions/key_conversion.rb +0 -82
- data/lib/hashie/extensions/mash/define_accessors.rb +90 -0
- data/lib/hashie/extensions/mash/keep_original_keys.rb +53 -0
- data/lib/hashie/extensions/mash/permissive_respond_to.rb +61 -0
- data/lib/hashie/extensions/mash/safe_assignment.rb +18 -0
- data/lib/hashie/extensions/mash/symbolize_keys.rb +38 -0
- data/lib/hashie/extensions/method_access.rb +154 -11
- data/lib/hashie/extensions/parsers/yaml_erb_parser.rb +48 -0
- data/lib/hashie/extensions/pretty_inspect.rb +19 -0
- data/lib/hashie/extensions/ruby_version.rb +60 -0
- data/lib/hashie/extensions/ruby_version_check.rb +21 -0
- data/lib/hashie/extensions/strict_key_access.rb +77 -0
- data/lib/hashie/extensions/stringify_keys.rb +71 -0
- data/lib/hashie/extensions/symbolize_keys.rb +71 -0
- data/lib/hashie/hash.rb +27 -8
- data/lib/hashie/logger.rb +18 -0
- data/lib/hashie/mash.rb +235 -57
- data/lib/hashie/railtie.rb +21 -0
- data/lib/hashie/rash.rb +40 -16
- data/lib/hashie/trash.rb +2 -88
- data/lib/hashie/utils.rb +44 -0
- data/lib/hashie/version.rb +1 -1
- metadata +42 -81
- data/.gitignore +0 -9
- data/.rspec +0 -2
- data/.rubocop.yml +0 -36
- data/.travis.yml +0 -15
- data/Gemfile +0 -11
- data/Guardfile +0 -5
- data/lib/hashie/hash_extensions.rb +0 -47
- data/spec/hashie/clash_spec.rb +0 -48
- data/spec/hashie/dash_spec.rb +0 -338
- data/spec/hashie/extensions/coercion_spec.rb +0 -156
- data/spec/hashie/extensions/deep_fetch_spec.rb +0 -70
- data/spec/hashie/extensions/deep_merge_spec.rb +0 -22
- data/spec/hashie/extensions/ignore_undeclared_spec.rb +0 -23
- data/spec/hashie/extensions/indifferent_access_spec.rb +0 -152
- data/spec/hashie/extensions/key_conversion_spec.rb +0 -103
- data/spec/hashie/extensions/merge_initializer_spec.rb +0 -23
- data/spec/hashie/extensions/method_access_spec.rb +0 -121
- data/spec/hashie/hash_spec.rb +0 -66
- data/spec/hashie/mash_spec.rb +0 -467
- data/spec/hashie/rash_spec.rb +0 -44
- data/spec/hashie/trash_spec.rb +0 -193
- data/spec/hashie/version_spec.rb +0 -7
- data/spec/spec.opts +0 -3
- data/spec/spec_helper.rb +0 -8
data/lib/hashie/dash.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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
|
-
|
48
|
-
|
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',
|
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
|
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.
|
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
|
-
|
121
|
+
assert_required_attributes_set!
|
96
122
|
end
|
97
123
|
|
98
|
-
|
99
|
-
|
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
|
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
|
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
|
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
|
161
|
-
self.class.required_properties.
|
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
|
-
|
174
|
-
|
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,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
|
39
|
+
def set_value_with_coercion(key, value)
|
11
40
|
into = self.class.key_coercion(key) || self.class.value_coercion(value)
|
12
41
|
|
13
|
-
|
14
|
-
|
15
|
-
value = into.
|
16
|
-
|
17
|
-
|
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
|
-
|
50
|
+
set_value_without_coercion(key, value)
|
22
51
|
end
|
23
52
|
|
24
|
-
def custom_writer(key, value,
|
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|
|
84
|
+
attrs.each { |key| key_coercions[key] = into }
|
54
85
|
end
|
55
86
|
|
56
|
-
|
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
|
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
|
-
|
131
|
+
strict_value_coercions[from] = into
|
94
132
|
else
|
95
133
|
while from.superclass && from.superclass != Object
|
96
|
-
|
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
|
-
@
|
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
|