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