Module: HashJoiner

Defined in:
lib/hash-joiner/version.rb,
lib/hash-joiner.rb

Overview

Author:

Defined Under Namespace

Classes: JoinError, MergeError

Constant Summary collapse

MERGEABLE_CLASSES =

The set of mergeable classes

[::Hash, ::Array]
VERSION =
"0.0.7"

Class Method Summary collapse

Class Method Details

.assert_hash_properties_are_mergeable(key, lhs_value, rhs_value) ⇒ nil

Asserts that rhs_value can be merged into lhs_value for the property identified by key.

Parameters:

  • key (String)

    Hash property name

  • lhs_value (Object)

    the property value of the data sink Hash (left-hand side value)

  • rhs_value (Object)

    the property value of the data source Hash (right-hand side value)

Returns:

  • (nil)

Raises:

  • (MergeError)

    if lhs_value exists and rhs_value is of a different class

See Also:



137
138
139
140
141
142
143
144
145
146
147
# File 'lib/hash-joiner.rb', line 137

def self.assert_hash_properties_are_mergeable(key, lhs_value, rhs_value)
  lhs_class = lhs_value == false ? ::TrueClass : lhs_value.class
  rhs_class = rhs_value == false ? ::TrueClass : rhs_value.class

  unless lhs_value.nil? or lhs_class == rhs_class
    raise MergeError.new(
      "LHS[#{key}] value (#{lhs_class}): #{lhs_value}\n" +
      "RHS[#{key}] value (#{rhs_class}): #{rhs_value}")
  end
  nil
end

.assert_is_hash_with_key(h, key, error_prefix) ⇒ nil

Asserts that h is a hash containing key. Used to ensure that a Hash can be joined with another Hash object.

Parameters:

  • h (Hash)

    object to verify

  • key (String)

    name of the property to verify

  • error_prefix (String)

    prefix for error message

Returns:

  • (nil)

Raises:

  • (JoinError)

    if h is not a Hash, or if key_field is absent from any element of lhs or rhs.

See Also:



216
217
218
219
220
221
222
# File 'lib/hash-joiner.rb', line 216

def self.assert_is_hash_with_key(h, key, error_prefix)
  if !h.instance_of? ::Hash
    raise JoinError.new("#{error_prefix} is not a Hash: #{h}")
  elsif !h.member? key
    raise JoinError.new("#{error_prefix} missing \"#{key}\": #{h}")
  end
end

.assert_objects_are_mergeable(lhs, rhs) ⇒ nil

Asserts that lhs and rhs are of the same type and can be merged.

Parameters:

  • lhs (Hash, Array)

    merged data sink (left-hand side)

  • rhs (Hash, Array)

    merged data source (right-hand side)

Returns:

  • (nil)

Raises:

  • (MergeError)

    if lhs and rhs are of the different types or are of a type that cannot be merged

See Also:



94
95
96
97
98
99
100
101
# File 'lib/hash-joiner.rb', line 94

def self.assert_objects_are_mergeable(lhs, rhs)
  if lhs.class != rhs.class
    raise MergeError.new("LHS (#{lhs.class}): #{lhs}\n" +
      "RHS (#{rhs.class}): #{rhs}")
  elsif !MERGEABLE_CLASSES.include? lhs.class
    raise MergeError.new "Class not mergeable: #{lhs.class}"
  end
end

.assign_empty_defaults(collection, array_properties, hash_properties, string_properties) ⇒ Object

Given a collection, initialize any missing properties to empty values.

Parameters:

  • collection (Hash<String>, Array<Hash<String>>)

    collection to update

  • array_properties (Array<String>)

    list of properties to initialize with an empty Array

  • hash_properties (Array<String>)

    list of properties to initialize with an empty Hash

  • string_properties (Array<String>)

    list of properties to initialize with an empty String

Returns:

  • collection



273
274
275
276
277
278
279
280
281
282
283
284
285
286
# File 'lib/hash-joiner.rb', line 273

def self.assign_empty_defaults(collection, array_properties, hash_properties,
  string_properties)
  if collection.instance_of? ::Hash
    array_properties.each {|i| collection[i] ||= Array.new}
    hash_properties.each {|i| collection[i] ||= Hash.new}
    string_properties.each {|i| collection[i] ||= String.new}
  elsif collection.instance_of? ::Array
    collection.each do |i|
      assign_empty_defaults(i,
        array_properties, hash_properties, string_properties)
    end
  end
  collection
end

.deep_merge(lhs, rhs) ⇒ Hash, Array

Performs a deep merge of Hash and Array structures. If the collections are Hashes, Hash or Array members of rhs will be deep-merged with any existing members in lhs. If the collections are Arrays, the values from rhs will be appended to lhs.

Parameters:

  • lhs (Hash, Array)

    merged data sink (left-hand side)

  • rhs (Hash, Array)

    merged data source (right-hand side)

Returns:

  • (Hash, Array)

    lhs

Raises:

  • (MergeError)

    if lhs and rhs are of different classes, or if they are of classes other than Hash or Array.



113
114
115
116
117
118
119
120
121
122
# File 'lib/hash-joiner.rb', line 113

def self.deep_merge(lhs, rhs)
  assert_objects_are_mergeable lhs, rhs

  if rhs.instance_of? ::Hash
    deep_merge_hashes lhs, rhs
  elsif rhs.instance_of? ::Array
    lhs.concat rhs
  end
  lhs
end

.deep_merge_hashes(lhs, rhs) ⇒ Hash

Performs a deep merge of Hash structures. Used to implement deep_merge.

Parameters:

  • lhs (Hash)

    merged data sink (left-hand side)

  • rhs (Hash)

    merged data source (right-hand side)

Returns:

  • (Hash)

    lhs

Raises:

  • (MergeError)

    if any value of rhs cannot be merged into lhs

See Also:



156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/hash-joiner.rb', line 156

def self.deep_merge_hashes(lhs, rhs)
  rhs.each do |key,rhs_value|
    lhs_value = lhs[key]
    assert_hash_properties_are_mergeable key, lhs_value, rhs_value

    if MERGEABLE_CLASSES.include? lhs_value.class
      deep_merge lhs_value, rhs_value
    else
      lhs[key] = rhs_value
    end
  end
end

.join_array_data(key_field, lhs, rhs) ⇒ Array<Hash>

Joins data in lhs with data from rhs based on key_field. Both lhs and rhs should be of type Array<Hash>. Performs a deep_merge on matching objects; assigns values from rhs to lhs if no corresponding value yet exists in lhs.

Parameters:

  • key_field (String)

    primary key for joined objects

  • lhs (Array<Hash>)

    joined data sink (left-hand side)

  • rhs (Array<Hash>)

    joined data source (right-hand side)

Returns:

  • (Array<Hash>)

    lhs

Raises:

  • (JoinError)

    if either lhs or rhs is not an Array<Hash>, or if key_field is absent from any element of lhs or rhs

See Also:



238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
# File 'lib/hash-joiner.rb', line 238

def self.join_array_data(key_field, lhs, rhs)
  unless lhs.instance_of? ::Array and rhs.instance_of? ::Array
    raise JoinError.new("Both lhs (#{lhs.class}) and " +
      "rhs (#{rhs.class}) must be an Array of Hash")
  end

  lhs_index = {}
  lhs.each do |i|
    self.assert_is_hash_with_key(i, key_field, "LHS element")
    lhs_index[i[key_field]] = i
  end

  # TODO(mbland): Make exception-safe by splitting into two loops: one for
  # the assert; one to modify lhs after all the assertions have succeeded.
  rhs.each do |i|
    self.assert_is_hash_with_key(i, key_field, "RHS element")
    key = i[key_field]
    if lhs_index.member? key
      deep_merge lhs_index[key], i
    else
      lhs << i
    end
  end
  lhs
end

.join_data(category, key_field, lhs, rhs) ⇒ Hash+

Joins objects in lhs[category] with data from rhs[category]. If the category objects are of type Array<Hash>, key_field will be used as the primary key to join the objects in the two collections; otherwise key_field is ignored.

Parameters:

  • category (String)

    determines member of lhs to join with rhs

  • key_field (String)

    primary key for objects in each Array<Hash> collection specified by category

  • lhs (Hash, Array<Hash>)

    joined data sink of type Hash (left-hand side)

  • rhs (Hash, Array<Hash>)

    joined data source of type Hash (right-hand side)

Returns:

  • (Hash, Array<Hash>)

    lhs

Raises:

See Also:



190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/hash-joiner.rb', line 190

def self.join_data(category, key_field, lhs, rhs)
  rhs_data = rhs[category]
  return lhs unless rhs_data

  lhs_data = lhs[category]
  if !(lhs_data and [::Hash, ::Array].include? lhs_data.class)
    lhs[category] = rhs_data
  elsif lhs_data.instance_of? ::Hash
    self.deep_merge lhs_data, rhs_data
  else
    self.join_array_data key_field, lhs_data, rhs_data
  end
  lhs
end

.promote_array_data(collection, key) ⇒ Array

Recursively promotes data within an Array. Used to implement promote_data.

Parameters:

  • collection (Array)

    collection in which to promote information

  • key (String)

    property to be promoted within collection

Returns:

  • (Array)

    collection after promotion

See Also:



63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/hash-joiner.rb', line 63

def self.promote_array_data(collection, key)
  collection.each do |i|
    # If the Array entry is a hash that contains only the target key,
    # then that key should map to an Array to be promoted.
    if i.instance_of? ::Hash and i.keys == [key]
      data_to_promote = i[key]
      i.delete key
      deep_merge collection, data_to_promote
    else
      promote_data i, key
    end
  end
  collection.delete_if {|i| i.empty?}
end

.promote_data(collection, key) ⇒ Hash, ...

Recursively promotes data within the collection matching key to the same level as key itself. After promotion, each key reference will be deleted.

Parameters:

  • collection (Hash, Array<Hash>)

    collection in which to promote information

  • key (String)

    property to be promoted within collection

Returns:

  • (Hash, Array<Hash>)

    collection if collection is a Hash or Array<Hash>

  • (nil)

    if collection is not a Hash or Array<Hash>



34
35
36
37
38
39
40
# File 'lib/hash-joiner.rb', line 34

def self.promote_data(collection, key)
  if collection.instance_of? ::Hash
    promote_hash_data collection, key
  elsif collection.instance_of? ::Array
    promote_array_data collection, key
  end
end

.promote_hash_data(collection, key) ⇒ Hash

Recursively promotes data within a Hash. Used to implement promote_data.

Parameters:

  • collection (Hash)

    collection in which to promote information

  • key (String)

    property to be promoted within collection

Returns:

  • (Hash)

    collection after promotion

See Also:



48
49
50
51
52
53
54
55
# File 'lib/hash-joiner.rb', line 48

def self.promote_hash_data(collection, key)
  if collection.member? key
    data_to_promote = collection[key]
    collection.delete key
    deep_merge collection, data_to_promote
  end
  collection.each_value {|i| promote_data i, key}
end

.prune_empty_properties(collection) ⇒ Object

Recursively prunes all empty properties from every element of a collection.

Parameters:

  • collection (Hash<String>, Array<Hash<String>>)

    collection to update



291
292
293
# File 'lib/hash-joiner.rb', line 291

def self.prune_empty_properties(collection)
  prune_empty_properties_helper(collection, {})
end

.remove_data(collection, key) ⇒ Hash, ...

Recursively strips information from collection matching key.

Parameters:

  • collection (Hash, Array<Hash>)

    collection from which to strip information

  • key (String)

    property to be stripped from collection

Returns:

  • (Hash, Array<Hash>)

    collection if collection is a Hash or Array<Hash>

  • (nil)

    if collection is not a Hash or Array<Hash>



14
15
16
17
18
19
20
21
22
# File 'lib/hash-joiner.rb', line 14

def self.remove_data(collection, key)
  if collection.instance_of? ::Hash
    collection.delete key
    collection.each_value {|i| remove_data i, key}
  elsif collection.instance_of? ::Array
    collection.each {|i| remove_data i, key}
    collection.delete_if {|i| i.empty?}
  end
end