Module: RightSupport::Data::HashTools
- Defined in:
- lib/right_support/data/hash_tools.rb
Overview
various tools for manipulating hash-like classes.
Defined Under Namespace
Classes: DeepSortedJsonState, NoJson
Constant Summary collapse
- HAS_JSON =
require checks
require_succeeds?('json')
Class Method Summary collapse
-
.canonicalize_header_key(key) ⇒ Object
provides a canonical representation of a header key that is acceptable to Rack, etc.
-
.canonicalize_headers(headers) ⇒ Hash
duplicates the given headers hash after canonicalizing header keys.
-
.deep_apply_diff!(target, diff) ⇒ Object
Recursively apply diff portion of a patch (generated by deep_create_patch, etc.).
-
.deep_apply_patch!(target, patch) ⇒ Object
Perform 3-way merge using given target and a patch hash (generated by deep_create_patch, etc.).
-
.deep_clone(original) {|value| ... } ⇒ Hash
deprecated
Deprecated.
in favor of more robust deep_clone2
-
.deep_clone2(any, options = {}) {|value| ... } ⇒ Object
Deeply duplicates (clones) a hashable object containing other hashes or arrays of hashes but passing other types through.
-
.deep_create_patch(left, right) ⇒ Hash
Produce a difference from two hashes.
-
.deep_freeze!(any) ⇒ Object
Deeply freezes the given hash/array and all of their non-hierarchical elements in a hierarchichal data structure such that an attempt to modify any part of it will raise an exception.
-
.deep_get(hash, path) ⇒ Object
Gets a value from a (deep) hash using a path given as an array of keys.
-
.deep_mash(any) {|value| ... } ⇒ Object
Deeply mashes and duplicates (clones) a hashable using deep_clone2 and our own version of Mash.
-
.deep_merge(target, source) ⇒ Hash
Performs a deep clone and merge of one hash into another, similar in behavior to Hash#merge.
-
.deep_merge!(target, source) ⇒ Hash
Performs a deep merge (but not a deep clone) of one hash into another, similar in behavior to Hash#merge!.
-
.deep_remove!(target, source) ⇒ Hash
Remove recursively values that exist equivalently in the match hash.
-
.deep_set!(hash, path, value, clazz = nil) ⇒ TrueClass
Set a given value on a (deep) hash using a path given as an array of keys.
-
.deep_sorted_json(hash, pretty = false) ⇒ String
Generates JSON from the given hash (of hashes) that is sorted by key at all levels.
-
.hash_like?(clazz) ⇒ TrueClass|FalseClass
Determines if given class is hash-like (i.e. instance of class responds to hash methods).
-
.hashable?(object) ⇒ TrueClass|FalseClass
Determines if given object is hashable (i.e. object responds to hash methods).
-
.header_value_get(headers, key) ⇒ String
Header value by canonical key name, if present.
-
.header_value_set(headers, key, value) ⇒ Hash
safely sets a header value by first locating any existing key by canonical search.
-
.merge_headers(target, source) ⇒ Hash
merges source headers hash into the duplicated target headers canonically.
Class Method Details
.canonicalize_header_key(key) ⇒ Object
provides a canonical representation of a header key that is acceptable to Rack, etc. the web standard for canonical header keys is all lowercase except with the first character capitalized at the start and after each dash. underscores are converted to dash characters.
494 495 496 |
# File 'lib/right_support/data/hash_tools.rb', line 494 def self.canonicalize_header_key(key) key.to_s.downcase.gsub('_', '-').gsub(/(^|-)(.)/) { $1 + $2.upcase } end |
.canonicalize_headers(headers) ⇒ Hash
duplicates the given headers hash after canonicalizing header keys.
539 540 541 542 543 544 |
# File 'lib/right_support/data/hash_tools.rb', line 539 def self.canonicalize_headers(headers) headers.inject({}) do |h, (k, v)| h[canonicalize_header_key(k)] = v h end end |
.deep_apply_diff!(target, diff) ⇒ Object
Recursively apply diff portion of a patch (generated by deep_create_patch, etc.).
Parameters
Return
392 393 394 395 396 397 398 399 400 401 |
# File 'lib/right_support/data/hash_tools.rb', line 392 def self.deep_apply_diff!(target, diff) diff.each do |k, v| if v[:left] && v[:right] target[k] = v[:right] if v[:left] == target[k] elsif target.has_key?(k) deep_apply_diff!(target[k], v) end end target end |
.deep_apply_patch!(target, patch) ⇒ Object
Perform 3-way merge using given target and a patch hash (generated by deep_create_patch, etc.). values in target whose keys are in :left_only component of patch are removed values in :right_only component of patch get deep merged into target values in target whose keys are in :diff component of patch and which are identical to left side of diff get overwritten with right side of patch
Parameters
Return
377 378 379 380 381 382 |
# File 'lib/right_support/data/hash_tools.rb', line 377 def self.deep_apply_patch!(target, patch) deep_remove!(target, patch[:left_only]) deep_merge!(target, patch[:right_only]) deep_apply_diff!(target, patch[:diff]) target end |
.deep_clone(original) {|value| ... } ⇒ Hash
in favor of more robust deep_clone2
Creates a deep clone of the given hash.
note that not all objects are clonable in Ruby even though all respond to clone (which is completely counter-intuitive and contrary to all other managed languages). Java, for example, has the built-in Cloneable marker interface which we will simulate here with .duplicable? (because cloneable isn’t actually a word) in case you need non-hash values to be deep-cloned by this method.
also note that .duplicable? may imply caling .dup instead of .clone but developers tend to override .clone and totally forget to override .dup (and having two names for the same method is, yes, a bad idea in any language).
Parameters
Block
Return
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 |
# File 'lib/right_support/data/hash_tools.rb', line 140 def self.deep_clone(original, &leaf_callback) # note that .clone preserves .frozen? state so this legacy method will # attempt to modify a still-frozen hash/array and raise an exception. # see deep_clone2 which clones by creating a new instance of the object # class and produce an unfrozen clone of the hash/array. # as a side note, calling .dup does not preserve .frozen? state but is has # its own side effects, which is why we have more specific logic like # leaf_callback and .duplicable? result = original.clone result.each do |k, v| if hashable?(v) # HACK: we should have passed &leaf_callback here but never did before # so it is techincally a bug. we are not going to change it due to not # wanting to break legacy code so use deep_clone2 instead. result[k] = deep_clone(v) elsif leaf_callback result[k] = leaf_callback.call(v) elsif v.respond_to?(:duplicable?) result[k] = (v.duplicable? ? v.clone : v) else result[k] = v end end result end |
.deep_clone2(any, options = {}) {|value| ... } ⇒ Object
Deeply duplicates (clones) a hashable object containing other hashes or arrays of hashes but passing other types through.
Optionally changes the target hashable type to ‘normalize’ to a specific hashable class (such as Mash).
Optionally traverses arrays (default) in an attempt to deep-clone any sub-hashes instead of simply associating them.
Parameters
Block
Return
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 |
# File 'lib/right_support/data/hash_tools.rb', line 186 def self.deep_clone2(any, = {}, &leaf_callback) if hashable?(any) # clone to a new instance of hashable class with deep cloning. any.inject(([:class] || any.class).new) do |m, (k, v)| m[k] = deep_clone2(v, , &leaf_callback) m end elsif any.kind_of?(::Array) # traverse arrays any.map { |e| deep_clone2(e, , &leaf_callback) } elsif leaf_callback leaf_callback.call(any) elsif any.respond_to?(:duplicable?) # see #deep_clone for remarks any.duplicable? ? any.clone : any else any # whatever end end |
.deep_create_patch(left, right) ⇒ Hash
Produce a difference from two hashes. The difference excludes any values which are common to both hashes.
The result is a hash with the following keys:
- :diff = hash with key common to both input hashes and value composed of the corresponding different values: { :left => <left value>, :right => <right value> }
- :left_only = hash composed of items only found in left hash
- :right_only = hash composed of items only found in right hash
Parameters
Return
345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 |
# File 'lib/right_support/data/hash_tools.rb', line 345 def self.deep_create_patch(left, right) result = { :diff=>{}, :left_only=>{}, :right_only=>{} } right.each do |k, v| if left.include?(k) if hashable?(v) && hashable?(left[k]) subdiff = deep_create_patch(left[k], v) result[:right_only].merge!(k=>subdiff[:right_only]) unless subdiff[:right_only].empty? result[:left_only].merge!(k=>subdiff[:left_only]) unless subdiff[:left_only].empty? result[:diff].merge!(k=>subdiff[:diff]) unless subdiff[:diff].empty? elsif v != left[k] result[:diff].merge!(k=>{:left => left[k], :right=>v}) end else result[:right_only].merge!({ k => v }) end end left.each { |k, v| result[:left_only].merge!({ k => v }) unless right.include?(k) } result end |
.deep_freeze!(any) ⇒ Object
Deeply freezes the given hash/array and all of their non-hierarchical elements in a hierarchichal data structure such that an attempt to modify any part of it will raise an exception.
Parameters
Return
215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 |
# File 'lib/right_support/data/hash_tools.rb', line 215 def self.deep_freeze!(any) if hashable?(any) # clone to a new instance of hashable class with deep cloning. any.each do |k, v| # notes: # a) either key or value of a hash could be a complex type. # b) Hash freezes simple type keys upon insertion out of necessity to # avoid spontaneous rehashing. Hash will not freeze complex types # used as keys so we will (re)freeze all keys for safety. deep_freeze!(k) deep_freeze!(v) end elsif any.kind_of?(::Array) # traverse arrays any.each { |e| deep_freeze!(e) } end any.freeze end |
.deep_get(hash, path) ⇒ Object
Gets a value from a (deep) hash using a path given as an array of keys.
Parameters
Return
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
# File 'lib/right_support/data/hash_tools.rb', line 69 def self.deep_get(hash, path) hash ||= {} path ||= [] last_index = path.size - 1 path.each_with_index do |key, index| value = hash[key] if index == last_index return value elsif hashable?(value) hash = value else break end end nil end |
.deep_mash(any) {|value| ... } ⇒ Object
Deeply mashes and duplicates (clones) a hashable using deep_clone2 and our own version of Mash.
The advantage of Mash over Hash is, of course, to be able to use either a String or Symbol as a key for the same value.
Note that Mash.new(my_mash) will convert child hashes to mashes but not with the guarantee of cloning and detaching the deep mash. In other words. if any part of the hash is already a mash then it is not cloned by invoking Mash.new()
Parameters
Block
Return
254 255 256 257 |
# File 'lib/right_support/data/hash_tools.rb', line 254 def self.deep_mash(any, &leaf_callback) = { :class => RightSupport::Data::Mash } deep_clone2(any, , &leaf_callback) end |
.deep_merge(target, source) ⇒ Hash
Performs a deep clone and merge of one hash into another, similar in behavior to Hash#merge
Parameters
Return
270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 |
# File 'lib/right_support/data/hash_tools.rb', line 270 def self.deep_merge(target, source) # merge or replace any values that appear in source. result = target.class.new (source || {}).each do |k, v| if hashable?(target[k]) && hashable?(v) result[k] = deep_merge(target[k], v) else result[k] = v end end # deep clone any target-only value. target.each do |k, v| unless result.has_key?(k) result[k] = deep_clone2(v) end end result end |
.deep_merge!(target, source) ⇒ Hash
Performs a deep merge (but not a deep clone) of one hash into another, similar in behavior to Hash#merge!
Parameters
Return
299 300 301 302 303 304 305 306 307 308 |
# File 'lib/right_support/data/hash_tools.rb', line 299 def self.deep_merge!(target, source) source.each do |k, v| if hashable?(target[k]) && hashable?(v) deep_merge!(target[k], v) else target[k] = v end end if source target end |
.deep_remove!(target, source) ⇒ Hash
Remove recursively values that exist equivalently in the match hash.
Parameters
Return
318 319 320 321 322 323 324 325 326 327 328 329 |
# File 'lib/right_support/data/hash_tools.rb', line 318 def self.deep_remove!(target, source) source.each do |k, v| if target.has_key?(k) if target[k] == v target.delete(k) elsif hashable?(v) && hashable?(target[k]) deep_remove!(target[k], v) end end end if source target end |
.deep_set!(hash, path, value, clazz = nil) ⇒ TrueClass
Set a given value on a (deep) hash using a path given as an array of keys.
Parameters
Return
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
# File 'lib/right_support/data/hash_tools.rb', line 96 def self.deep_set!(hash, path, value, clazz=nil) raise ArgumentError.new("hash is invalid") unless hashable?(hash) raise ArgumentError.new("path is invalid") if path.empty? clazz ||= hash.class raise ArgumentError.new("clazz is invalid") unless hash_like?(clazz) last_index = path.size - 1 path.each_with_index do |key, index| if index == last_index hash[key] = value else subhash = hash[key] unless hashable?(subhash) subhash = clazz.new hash[key] = subhash end hash = subhash end end true end |
.deep_sorted_json(hash, pretty = false) ⇒ String
Generates JSON from the given hash (of hashes) that is sorted by key at all levels. Does not handle case of hash to array of hashes, etc.
Parameters
Return
480 481 482 483 484 485 486 487 488 |
# File 'lib/right_support/data/hash_tools.rb', line 480 def self.deep_sorted_json(hash, pretty=false) if HAS_JSON raise ArgumentError("'hash' was not hashable") unless hashable?(hash) state = ::RightSupport::Data::HashTools::DeepSortedJsonState.new(pretty) state.generate(hash) else raise NoJson, "JSON is unavailable" end end |
.hash_like?(clazz) ⇒ TrueClass|FalseClass
Determines if given class is hash-like (i.e. instance of class responds to hash methods).
Parameters
Return
57 58 59 |
# File 'lib/right_support/data/hash_tools.rb', line 57 def self.hash_like?(clazz) clazz.public_method_defined?('has_key?') end |
.hashable?(object) ⇒ TrueClass|FalseClass
Determines if given object is hashable (i.e. object responds to hash methods).
Parameters
Return
41 42 43 44 45 46 47 48 |
# File 'lib/right_support/data/hash_tools.rb', line 41 def self.hashable?(object) # note that we could obviously be more critical here, but this test has always been # sufficient and excludes Arrays and Strings which respond to [] but not has_key? # # what we specifically don't want to do is .kind_of?(Hash) because that excludes the # range of hash-like classes (such as Chef::Node::Attribute) object.respond_to?(:has_key?) end |
.header_value_get(headers, key) ⇒ String
Returns header value by canonical key name, if present.
499 500 501 502 503 504 505 506 507 508 509 510 |
# File 'lib/right_support/data/hash_tools.rb', line 499 def self.header_value_get(headers, key) return nil unless headers canonical_key = canonicalize_header_key(key) value = nil headers.each do |k, v| if canonicalize_header_key(k) == canonical_key value = v break end end value end |
.header_value_set(headers, key, value) ⇒ Hash
safely sets a header value by first locating any existing key by canonical search. if you know that the header hash is already canonical then you can alternatively set by using the canonicalized key.
521 522 523 524 525 526 527 528 529 530 531 532 |
# File 'lib/right_support/data/hash_tools.rb', line 521 def self.header_value_set(headers, key, value) headers ||= {} canonical_key = canonicalize_header_key(key) headers.each do |k, v| if canonicalize_header_key(k) == canonical_key key = k break end end headers[key] = value headers end |
.merge_headers(target, source) ⇒ Hash
merges source headers hash into the duplicated target headers canonically. also supports removal of a target header when the source value is nil.
552 553 554 555 556 557 558 559 560 561 562 563 |
# File 'lib/right_support/data/hash_tools.rb', line 552 def self.merge_headers(target, source) target = canonicalize_headers(target) (source || {}).each do |k, v| canonical_key = canonicalize_header_key(k) if v.nil? target.delete(canonical_key) else target[canonical_key] = v end end target end |