class Treet::Hash

Attributes

data[R]

Public Class Methods

digestify(data) click to toggle source
# File lib/treet/hash.rb, line 75
def self.digestify(data)
  case data
  when Hash
    Digest::SHA1.hexdigest(data.to_a.sort.flatten.join)
  else # String
    data
  end
end
new(source) click to toggle source

when loading an Array (at the top level), members are always sorted so that array comparisons will be order-independent

# File lib/treet/hash.rb, line 14
def initialize(source)
  d = case source
  when Hash
    source
  when String
    # treat as filename
    JSON.load(File.read(source))
  else
    if source.respond_to?(:to_hash)
      initialize(source.to_hash)
    else
      raise "Invalid source data type #{source.class} for Treet::Hash"
    end
  end

  @data = normalize(d)
end

Private Class Methods

diff(hash1, hash2) click to toggle source

Diffs need to be idempotent when applied via patch. Therefore we can't specify individual index positions for an array, because items can move. Instead, we must include the entire contents of the sub-hash, and during the patch process compare that against each element in the array. This means that an array cannot have exact duplicate entries.

# File lib/treet/hash.rb, line 151
def self.diff(hash1, hash2)
  diffs = []

  keys = hash1.keys | hash2.keys
  keys.each do |k|
    # if a value is missing from hash1, create a dummy of the same type that appears in hash2
    v1 = hash1[k] || hash2[k].class.new
    v2 = hash2[k] || hash1[k].class.new

    case v1
    when Hash
      (v2.keys - v1.keys).each do |k2|
        # new sub-elements: (-, key, after-value)
        diffs << ['+', "#{k}.#{k2}", v2[k2]]
      end
      (v1.keys - v2.keys).each do |k2|
        # deleted sub-elements: (-, key, before-value)
        diffs << ['-', "#{k}.#{k2}", v1[k2]]
      end
      (v1.keys & v2.keys).each do |k2|
        if v1[k2] != v2[k2]
          # altered sub-elements: (~, key, after-value, before-value-for-reference)
          diffs << ['~', "#{k}.#{k2}", v2[k2], v1[k2]]
        end
      end

    when Array
      v2 = v2.map {|e| (Hash === e) ? e.to_hash : e}
      v1.each do |e1|
        if !v2.include?(e1)
          # element has been removed
          diffs << ['-', "#{k}[]", e1]
        end
      end

      (v2 - v1).each do |e2|
        # new array element
        diffs << ['+', "#{k}[]", e2]
      end

    else # scalar values
      if v1 != v2
        if v1.nil?
          diffs << ['+', k, v2]
        elsif v2.nil?
          diffs << ['-', k, v1]
        else
          diffs << ['~', k, v2, v1]
        end
      end

    end
  end

  diffs
end
patch(hash, diffs) click to toggle source
# File lib/treet/hash.rb, line 208
def self.patch(hash, diffs)
  result = hash.dup

  diffs.each do |diff|
    flag, key, v1, _ = diff
    if key =~ /\[/
      keyname, is_array = key.match(/^(.*)(\[\])$/).captures
    elsif key =~ /\./
      keyname, subkey = key.match(/^(.*)\.(.*)$/).captures
    else
      keyname = key
    end

    case flag
    when '~'
      # change a value in place

      if subkey
        result[keyname][subkey] = v1
      else
        result[keyname] = v1
      end

    when '+'
      # add something
      if subkey
        result[keyname] ||= Map.new
        result[keyname][subkey] = v1
      elsif is_array
        result[keyname] ||= []
        result[keyname] << v1
      else
        result[keyname] = v1
      end

    when '-'
      # remove something
      if subkey
        result[keyname].delete(subkey)
      elsif is_array
        result[keyname].delete_if {|v| v == v1}
      else
        result.delete(keyname)
      end
    end
  end

  # result.delete_if {|k,v| v.nil? || v.empty?}
  # TODO: delete_if started failing when I switched to Map, can't figure out why
  result.each do |k,v|
    result.delete(k) if v.nil? || v.empty?
  end
  result
end

Public Instance Methods

==(target) click to toggle source
# File lib/treet/hash.rb, line 62
def ==(target)
  eql?(target)
end
compare(target) click to toggle source

def normalized_data

data.each_with_object({}) do |(k,v),h|
  h[k.to_s] = case v
  when Array
    v.sort_by {|x| x.hash}
  else
    v
  end
end

end def hash

data.hash
# normalized_data.hash

end

# File lib/treet/hash.rb, line 57
def compare(target)
  # HashDiff.diff(data, target.to_hash)
  Treet::Hash.diff(data.to_hash, target.to_hash)
end
eql?(target) click to toggle source
# File lib/treet/hash.rb, line 65
def eql?(target)
  self.hash.eql?(Treet::Hash.new(target).hash)
end
patch(diffs) click to toggle source

apply diffs (created via the `#compare` function) to create a new object

# File lib/treet/hash.rb, line 69
def patch(diffs)
  # newhash = Treet::Hash.patch(self.to_hash, diffs)
  newhash = Treet::Hash.patch(data, diffs)
  Treet::Hash.new(newhash)
end
to_hash() click to toggle source
# File lib/treet/hash.rb, line 38
def to_hash
  data #.to_hash
end
to_repo(root, opts = {}) click to toggle source
# File lib/treet/hash.rb, line 32
def to_repo(root, opts = {})
  construct(data, root)
  repotype = opts[:repotype] || Treet::Repo
  repotype.new(root, opts)
end

Private Instance Methods

construct(data, filename) click to toggle source
# File lib/treet/hash.rb, line 86
def construct(data, filename)
  unless filename == '.'
    # create the root of the repository tree
    Dir.mkdir(filename) rescue nil
  end

  Dir.chdir(filename) do
    data.each do |k,v|
      case v
      when Hash
        File.open(k.to_s, "w") {|f| f << JSON.pretty_generate(v)}

      when Array
        Dir.mkdir(k.to_s)
        v.each do |v2|
          case v2
          when String
            # create empty file with this name
            FileUtils.touch("#{k}/#{v2}")

          else
            # store object contents as JSON into a generated filename
            subfile = "#{k}/#{Treet::Hash.digestify(v2)}"
            File.open(subfile, "w") {|f| f << JSON.pretty_generate(v2)}
          end
        end

      when String
        File.open(k.to_s, "w") {|f| f << v}

      else
        raise "Unsupported object type #{v.class} for '#{k}'"
      end
    end
  end
end
normalize(hash) click to toggle source
# File lib/treet/hash.rb, line 123
def normalize(hash)
  Map.new(hash).sort_by(&:first).each_with_object(Map.new) do |(k,v),h|
  # Map.new(hash).each_with_object(Map.new) do |(k,v),h|
  # hash.each_with_object(Map.new) do |(k,v),h|
    case v
    when Array
      h[k] = v.uniq.sort_by(&:hash) if v.any?
      # if v.map(&:class).uniq == Map
      #   # all elements are Maps
      #   h[k] = v.sort_by(&:hash)
      #   # h[k] = v.sort do |a,b|
      #   #   a.to_a.sort_by(&:first).flatten <=> b.to_a.sort_by(&:first).flatten
      #   # end
      # else
      #   h[k] = v
      # end

    else
      h[k] = v
    end
  end
end