module Ecoportal::API::Common::Content::HashDiffPatch

Constants

ID_KEYS
META_KEYS
NO_CHANGES

Public Class Methods

patch_diff(a, b) click to toggle source

The `patch data` is built as follows:

  1. detect changes that have occurred translate into one `operation` of `OP_TYPE`:

* `changed`: meaning that the object has changed (existed and has not been removed)
* `delete`:  the object has been removed
* `new`:     the object is new
  1. at the level of the target object of the model, the object is opened for change

with `id` and `operation` as follows:

```json
{
   "id": "objectID",
   "operation": "OP_TYPE",
   "data": {
      "patch_ver": "prev_patch_ver_+1",
      "property":  "value",
      "...":       "..."
   }
}
```
  1. the `data` property holds the specific changes of the object

- the `patch_ver` (compulsory) is **incremental** (for data integrity)
- the properties that have changed

@note

* there should not be difference between `null` and `""` (empty string)

@param a [Hash] current hash model @param b [Hash] previous hash model @return [Hash] a `patch data`

# File lib/ecoportal/api/common/content/hash_diff_patch.rb, line 39
def patch_diff(a, b)
  case
  when b.is_a?(Hash) && !empty?(b) && empty?(a)
    patch_delete(b)
  when a.is_a?(Hash) && !empty?(a) && empty?(b)
    patch_new(a)
  when a.is_a?(Hash) && b.is_a?(Hash)
    patch_update(a, b)
  when any_array?(a, b)
    patch_data_array(a, b)
  else
    a
  end
end

Private Class Methods

any_array?(a, b) click to toggle source
# File lib/ecoportal/api/common/content/hash_diff_patch.rb, line 169
def any_array?(a, b)
  [a, b].any? {|item| item.is_a?(Array)}
end
empty?(a) click to toggle source
# File lib/ecoportal/api/common/content/hash_diff_patch.rb, line 173
def empty?(a)
  bool   = !a
  bool ||= a.respond_to?(:empty?) && a.empty?
end
equal_values(a, b) click to toggle source
# File lib/ecoportal/api/common/content/hash_diff_patch.rb, line 56
def equal_values(a, b)
  if a.is_a?(String) || b.is_a?(String)
    return true if a.to_s.strip.empty? && b.to_s.strip.empty?
  end
  a == b
end
nested_array?(*arr) click to toggle source
# File lib/ecoportal/api/common/content/hash_diff_patch.rb, line 157
def nested_array?(*arr)
  case
  when arr.length > 1
    arr.any? {|a| nested_array?(a)}
  when arr.length == 1
    arr = arr.first
    arr.any? {|item| item.is_a?(Hash)}
  else
    false
  end
end
patch_data(a, b = nil, delete: false) click to toggle source
# File lib/ecoportal/api/common/content/hash_diff_patch.rb, line 63
def patch_data(a, b = nil, delete: false)
  {}.tap do |data_hash|
    if delete
      patch_ver = (a && a["patch_ver"]) || 1
      data_hash["patch_ver"] = patch_ver
      next
    end
    a.each do |key, a_value|
      b_value = b[key] if b_has_key = b && b.key?(key)
      is_meta_key = META_KEYS.include?(key)
      skip_equals = b_has_key && equal_values(a_value, b_value)
      next if is_meta_key || skip_equals
      data_hash[key] = patch_diff(a_value, b_value)
      data_hash.delete(key) if data_hash[key] == NO_CHANGES
    end
    if (data_hash.keys - META_KEYS).empty?
      return NO_CHANGES
    else
      patch_ver = (b && b["patch_ver"]) || 1
      data_hash["patch_ver"] = patch_ver
    end
  end
end
patch_data_array(a, b) click to toggle source
# File lib/ecoportal/api/common/content/hash_diff_patch.rb, line 116
def patch_data_array(a, b)
  original_b = b
  a ||= []; b ||= []
  if !nested_array?(a, b)
    if a.length == b.length && (a & b).length == b.length
      if original_b
        NO_CHANGES
      else
        a
      end
    else
      a
    end
  else # array with nested elements
    a_ids = array_ids(a)
    b_ids = array_ids(b)

    del_ids = b_ids - a_ids
    oth_ids = b_ids & a_ids
    new_ids = a_ids - b_ids

    arr_delete = del_ids.map do |id|
      patch_delete(array_id_item(b, id))
    end.compact

    arr_update = oth_ids.map do |id|
      patch_update(array_id_item(a, id), array_id_item(b, id))
    end.compact

    arr_new = new_ids.map do |id|
      patch_new(array_id_item(a, id))
    end.compact

    (arr_delete.concat(arr_update).concat(arr_new)).tap do |patch_array|
      # remove data with no `id`
      patch_array.reject! {|item| !item.is_a?(Hash)}
      return NO_CHANGES if patch_array.empty?
    end
  end
end
patch_delete(b) click to toggle source
# File lib/ecoportal/api/common/content/hash_diff_patch.rb, line 87
def patch_delete(b)
  return NO_CHANGES unless b.is_a?(Hash) && id = get_id(b, exception: false)
  {
    "id"        => id,
    "operation" => "deleted",
    "data"      => patch_data(b, delete: true)
  }
end
patch_new(a) click to toggle source
# File lib/ecoportal/api/common/content/hash_diff_patch.rb, line 96
def patch_new(a)
  return NO_CHANGES unless a.is_a?(Hash) && id = get_id(a, exception: false)
  {
    "id"        => id,
    "operation" => "new",
    "data"      => patch_data(a)
  }
end
patch_update(a, b) click to toggle source
# File lib/ecoportal/api/common/content/hash_diff_patch.rb, line 105
def patch_update(a, b)
  return NO_CHANGES unless a.is_a?(Hash) && id = get_id(a, exception: false)
  {
    "id"        => id,
    "operation" => "changed",
    "data"      => patch_data(a, b)
  }.tap do |update_hash|
    return nil unless update_hash["data"] != NO_CHANGES
  end
end