hashie 3.2.0 → 3.3.1
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 +4 -4
- data/.rubocop.yml +13 -15
- data/.travis.yml +2 -2
- data/CHANGELOG.md +18 -0
- data/Gemfile +12 -4
- data/Guardfile +4 -4
- data/README.md +161 -2
- data/RELEASING.md +83 -0
- data/UPGRADING.md +16 -0
- data/lib/hashie.rb +7 -2
- data/lib/hashie/extensions/coercion.rb +58 -12
- data/lib/hashie/extensions/deep_find.rb +59 -0
- data/lib/hashie/extensions/indifferent_access.rb +2 -2
- data/lib/hashie/extensions/mash/safe_assignment.rb +13 -0
- data/lib/hashie/extensions/method_access.rb +75 -0
- data/lib/hashie/extensions/parsers/yaml_erb_parser.rb +21 -0
- data/lib/hashie/mash.rb +25 -1
- data/lib/hashie/rash.rb +26 -0
- data/lib/hashie/trash.rb +35 -15
- data/lib/hashie/version.rb +1 -1
- data/spec/hashie/extensions/coercion_spec.rb +286 -2
- data/spec/hashie/extensions/dash/indifferent_access_spec.rb +1 -1
- data/spec/hashie/extensions/deep_find_spec.rb +45 -0
- data/spec/hashie/extensions/indifferent_access_spec.rb +48 -0
- data/spec/hashie/extensions/mash/safe_assignment_spec.rb +17 -0
- data/spec/hashie/extensions/method_access_spec.rb +55 -0
- data/spec/hashie/mash_spec.rb +92 -0
- data/spec/hashie/rash_spec.rb +27 -0
- data/spec/hashie/trash_spec.rb +64 -5
- data/spec/spec_helper.rb +1 -0
- metadata +10 -2
@@ -1,35 +1,75 @@
|
|
1
1
|
module Hashie
|
2
|
+
class CoercionError < StandardError; end
|
3
|
+
|
2
4
|
module Extensions
|
3
5
|
module Coercion
|
6
|
+
CORE_TYPES = {
|
7
|
+
Integer => :to_i,
|
8
|
+
Float => :to_f,
|
9
|
+
Complex => :to_c,
|
10
|
+
Rational => :to_r,
|
11
|
+
String => :to_s,
|
12
|
+
Symbol => :to_sym
|
13
|
+
}
|
14
|
+
|
15
|
+
ABSTRACT_CORE_TYPES = {
|
16
|
+
Integer => [Fixnum, Bignum],
|
17
|
+
Numeric => [Fixnum, Bignum, Float, Complex, Rational]
|
18
|
+
}
|
19
|
+
|
4
20
|
def self.included(base)
|
5
21
|
base.send :include, InstanceMethods
|
6
22
|
base.extend ClassMethods # NOTE: we wanna make sure we first define set_value_with_coercion before extending
|
7
23
|
|
8
|
-
base.send :alias_method, :
|
9
|
-
base.send :alias_method, :[]=, :
|
24
|
+
base.send :alias_method, :set_value_without_coercion, :[]= unless base.method_defined?(:set_value_without_coercion)
|
25
|
+
base.send :alias_method, :[]=, :set_value_with_coercion
|
10
26
|
end
|
11
27
|
|
12
28
|
module InstanceMethods
|
13
29
|
def set_value_with_coercion(key, value)
|
14
30
|
into = self.class.key_coercion(key) || self.class.value_coercion(value)
|
15
31
|
|
16
|
-
return set_value_without_coercion(key, value)
|
17
|
-
return set_value_without_coercion(key, coerce_or_init(into).call(value)) unless into.is_a?(Enumerable)
|
32
|
+
return set_value_without_coercion(key, value) if value.nil? || into.nil?
|
18
33
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
34
|
+
begin
|
35
|
+
return set_value_without_coercion(key, coerce_or_init(into).call(value)) unless into.is_a?(Enumerable)
|
36
|
+
|
37
|
+
if into.class >= Hash
|
38
|
+
key_coerce = coerce_or_init(into.flatten[0])
|
39
|
+
value_coerce = coerce_or_init(into.flatten[-1])
|
40
|
+
value = Hash[value.map { |k, v| [key_coerce.call(k), value_coerce.call(v)] }]
|
41
|
+
else # Enumerable but not Hash: Array, Set
|
42
|
+
value_coerce = coerce_or_init(into.first)
|
43
|
+
value = into.class.new(value.map { |v| value_coerce.call(v) })
|
44
|
+
end
|
45
|
+
rescue NoMethodError, TypeError => e
|
46
|
+
raise CoercionError, "Cannot coerce property #{key.inspect} from #{value.class} to #{into}: #{e.message}"
|
26
47
|
end
|
27
48
|
|
28
49
|
set_value_without_coercion(key, value)
|
29
50
|
end
|
30
51
|
|
31
52
|
def coerce_or_init(type)
|
32
|
-
|
53
|
+
return type if type.is_a? Proc
|
54
|
+
|
55
|
+
if CORE_TYPES.key?(type)
|
56
|
+
lambda do |v|
|
57
|
+
return v if v.is_a? type
|
58
|
+
return v.send(CORE_TYPES[type])
|
59
|
+
end
|
60
|
+
elsif type.respond_to?(:coerce)
|
61
|
+
lambda do |v|
|
62
|
+
return v if v.is_a? type
|
63
|
+
type.coerce(v)
|
64
|
+
end
|
65
|
+
elsif type.respond_to?(:new)
|
66
|
+
lambda do |v|
|
67
|
+
return v if v.is_a? type
|
68
|
+
type.new(v)
|
69
|
+
end
|
70
|
+
else
|
71
|
+
fail TypeError, "#{type} is not a coercable type"
|
72
|
+
end
|
33
73
|
end
|
34
74
|
|
35
75
|
private :coerce_or_init
|
@@ -102,6 +142,12 @@ module Hashie
|
|
102
142
|
def coerce_value(from, into, options = {})
|
103
143
|
options = { strict: true }.merge(options)
|
104
144
|
|
145
|
+
if ABSTRACT_CORE_TYPES.key? from
|
146
|
+
ABSTRACT_CORE_TYPES[from].each do | type |
|
147
|
+
coerce_value type, into, options
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
105
151
|
if options[:strict]
|
106
152
|
(@strict_value_coercions ||= {})[from] = into
|
107
153
|
else
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Hashie
|
2
|
+
module Extensions
|
3
|
+
module DeepFind
|
4
|
+
# Performs a depth-first search on deeply nested data structures for
|
5
|
+
# a key and returns the first occurrence of the key.
|
6
|
+
#
|
7
|
+
# options = {user: {location: {address: '123 Street'}}}
|
8
|
+
# options.deep_find(:address) # => '123 Street'
|
9
|
+
def deep_find(key)
|
10
|
+
_deep_find(key)
|
11
|
+
end
|
12
|
+
|
13
|
+
alias_method :deep_detect, :deep_find
|
14
|
+
|
15
|
+
# Performs a depth-first search on deeply nested data structures for
|
16
|
+
# a key and returns all occurrences of the key.
|
17
|
+
#
|
18
|
+
# options = {users: [{location: {address: '123 Street'}}, {location: {address: '234 Street'}}]}
|
19
|
+
# options.deep_find_all(:address) # => ['123 Street', '234 Street']
|
20
|
+
def deep_find_all(key)
|
21
|
+
matches = _deep_find_all(key)
|
22
|
+
matches.empty? ? nil : matches
|
23
|
+
end
|
24
|
+
|
25
|
+
alias_method :deep_select, :deep_find_all
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def _deep_find(key, object = self)
|
30
|
+
if object.respond_to?(:key?)
|
31
|
+
return object[key] if object.key?(key)
|
32
|
+
|
33
|
+
reduce_to_match(key, object.values)
|
34
|
+
elsif object.is_a?(Enumerable)
|
35
|
+
reduce_to_match(key, object)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def _deep_find_all(key, object = self, matches = [])
|
40
|
+
if object.respond_to?(:key?)
|
41
|
+
matches << object[key] if object.key?(key)
|
42
|
+
object.values.each { |v| _deep_find_all(key, v, matches) }
|
43
|
+
elsif object.is_a?(Enumerable)
|
44
|
+
object.each { |v| _deep_find_all(key, v, matches) }
|
45
|
+
end
|
46
|
+
|
47
|
+
matches
|
48
|
+
end
|
49
|
+
|
50
|
+
def reduce_to_match(key, enumerable)
|
51
|
+
enumerable.reduce(nil) do |found, value|
|
52
|
+
return found if found
|
53
|
+
|
54
|
+
_deep_find(key, value)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -83,9 +83,9 @@ module Hashie
|
|
83
83
|
|
84
84
|
def convert_value(value)
|
85
85
|
if hash_lacking_indifference?(value)
|
86
|
-
IndifferentAccess.inject(value
|
86
|
+
IndifferentAccess.inject!(value)
|
87
87
|
elsif value.is_a?(::Array)
|
88
|
-
value.
|
88
|
+
value.replace(value.map { |e| convert_value(e) })
|
89
89
|
else
|
90
90
|
value
|
91
91
|
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Hashie
|
2
|
+
module Extensions
|
3
|
+
module Mash
|
4
|
+
module SafeAssignment
|
5
|
+
def assign_property(name, value)
|
6
|
+
fail ArgumentError, "The property #{name} clashes with an existing method." if methods.include?(name.to_sym)
|
7
|
+
|
8
|
+
self[name] = value
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -120,5 +120,80 @@ module Hashie
|
|
120
120
|
end
|
121
121
|
end
|
122
122
|
end
|
123
|
+
|
124
|
+
# MethodOverridingWriter gives you #key_name= shortcuts for
|
125
|
+
# writing to your hash. It allows methods to be overridden by
|
126
|
+
# #key_name= shortcuts and aliases those methods with two
|
127
|
+
# leading underscores.
|
128
|
+
#
|
129
|
+
# Keys are written as strings. Override #convert_key if you
|
130
|
+
# would like to have symbols or something else.
|
131
|
+
#
|
132
|
+
# Note that MethodOverridingWriter also overrides
|
133
|
+
# #respond_to_missing? such that any #method_name= will respond
|
134
|
+
# appropriately as true.
|
135
|
+
#
|
136
|
+
# @example
|
137
|
+
# class MyHash < Hash
|
138
|
+
# include Hashie::Extensions::MethodOverridingWriter
|
139
|
+
# end
|
140
|
+
#
|
141
|
+
# h = MyHash.new
|
142
|
+
# h.awesome = 'sauce'
|
143
|
+
# h['awesome'] # => 'sauce'
|
144
|
+
# h.zip = 'a-dee-doo-dah'
|
145
|
+
# h.zip # => 'a-dee-doo-dah'
|
146
|
+
# h.__zip # => [[['awesome', 'sauce'], ['zip', 'a-dee-doo-dah']]]
|
147
|
+
#
|
148
|
+
module MethodOverridingWriter
|
149
|
+
def convert_key(key)
|
150
|
+
key.to_s
|
151
|
+
end
|
152
|
+
|
153
|
+
def method_missing(name, *args)
|
154
|
+
if args.size == 1 && name.to_s =~ /(.*)=$/
|
155
|
+
key = Regexp.last_match[1]
|
156
|
+
redefine_method(key) if method?(key) && !already_overridden?(key)
|
157
|
+
return self[convert_key(key)] = args.first
|
158
|
+
end
|
159
|
+
|
160
|
+
super
|
161
|
+
end
|
162
|
+
|
163
|
+
def respond_to_missing?(name, include_private = false)
|
164
|
+
return true if name.to_s.end_with?('=')
|
165
|
+
super
|
166
|
+
end
|
167
|
+
|
168
|
+
protected
|
169
|
+
|
170
|
+
def already_overridden?(name)
|
171
|
+
method?("__#{name}")
|
172
|
+
end
|
173
|
+
|
174
|
+
def method?(name)
|
175
|
+
methods.map { |m| m.to_s }.include?(name)
|
176
|
+
end
|
177
|
+
|
178
|
+
def redefine_method(method_name)
|
179
|
+
eigenclass = class << self; self; end
|
180
|
+
eigenclass.__send__(:alias_method, "__#{method_name}", method_name)
|
181
|
+
eigenclass.__send__(:define_method, method_name, -> { self[method_name] })
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
# A macro module that will automatically include MethodReader,
|
186
|
+
# MethodOverridingWriter, and MethodQuery, giving you the ability
|
187
|
+
# to read, write, and query keys in a hash using method call
|
188
|
+
# shortcuts that can override object methods. Any overridden
|
189
|
+
# object method is automatically aliased with two leading
|
190
|
+
# underscores.
|
191
|
+
module MethodAccessWithOverride
|
192
|
+
def self.included(base)
|
193
|
+
[MethodReader, MethodOverridingWriter, MethodQuery].each do |mod|
|
194
|
+
base.send :include, mod
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
123
198
|
end
|
124
199
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'erb'
|
3
|
+
module Hashie
|
4
|
+
module Extensions
|
5
|
+
module Parsers
|
6
|
+
class YamlErbParser
|
7
|
+
def initialize(file_path)
|
8
|
+
@content = File.read(file_path)
|
9
|
+
end
|
10
|
+
|
11
|
+
def perform
|
12
|
+
YAML.load ERB.new(@content).result
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.perform(file_path)
|
16
|
+
new(file_path).perform
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/hashie/mash.rb
CHANGED
@@ -59,6 +59,25 @@ module Hashie
|
|
59
59
|
|
60
60
|
ALLOWED_SUFFIXES = %w(? ! = _)
|
61
61
|
|
62
|
+
def self.load(path, options = {})
|
63
|
+
@_mashes ||= new do |h, file_path|
|
64
|
+
fail ArgumentError, "The following file doesn't exist: #{file_path}" unless File.file?(file_path)
|
65
|
+
|
66
|
+
parser = options.fetch(:parser) { Hashie::Extensions::Parsers::YamlErbParser }
|
67
|
+
h[file_path] = new(parser.perform(file_path)).freeze
|
68
|
+
end
|
69
|
+
@_mashes[path]
|
70
|
+
end
|
71
|
+
|
72
|
+
def to_module(mash_method_name = :settings)
|
73
|
+
mash = self
|
74
|
+
Module.new do |m|
|
75
|
+
m.send :define_method, mash_method_name.to_sym do
|
76
|
+
mash
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
62
81
|
alias_method :to_s, :inspect
|
63
82
|
|
64
83
|
# If you pass in an existing hash, it will
|
@@ -171,6 +190,11 @@ module Hashie
|
|
171
190
|
alias_method :update, :deep_update
|
172
191
|
alias_method :merge!, :update
|
173
192
|
|
193
|
+
# Assigns a value to a key
|
194
|
+
def assign_property(name, value)
|
195
|
+
self[name] = value
|
196
|
+
end
|
197
|
+
|
174
198
|
# Performs a shallow_update on a duplicate of the current mash
|
175
199
|
def shallow_merge(other_hash)
|
176
200
|
dup.shallow_update(other_hash)
|
@@ -212,7 +236,7 @@ module Hashie
|
|
212
236
|
name, suffix = method_suffix(method_name)
|
213
237
|
case suffix
|
214
238
|
when '='
|
215
|
-
|
239
|
+
assign_property(name, args.first)
|
216
240
|
when '?'
|
217
241
|
!!self[name]
|
218
242
|
when '!'
|
data/lib/hashie/rash.rb
CHANGED
@@ -60,6 +60,28 @@ module Hashie
|
|
60
60
|
all(key).first
|
61
61
|
end
|
62
62
|
|
63
|
+
#
|
64
|
+
# Raise (or yield) unless something matches the key.
|
65
|
+
#
|
66
|
+
def fetch(*args)
|
67
|
+
fail ArgumentError, "Expected 1-2 arguments, got #{args.length}" \
|
68
|
+
unless (1..2).cover?(args.length)
|
69
|
+
|
70
|
+
key, default = args
|
71
|
+
|
72
|
+
all(key) do |value|
|
73
|
+
return value
|
74
|
+
end
|
75
|
+
|
76
|
+
if block_given?
|
77
|
+
yield key
|
78
|
+
elsif default
|
79
|
+
default
|
80
|
+
else
|
81
|
+
fail KeyError, "key not found: #{key.inspect}"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
63
85
|
#
|
64
86
|
# Return everything that matches the query.
|
65
87
|
#
|
@@ -106,6 +128,10 @@ module Hashie
|
|
106
128
|
@hash.send(*args, &block)
|
107
129
|
end
|
108
130
|
|
131
|
+
def respond_to_missing?(*args)
|
132
|
+
@hash.respond_to?(*args)
|
133
|
+
end
|
134
|
+
|
109
135
|
private
|
110
136
|
|
111
137
|
def optimize_if_necessary!
|
data/lib/hashie/trash.rb
CHANGED
@@ -26,11 +26,13 @@ module Hashie
|
|
26
26
|
fail ArgumentError, "Property name (#{property_name}) and :from option must not be the same"
|
27
27
|
end
|
28
28
|
|
29
|
-
|
29
|
+
translations_hash[options[:from]] ||= {}
|
30
|
+
translations_hash[options[:from]][property_name] = options[:with] || options[:transform_with]
|
30
31
|
|
31
32
|
define_method "#{options[:from]}=" do |val|
|
32
|
-
|
33
|
-
|
33
|
+
self.class.translations_hash[options[:from]].each do |name, with|
|
34
|
+
self[name] = with.respond_to?(:call) ? with.call(val) : val
|
35
|
+
end
|
34
36
|
end
|
35
37
|
else
|
36
38
|
if options[:transform_with].respond_to? :call
|
@@ -39,6 +41,18 @@ module Hashie
|
|
39
41
|
end
|
40
42
|
end
|
41
43
|
|
44
|
+
class << self
|
45
|
+
attr_reader :transforms, :translations_hash
|
46
|
+
end
|
47
|
+
instance_variable_set('@transforms', {})
|
48
|
+
instance_variable_set('@translations_hash', {})
|
49
|
+
|
50
|
+
def self.inherited(klass)
|
51
|
+
super
|
52
|
+
klass.instance_variable_set('@transforms', transforms.dup)
|
53
|
+
klass.instance_variable_set('@translations_hash', translations_hash.dup)
|
54
|
+
end
|
55
|
+
|
42
56
|
# Set a value on the Dash in a Hash-like way. Only works
|
43
57
|
# on pre-existing properties.
|
44
58
|
def []=(property, value)
|
@@ -56,7 +70,7 @@ module Hashie
|
|
56
70
|
end
|
57
71
|
|
58
72
|
def self.translation_exists?(name)
|
59
|
-
|
73
|
+
translations_hash.key? name
|
60
74
|
end
|
61
75
|
|
62
76
|
def self.transformation_exists?(name)
|
@@ -69,20 +83,26 @@ module Hashie
|
|
69
83
|
|
70
84
|
private
|
71
85
|
|
72
|
-
def self.properties
|
73
|
-
@properties ||= []
|
74
|
-
end
|
75
|
-
|
76
86
|
def self.translations
|
77
|
-
@translations ||=
|
87
|
+
@translations ||= begin
|
88
|
+
h = {}
|
89
|
+
translations_hash.each do |(property_name, property_translations)|
|
90
|
+
h[property_name] = property_translations.size > 1 ? property_translations.keys : property_translations.keys.first
|
91
|
+
end
|
92
|
+
h
|
93
|
+
end
|
78
94
|
end
|
79
95
|
|
80
96
|
def self.inverse_translations
|
81
|
-
@inverse_translations ||=
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
97
|
+
@inverse_translations ||= begin
|
98
|
+
h = {}
|
99
|
+
translations_hash.each do |(property_name, property_translations)|
|
100
|
+
property_translations.keys.each do |k|
|
101
|
+
h[k] = property_name
|
102
|
+
end
|
103
|
+
end
|
104
|
+
h
|
105
|
+
end
|
86
106
|
end
|
87
107
|
|
88
108
|
# Raises an NoMethodError if the property doesn't exist
|
@@ -98,7 +118,7 @@ module Hashie
|
|
98
118
|
def initialize_attributes(attributes)
|
99
119
|
return unless attributes
|
100
120
|
attributes_copy = attributes.dup.delete_if do |k, v|
|
101
|
-
if self.class.
|
121
|
+
if self.class.translations_hash.include?(k)
|
102
122
|
self[k] = v
|
103
123
|
true
|
104
124
|
end
|