module ObjectPatch::Generator

This module handles the generation of patches between two objects.

Public Class Methods

array_compare(src_ary, tgt_ary, current_path) click to toggle source
# File lib/object_patch/generator.rb, line 64
def array_compare(src_ary, tgt_ary, current_path)
  operations = []

  if src_ary.size > tgt_ary.size
    # We'll need to remove some elements
    base_size = tgt_ary.size
    src_ary[base_size..-1].each_with_index do |itm, idx|
      path = Pointer.encode(current_path + Array(base_size + idx))
      operations.push(
        Operations::Test.new("path" => path, "value" => src_ary[base_size + idx]),
        Operations::Remove.new("path" => path)
      )
    end
  elsif src_ary.size < tgt_ary.size
    # We'll need to add some elements
    base_size = tgt_ary.size
    src_ary[base_size..-1].each_with_index do |itm, idx|
      path = Pointer.encode(current_path + Array(base_size + idx))
      operations.push(
        Operations::Add.new("path" => path, "value" => tgt_ary[base_size + idx]),
      )
    end
  end

  # Compare the existing values in the array
  smallest_length = (src_ary.size > tgt_ary.size) ? tgt_ary.size : src_ary.size
  smallest_length.times do |n|
    # Handle arrays and hashes in a special way as their values are
    # potentially not going to be able to hold up to a comparison.
    if src_ary[n].is_a?(Array) || src_ary[n].is_a?(Hash) || tgt_ary[n].is_a?(Array) || tgt_ary[n].is_a?(Hash)
      operations.push(*generate(src_ary[n], tgt_ary[n], current_path + Array(n)))
      next
    end

    # We've gotten the complicated cases out, this is a simple test and
    # replace.
    unless src_ary[n] == tgt_ary[n]
      path = Pointer.encode(current_path + Array(n))
      operations.push(
        Operations::Test.new("path" => path, "value" => src_ary[n]),
        Operations::Replace.new("path" => path, "value" => tgt_ary[n]),
      )
    end
  end

  operations
end
generate(source, target, current_path = []) click to toggle source

Generate a series of patch operations that describe the changes from the source object to the target object.

@todo This doesn’t do anything yet… @param [Object] source The source document @param [Object] target The target document we’ll generate the

differences for.

@return [Array<Hash>]

# File lib/object_patch/generator.rb, line 15
def generate(source, target, current_path = [])
  if source.class != target.class
    # The simplest of cases is that we have incompatible types, in which
    # case we'll full replace the root. This of course could be optimized
    # for situations where the root as moved to a child element but that is
    # harder to detect...
    return [Operations::Replace.new("path" => Pointer.encode(current_path), "value" => target)]
  end

  case source.class.to_s
  when "Hash"
    return hash_compare(source, target, current_path)
  when "Array"
    return array_compare(source, target, current_path)
  else
    # A scaler value
    return [] if source == target
    return [Operations::Replace.new("path" => Pointer.encode(current_path), "value" => target)]
  end
end
hash_compare(src_hash, tgt_hash, current_path) click to toggle source
# File lib/object_patch/generator.rb, line 36
def hash_compare(src_hash, tgt_hash, current_path)
  operations = []

  # Keys to remove
  (src_hash.keys - tgt_hash.keys).each do |k|
    path = Pointer.encode(current_path + Array(k))
    operations.push(
      Operations::Test.new("path" => path, "value" => src_hash[k]),
      Operations::Remove.new("path" => path)
    )
  end

  # Missing keys to add
  (tgt_hash.keys - src_hash.keys).each do |k|
    path = Pointer.encode(current_path + Array(k))
    operations.push(
      Operations::Add.new("path" => path, "value" => tgt_hash[k])
    )
  end

  # When both hashes share the same key we need to go deeper...
  (src_hash.keys & tgt_hash.keys).each do |k|
    operations += generate(src_hash[k], tgt_hash[k], current_path + Array(k))
  end

  operations
end

Private Instance Methods

array_compare(src_ary, tgt_ary, current_path) click to toggle source
# File lib/object_patch/generator.rb, line 64
def array_compare(src_ary, tgt_ary, current_path)
  operations = []

  if src_ary.size > tgt_ary.size
    # We'll need to remove some elements
    base_size = tgt_ary.size
    src_ary[base_size..-1].each_with_index do |itm, idx|
      path = Pointer.encode(current_path + Array(base_size + idx))
      operations.push(
        Operations::Test.new("path" => path, "value" => src_ary[base_size + idx]),
        Operations::Remove.new("path" => path)
      )
    end
  elsif src_ary.size < tgt_ary.size
    # We'll need to add some elements
    base_size = tgt_ary.size
    src_ary[base_size..-1].each_with_index do |itm, idx|
      path = Pointer.encode(current_path + Array(base_size + idx))
      operations.push(
        Operations::Add.new("path" => path, "value" => tgt_ary[base_size + idx]),
      )
    end
  end

  # Compare the existing values in the array
  smallest_length = (src_ary.size > tgt_ary.size) ? tgt_ary.size : src_ary.size
  smallest_length.times do |n|
    # Handle arrays and hashes in a special way as their values are
    # potentially not going to be able to hold up to a comparison.
    if src_ary[n].is_a?(Array) || src_ary[n].is_a?(Hash) || tgt_ary[n].is_a?(Array) || tgt_ary[n].is_a?(Hash)
      operations.push(*generate(src_ary[n], tgt_ary[n], current_path + Array(n)))
      next
    end

    # We've gotten the complicated cases out, this is a simple test and
    # replace.
    unless src_ary[n] == tgt_ary[n]
      path = Pointer.encode(current_path + Array(n))
      operations.push(
        Operations::Test.new("path" => path, "value" => src_ary[n]),
        Operations::Replace.new("path" => path, "value" => tgt_ary[n]),
      )
    end
  end

  operations
end
generate(source, target, current_path = []) click to toggle source

Generate a series of patch operations that describe the changes from the source object to the target object.

@todo This doesn’t do anything yet… @param [Object] source The source document @param [Object] target The target document we’ll generate the

differences for.

@return [Array<Hash>]

# File lib/object_patch/generator.rb, line 15
def generate(source, target, current_path = [])
  if source.class != target.class
    # The simplest of cases is that we have incompatible types, in which
    # case we'll full replace the root. This of course could be optimized
    # for situations where the root as moved to a child element but that is
    # harder to detect...
    return [Operations::Replace.new("path" => Pointer.encode(current_path), "value" => target)]
  end

  case source.class.to_s
  when "Hash"
    return hash_compare(source, target, current_path)
  when "Array"
    return array_compare(source, target, current_path)
  else
    # A scaler value
    return [] if source == target
    return [Operations::Replace.new("path" => Pointer.encode(current_path), "value" => target)]
  end
end
hash_compare(src_hash, tgt_hash, current_path) click to toggle source
# File lib/object_patch/generator.rb, line 36
def hash_compare(src_hash, tgt_hash, current_path)
  operations = []

  # Keys to remove
  (src_hash.keys - tgt_hash.keys).each do |k|
    path = Pointer.encode(current_path + Array(k))
    operations.push(
      Operations::Test.new("path" => path, "value" => src_hash[k]),
      Operations::Remove.new("path" => path)
    )
  end

  # Missing keys to add
  (tgt_hash.keys - src_hash.keys).each do |k|
    path = Pointer.encode(current_path + Array(k))
    operations.push(
      Operations::Add.new("path" => path, "value" => tgt_hash[k])
    )
  end

  # When both hashes share the same key we need to go deeper...
  (src_hash.keys & tgt_hash.keys).each do |k|
    operations += generate(src_hash[k], tgt_hash[k], current_path + Array(k))
  end

  operations
end