right_support 2.2.2 → 2.3.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.
- data/lib/right_support/data/hash_tools.rb +255 -0
- data/lib/right_support/data.rb +1 -0
- data/right_support.gemspec +1 -1
- metadata +4 -3
@@ -0,0 +1,255 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2012 RightScale Inc
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
# a copy of this software and associated documentation files (the
|
6
|
+
# "Software"), to deal in the Software without restriction, including
|
7
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
# the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be
|
13
|
+
# included in all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
19
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
20
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
|
23
|
+
module RightSupport::Data
|
24
|
+
|
25
|
+
# various tools for manipulating hash-like classes.
|
26
|
+
module HashTools
|
27
|
+
|
28
|
+
# Determines if given object is hashable (i.e. object responds to hash methods).
|
29
|
+
#
|
30
|
+
# === Parameters
|
31
|
+
# @param [Object] object to be tested
|
32
|
+
#
|
33
|
+
# === Return
|
34
|
+
# @return [TrueClass|FalseClass] true if object is hashable, false otherwise
|
35
|
+
def self.hashable?(object)
|
36
|
+
# note that we could obviously be more critical here, but this test has always been
|
37
|
+
# sufficient and excludes Arrays and Strings which respond to [] but not has_key?
|
38
|
+
#
|
39
|
+
# what we specifically don't want to do is .kind_of?(Hash) because that excludes the
|
40
|
+
# range of hash-like classes (such as Chef::Node::Attribute)
|
41
|
+
object.respond_to?(:has_key?)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Determines if given class is hash-like (i.e. instance of class responds to hash methods).
|
45
|
+
#
|
46
|
+
# === Parameters
|
47
|
+
# @param [Class] clazz to be tested
|
48
|
+
#
|
49
|
+
# === Return
|
50
|
+
# @return [TrueClass|FalseClass] true if clazz is hash-like, false otherwise
|
51
|
+
def self.hash_like?(clazz)
|
52
|
+
clazz.public_instance_methods.include?('has_key?')
|
53
|
+
end
|
54
|
+
|
55
|
+
# Gets a value from a (deep) hash using a path given as an array of keys.
|
56
|
+
#
|
57
|
+
# === Parameters
|
58
|
+
# @param [Hash] hash for lookup or nil or empty
|
59
|
+
# @param [Array] path to existing value as array of keys or nil or empty
|
60
|
+
#
|
61
|
+
# === Return
|
62
|
+
# @return [Object] value or nil
|
63
|
+
def self.deep_get(hash, path)
|
64
|
+
hash ||= {}
|
65
|
+
path ||= []
|
66
|
+
last_index = path.size - 1
|
67
|
+
path.each_with_index do |key, index|
|
68
|
+
value = hash[key]
|
69
|
+
if index == last_index
|
70
|
+
return value
|
71
|
+
elsif hashable?(value)
|
72
|
+
hash = value
|
73
|
+
else
|
74
|
+
break
|
75
|
+
end
|
76
|
+
end
|
77
|
+
nil
|
78
|
+
end
|
79
|
+
|
80
|
+
# Set a given value on a (deep) hash using a path given as an array of keys.
|
81
|
+
#
|
82
|
+
# === Parameters
|
83
|
+
# @param [Hash] hash for insertion
|
84
|
+
# @param [Array] path to new value as array of keys
|
85
|
+
# @param [Object] value to insert
|
86
|
+
# @param [Class] clazz to allocate as needed when building deep hash or nil to infer from hash argument
|
87
|
+
#
|
88
|
+
# === Return
|
89
|
+
# @return [TrueClass] always true
|
90
|
+
def self.deep_set!(hash, path, value, clazz=nil)
|
91
|
+
raise ArgumentError.new("hash is invalid") unless hashable?(hash)
|
92
|
+
raise ArgumentError.new("path is invalid") if path.empty?
|
93
|
+
clazz ||= hash.class
|
94
|
+
raise ArgumentError.new("clazz is invalid") unless hash_like?(clazz)
|
95
|
+
last_index = path.size - 1
|
96
|
+
path.each_with_index do |key, index|
|
97
|
+
if index == last_index
|
98
|
+
hash[key] = value
|
99
|
+
else
|
100
|
+
subhash = hash[key]
|
101
|
+
unless hashable?(subhash)
|
102
|
+
subhash = clazz.new
|
103
|
+
hash[key] = subhash
|
104
|
+
end
|
105
|
+
hash = subhash
|
106
|
+
end
|
107
|
+
end
|
108
|
+
true
|
109
|
+
end
|
110
|
+
|
111
|
+
# Creates a deep clone of the given hash.
|
112
|
+
#
|
113
|
+
# note that not all objects are clonable in Ruby even though all respond to clone
|
114
|
+
# (which is completely counter-intuitive and contrary to all other managed languages).
|
115
|
+
# Java, for example, has the built-in Cloneable marker interface which we will simulate
|
116
|
+
# here with .duplicable? (because cloneable isn't actually a word) in case you need
|
117
|
+
# non-hash values to be deep-cloned by this method.
|
118
|
+
#
|
119
|
+
# also note that .duplicable? may imply caling .dup instead of .clone but developers tend
|
120
|
+
# to override .clone and totally forget to override .dup (and having two names for the
|
121
|
+
# same method is, yes, a bad idea in any language).
|
122
|
+
#
|
123
|
+
# === Parameters
|
124
|
+
# @param [Hash] original hash to clone
|
125
|
+
#
|
126
|
+
# === Return
|
127
|
+
# @return [Hash] deep cloned hash
|
128
|
+
def self.deep_clone(original)
|
129
|
+
result = original.clone
|
130
|
+
result.each do |k, v|
|
131
|
+
if hashable?(v)
|
132
|
+
result[k] = deep_clone(v)
|
133
|
+
elsif v.respond_to?(:duplicable?)
|
134
|
+
result[k] = (v.duplicable? ? v.clone : v)
|
135
|
+
else
|
136
|
+
result[k] = v
|
137
|
+
end
|
138
|
+
end
|
139
|
+
result
|
140
|
+
end
|
141
|
+
|
142
|
+
# Performs a deep merge (but not a deep clone) of one hash into another.
|
143
|
+
#
|
144
|
+
# === Parameters
|
145
|
+
# @param [Hash] target hash to contain original and merged data
|
146
|
+
# @param [Hash] source hash containing data to recursively assign or nil or empty
|
147
|
+
#
|
148
|
+
# === Return
|
149
|
+
# @return [Hash] to_hash result of merge
|
150
|
+
def self.deep_merge!(target, source)
|
151
|
+
source.each do |k, v|
|
152
|
+
if hashable?(target[k]) && hashable?(v)
|
153
|
+
deep_merge!(target[k], v)
|
154
|
+
else
|
155
|
+
target[k] = v
|
156
|
+
end
|
157
|
+
end if source
|
158
|
+
target
|
159
|
+
end
|
160
|
+
|
161
|
+
# Remove recursively values that exist equivalently in the match hash.
|
162
|
+
#
|
163
|
+
# === Parameters
|
164
|
+
# @param [Hash] target hash from which to remove matching values
|
165
|
+
# @param [Hash] source hash to compare against target for removal or nil or empty
|
166
|
+
#
|
167
|
+
# === Return
|
168
|
+
# @return [Hash] target hash with matching values removed, if any
|
169
|
+
def self.deep_remove!(target, source)
|
170
|
+
source.each do |k, v|
|
171
|
+
if target.has_key?(k)
|
172
|
+
if target[k] == v
|
173
|
+
target.delete(k)
|
174
|
+
elsif hashable?(v) && hashable?(target[k])
|
175
|
+
deep_remove!(target[k], v)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end if source
|
179
|
+
target
|
180
|
+
end
|
181
|
+
|
182
|
+
# Produce a difference from two hashes. The difference excludes any values which are
|
183
|
+
# common to both hashes.
|
184
|
+
#
|
185
|
+
# The result is a hash with the following keys:
|
186
|
+
# - :diff = hash with key common to both input hashes and value composed of the corresponding different values: { :left => <left value>, :right => <right value> }
|
187
|
+
# - :left_only = hash composed of items only found in left hash
|
188
|
+
# - :right_only = hash composed of items only found in right hash
|
189
|
+
#
|
190
|
+
# === Parameters
|
191
|
+
# @param [Hash] left side
|
192
|
+
# @param [Hash] right side
|
193
|
+
#
|
194
|
+
# === Return
|
195
|
+
# @return [Hash] result as hash of diffage
|
196
|
+
def self.deep_create_patch(left, right)
|
197
|
+
result = { :diff=>{}, :left_only=>{}, :right_only=>{} }
|
198
|
+
right.each do |k, v|
|
199
|
+
if left.include?(k)
|
200
|
+
if hashable?(v) && hashable?(left[k])
|
201
|
+
subdiff = deep_create_patch(left[k], v)
|
202
|
+
result[:right_only].merge!(k=>subdiff[:right_only]) unless subdiff[:right_only].empty?
|
203
|
+
result[:left_only].merge!(k=>subdiff[:left_only]) unless subdiff[:left_only].empty?
|
204
|
+
result[:diff].merge!(k=>subdiff[:diff]) unless subdiff[:diff].empty?
|
205
|
+
elsif v != left[k]
|
206
|
+
result[:diff].merge!(k=>{:left => left[k], :right=>v})
|
207
|
+
end
|
208
|
+
else
|
209
|
+
result[:right_only].merge!({ k => v })
|
210
|
+
end
|
211
|
+
end
|
212
|
+
left.each { |k, v| result[:left_only].merge!({ k => v }) unless right.include?(k) }
|
213
|
+
result
|
214
|
+
end
|
215
|
+
|
216
|
+
# Perform 3-way merge using given target and a patch hash (generated by deep_create_patch, etc.).
|
217
|
+
# values in target whose keys are in :left_only component of patch are removed
|
218
|
+
# values in :right_only component of patch get deep merged into target
|
219
|
+
# values in target whose keys are in :diff component of patch and which are identical to left
|
220
|
+
# side of diff get overwritten with right side of patch
|
221
|
+
#
|
222
|
+
# === Parameters
|
223
|
+
# @param [Hash] target hash where patch will be applied.
|
224
|
+
# @param [Hash] patch hash containing changes to apply.
|
225
|
+
#
|
226
|
+
# === Return
|
227
|
+
# @param [Hash] target hash with patch applied.
|
228
|
+
def self.deep_apply_patch!(target, patch)
|
229
|
+
deep_remove!(target, patch[:left_only])
|
230
|
+
deep_merge!(target, patch[:right_only])
|
231
|
+
deep_apply_diff!(target, patch[:diff])
|
232
|
+
target
|
233
|
+
end
|
234
|
+
|
235
|
+
# Recursively apply diff portion of a patch (generated by deep_create_patch, etc.).
|
236
|
+
#
|
237
|
+
# === Parameters
|
238
|
+
# @param [Hash] target hash where diff will be applied.
|
239
|
+
# @param [Hash] diff hash containing changes to apply.
|
240
|
+
#
|
241
|
+
# === Return
|
242
|
+
# @param [Hash] target hash with diff applied.
|
243
|
+
def self.deep_apply_diff!(target, diff)
|
244
|
+
diff.each do |k, v|
|
245
|
+
if v[:left] && v[:right]
|
246
|
+
target[k] = v[:right] if v[:left] == target[k]
|
247
|
+
elsif target.has_key?(k)
|
248
|
+
deep_apply_diff!(target[k], v)
|
249
|
+
end
|
250
|
+
end
|
251
|
+
target
|
252
|
+
end
|
253
|
+
|
254
|
+
end
|
255
|
+
end
|
data/lib/right_support/data.rb
CHANGED
data/right_support.gemspec
CHANGED
@@ -7,7 +7,7 @@ spec = Gem::Specification.new do |s|
|
|
7
7
|
s.required_ruby_version = Gem::Requirement.new(">= 1.8.7")
|
8
8
|
|
9
9
|
s.name = 'right_support'
|
10
|
-
s.version = '2.
|
10
|
+
s.version = '2.3.0'
|
11
11
|
s.date = '2012-06-27'
|
12
12
|
|
13
13
|
s.authors = ['Tony Spataro', 'Sergey Sergyenko', 'Ryan Williamson', 'Lee Kirchhoff', 'Sergey Enin']
|
metadata
CHANGED
@@ -5,9 +5,9 @@ version: !ruby/object:Gem::Version
|
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 2
|
8
|
-
-
|
9
|
-
-
|
10
|
-
version: 2.
|
8
|
+
- 3
|
9
|
+
- 0
|
10
|
+
version: 2.3.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Tony Spataro
|
@@ -42,6 +42,7 @@ files:
|
|
42
42
|
- lib/right_support/crypto.rb
|
43
43
|
- lib/right_support/crypto/signed_hash.rb
|
44
44
|
- lib/right_support/data.rb
|
45
|
+
- lib/right_support/data/hash_tools.rb
|
45
46
|
- lib/right_support/data/uuid.rb
|
46
47
|
- lib/right_support/db.rb
|
47
48
|
- lib/right_support/db/cassandra_model.rb
|