├── .document ├── .gitignore ├── .rspec ├── Gemfile ├── LICENSE.txt ├── README.rdoc ├── Rakefile ├── VERSION ├── easy_diff.gemspec ├── lib ├── easy_diff.rb └── easy_diff │ ├── core.rb │ ├── hash_ext.rb │ └── safe_dup.rb └── spec ├── easy_diff_spec.rb ├── spec_helper.rb └── test_data.yml /.document: -------------------------------------------------------------------------------- 1 | lib/**/*.rb 2 | bin/* 3 | - 4 | features/**/*.feature 5 | LICENSE.txt 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # rcov/simplecov generated 2 | coverage/ 3 | 4 | # rdoc generated 5 | rdoc 6 | 7 | # yard generated 8 | doc 9 | .yardoc 10 | 11 | # bundler 12 | .bundle 13 | Gemfile.lock 14 | 15 | # jeweler generated 16 | pkg 17 | 18 | # Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore: 19 | # 20 | # * Create a file at ~/.gitignore 21 | # * Include files you want ignored 22 | # * Run: git config --global core.excludesfile ~/.gitignore 23 | # 24 | # After doing this, these files will be ignored in all your git projects, 25 | # saving you from having to 'pollute' every project you touch with them 26 | # 27 | # Not sure what to needs to be ignored for particular editors/OSes? Here's some ideas to get you started. (Remember, remove the leading # of the line) 28 | # 29 | # For MacOS: 30 | # 31 | #.DS_Store 32 | # 33 | # For TextMate 34 | #*.tmproj 35 | #tmtags 36 | # 37 | # For emacs: 38 | #*~ 39 | #\#* 40 | #.\#* 41 | # 42 | # For vim: 43 | #*.swp 44 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | # Add dependencies required to use your gem here. 3 | # Example: 4 | # gem "activesupport", ">= 2.3.5" 5 | 6 | # Add dependencies to develop your gem here. 7 | # Include everything needed to run rake, tests, features, etc. 8 | group :development do 9 | gem "rspec", "~> 2.4.0" 10 | gem "yard", "~> 0.6.0" 11 | gem "jeweler" 12 | gem "simplecov", ">= 0", require: false 13 | end 14 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Abner Qian 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = easy_diff 2 | 3 | Easy Diff enhances the functionality of Hash, allowing recursive diff, merge, and unmerge of arbitrarily constructed hashes. 4 | This is perfect for people who need to do diffs on not only plain text files but also data as Hash or JSON objects. 5 | Unmerge is included with diff and merge to more easily allow versioning of arbitrary data. 6 | 7 | There are a few caveats when using this gem: 8 | * Unmerge should be used before merge to properly transform one hash into another. It will still work in most cases if you don't, but some edge cases will break. 9 | * The order of elements within an array may be different when transforming one hash into another using unmerge and merge. There is currently no way to guarantee the order is untouched. 10 | 11 | == Quick Start Example 12 | 13 | old_data = { 14 | :id => 1, 15 | :name => "Foo", 16 | :tags => [ 17 | "pretend", 18 | "i", 19 | "am", 20 | "good", 21 | "at", 22 | "examples" 23 | ], 24 | :config => { 25 | :awesome => true, 26 | :go_fast => false, 27 | :actually_work => false 28 | } 29 | } 30 | 31 | new_data = { 32 | :id => 1, 33 | :name => "Bar", 34 | :description => "An awesome thingy I made.", 35 | :tags => [ 36 | "i", 37 | "am", 38 | "really", 39 | "good" 40 | ], 41 | :config => { 42 | :awesome => true, 43 | :go_fast => true, 44 | :actually_work => true 45 | } 46 | } 47 | 48 | # Diff the two hashes to get what was removed and what was added. 49 | removed, added = old_data.easy_diff(new_data) 50 | 51 | # removed will equal 52 | # { 53 | # :name => "Foo", 54 | # :tags => [ 55 | # "pretend", 56 | # "at", 57 | # "examples" 58 | # ], 59 | # :config => { 60 | # :go_fast => false, 61 | # :actually_work => false 62 | # } 63 | # } 64 | # 65 | # 66 | # added will equal 67 | # { 68 | # :name => "Bar", 69 | # :description => "An awesome thingy I made.", 70 | # :tags => [ 71 | # "really" 72 | # ], 73 | # :config => { 74 | # :go_fast => true, 75 | # :actually_work => true 76 | # } 77 | # } 78 | 79 | # Now that we have the removed and added hashes, we can transform the old data into the new data or vice versa 80 | transformed_old_data = old_data.easy_unmerge(removed).easy_merge(added) 81 | transformed_new_data = new_data.easy_unmerge(added).easy_merge(removed) 82 | 83 | # Let's see if there are any diffs between the transformed hashes and their respective counterparts 84 | 85 | transformed_old_data.easy_diff(new_data) 86 | # => [{}, {}] # Two empty hashes, indicating no diffs. 87 | 88 | transformed_new_data.easy_diff(old_data) 89 | # => [{}, {}] # Two empty hashes, indicating no diffs. 90 | 91 | == Install 92 | 93 | gem install easy_diff 94 | 95 | == Methods 96 | 97 | === Hash#easy_diff 98 | 99 | Takes another hash and recursively determines the differences between self and the other hash. This method returns two hashes. 100 | The first contains what has to be removed from self to create the second hash. The second contains what has to be added. 101 | When diffing arrays, the order of the arrays is ignored and only the contents are compared. 102 | 103 | original = { 104 | :tags => ['a', 'b', 'c'], 105 | :pos => {:x => '1', :y => '2'}, 106 | :some_str => "bla", 107 | :some_int => 1, 108 | :some_bool => false, 109 | :extra_removed => "bye" 110 | } 111 | 112 | modified = { 113 | :tags => ['b', 'c', 'd'], 114 | :pos => {:x => '3', :y => '2'}, 115 | :some_str => "bla", 116 | :some_int => 2, 117 | :some_bool => true, 118 | :extra_added => "hi" 119 | } 120 | 121 | removed, added = original.easy_diff modified 122 | 123 | # The removed and added hashes should contain the following: 124 | # removed = { 125 | # :tags => ['a'], 126 | # :pos => {:x => '1'}, 127 | # :some_int => 1, 128 | # :some_bool => false, 129 | # :extra_removed => "bye" 130 | # } 131 | # 132 | # added = { 133 | # :tags => ['d'], 134 | # :pos => {:x => '3'}, 135 | # :some_int => 2, 136 | # :some_bool => true, 137 | # :extra_added => "hi" 138 | # } 139 | 140 | === Hash#easy_merge 141 | 142 | Takes a hash and recursively merges it with self. Arrays are merged by concatenation. 143 | 144 | Using Hash#easy_merge! will modify self instead of returning a new hash. 145 | 146 | original = { 147 | :tags => ['a', 'b', 'c'], 148 | :pos => {:x => '1', :y => '2'}, 149 | :some_str => "bla", 150 | :some_int => 1, 151 | :some_bool => false, 152 | :extra_removed => "bye" 153 | } 154 | 155 | extra = { 156 | :tags => ['d'], 157 | :pos => {:x => '3'}, 158 | :some_int => 2, 159 | :some_bool => true, 160 | :extra_added => "hi" 161 | } 162 | 163 | merged = original.easy_merge extra 164 | 165 | # The merged hash should look like this: 166 | # merged = { 167 | # :tags => ['a', 'b', 'c', 'd'], 168 | # :pos => {:x => '3', :y => '2'}, 169 | # :some_str => "bla", 170 | # :some_int => 2, 171 | # :some_bool => true, 172 | # :extra_removed => "bye", 173 | # :extra_added => "hi" 174 | # } 175 | 176 | === Hash#easy_unmerge 177 | 178 | Takes a hash and recursively unmerges it with self. Unmerging means it will remove all matching values from the hash. 179 | Arrays will be unmerged by removing matching values in a non-greedy manner (i.e. [1, 1, 1] unmerged with [1] is [1, 1] instead of []). 180 | 181 | Using Hash#easy_unmerge! will modify self instead of returning a new hash. 182 | 183 | original = { 184 | :tags => ['b', 'c', 'd'], 185 | :pos => {:x => '3', :y => '2'}, 186 | :some_str => "bla", 187 | :some_int => 2, 188 | :some_bool => true, 189 | :extra_added => "hi" 190 | } 191 | 192 | extra = { 193 | :tags => ['d'], 194 | :pos => {:x => '3'}, 195 | :some_int => 2, 196 | :some_bool => true, 197 | :extra_added => "hi" 198 | } 199 | 200 | unmerged = original.easy_unmerge extra 201 | 202 | # The unmerged hash should look like this: 203 | # unmerged = { 204 | # :tags => ['b', 'c'], 205 | # :pos => {:y => '2'}, 206 | # :some_str => "bla" 207 | # } 208 | 209 | === Hash#easy_clone 210 | 211 | Performs a deep clone on a hash 212 | 213 | original = { 214 | :tags => ['b', 'c', 'd'], 215 | :pos => {:x => '3', :y => '2'} 216 | } 217 | 218 | new = original.easy_clone 219 | 220 | new[:tags] << 'e' 221 | new[:pos][:y] = '1' 222 | 223 | # The original hash will still look like this: 224 | # original = { 225 | # :tags => ['b', 'c', 'd'], 226 | # :pos => {:x => '3', :y => '2'} 227 | # } 228 | 229 | == Contributing to easy_diff 230 | 231 | * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet 232 | * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it 233 | * Fork the project 234 | * Start a feature/bugfix branch 235 | * Commit and push until you are happy with your contribution 236 | * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally. 237 | * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it. 238 | 239 | == Copyright 240 | 241 | Copyright (c) 2011 Abner Qian. See LICENSE.txt for 242 | further details. 243 | 244 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler' 3 | begin 4 | Bundler.setup(:default, :development) 5 | rescue Bundler::BundlerError => e 6 | $stderr.puts e.message 7 | $stderr.puts "Run `bundle install` to install missing gems" 8 | exit e.status_code 9 | end 10 | require 'rake' 11 | 12 | require 'jeweler' 13 | Jeweler::Tasks.new do |gem| 14 | # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options 15 | gem.name = "easy_diff" 16 | gem.homepage = "http://github.com/Blargel/easy_diff" 17 | gem.license = "MIT" 18 | gem.summary = %Q{Recursive diff, merge, and unmerge for hashes and arrays.} 19 | gem.description = %Q{Easy Diff enhances the functionality of Hash, allowing recursive diff, merge, and unmerge of arbitrarily constructed hashes. 20 | This is perfect for people who need to do diffs on not only plain text files but also data as Hash or JSON objects. Unmerge 21 | is included with diff and merge to more easily allow versioning of arbitrary data.} 22 | gem.email = "LargeBagel@gmail.com" 23 | gem.authors = ["Abner Qian"] 24 | # Include your dependencies below. Runtime dependencies are required when using your gem, 25 | # and development dependencies are only needed for development (ie running rake tasks, tests, etc) 26 | # gem.add_runtime_dependency 'jabber4r', '> 0.1' 27 | # gem.add_development_dependency 'rspec', '> 1.2.3' 28 | end 29 | Jeweler::RubygemsDotOrgTasks.new 30 | 31 | require 'rspec/core' 32 | require 'rspec/core/rake_task' 33 | RSpec::Core::RakeTask.new(:spec) do |spec| 34 | spec.pattern = FileList['spec/**/*_spec.rb'] 35 | end 36 | 37 | RSpec::Core::RakeTask.new(:rcov) do |spec| 38 | spec.pattern = 'spec/**/*_spec.rb' 39 | spec.rcov = true 40 | end 41 | 42 | task :default => :spec 43 | 44 | require 'yard' 45 | YARD::Rake::YardocTask.new 46 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.0.0 2 | -------------------------------------------------------------------------------- /easy_diff.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec' 4 | # -*- encoding: utf-8 -*- 5 | 6 | Gem::Specification.new do |s| 7 | s.name = "easy_diff" 8 | s.version = "0.0.2" 9 | 10 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 11 | s.authors = ["Abner Qian"] 12 | s.date = "2012-02-13" 13 | s.description = "Easy Diff enhances the functionality of Hash, allowing recursive diff, merge, and unmerge of arbitrarily constructed hashes.\n This is perfect for people who need to do diffs on not only plain text files but also data as Hash or JSON objects. Unmerge\n is included with diff and merge to more easily allow versioning of arbitrary data." 14 | s.email = "LargeBagel@gmail.com" 15 | s.extra_rdoc_files = [ 16 | "LICENSE.txt", 17 | "README.rdoc" 18 | ] 19 | s.files = [ 20 | ".document", 21 | ".rspec", 22 | "Gemfile", 23 | "Gemfile.lock", 24 | "LICENSE.txt", 25 | "README.rdoc", 26 | "Rakefile", 27 | "VERSION", 28 | "easy_diff.gemspec", 29 | "lib/easy_diff.rb", 30 | "lib/easy_diff/core.rb", 31 | "lib/easy_diff/hash_ext.rb", 32 | "lib/easy_diff/safe_dup.rb", 33 | "spec/easy_diff_spec.rb", 34 | "spec/spec_helper.rb" 35 | ] 36 | s.homepage = "http://github.com/Blargel/easy_diff" 37 | s.licenses = ["MIT"] 38 | s.require_paths = ["lib"] 39 | s.rubygems_version = "1.8.12" 40 | s.summary = "Recursive diff, merge, and unmerge for hashes and arrays." 41 | s.test_files = [ 42 | "spec/easy_diff_spec.rb", 43 | "spec/spec_helper.rb" 44 | ] 45 | 46 | if s.respond_to? :specification_version then 47 | s.specification_version = 3 48 | 49 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 50 | s.add_development_dependency(%q, ["~> 2.4.0"]) 51 | s.add_development_dependency(%q, ["~> 0.6.0"]) 52 | s.add_development_dependency(%q, ["~> 1.0.0"]) 53 | s.add_development_dependency(%q, ["~> 1.5.2"]) 54 | s.add_development_dependency(%q, [">= 0"]) 55 | else 56 | s.add_dependency(%q, ["~> 2.4.0"]) 57 | s.add_dependency(%q, ["~> 0.6.0"]) 58 | s.add_dependency(%q, ["~> 1.0.0"]) 59 | s.add_dependency(%q, ["~> 1.5.2"]) 60 | s.add_dependency(%q, [">= 0"]) 61 | end 62 | else 63 | s.add_dependency(%q, ["~> 2.4.0"]) 64 | s.add_dependency(%q, ["~> 0.6.0"]) 65 | s.add_dependency(%q, ["~> 1.0.0"]) 66 | s.add_dependency(%q, ["~> 1.5.2"]) 67 | s.add_dependency(%q, [">= 0"]) 68 | end 69 | end 70 | 71 | -------------------------------------------------------------------------------- /lib/easy_diff.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/easy_diff/safe_dup') 2 | require File.expand_path(File.dirname(__FILE__) + '/easy_diff/core') 3 | require File.expand_path(File.dirname(__FILE__) + '/easy_diff/hash_ext') 4 | 5 | Object.send :include, EasyDiff::SafeDup 6 | Hash.send :include, EasyDiff::HashExt 7 | -------------------------------------------------------------------------------- /lib/easy_diff/core.rb: -------------------------------------------------------------------------------- 1 | module EasyDiff 2 | module Core 3 | def self.easy_diff(original, modified) 4 | removed = nil 5 | added = nil 6 | 7 | if original.nil? 8 | added = modified.safe_dup 9 | 10 | elsif modified.nil? 11 | removed = original.safe_dup 12 | 13 | elsif original.is_a?(Hash) && modified.is_a?(Hash) 14 | removed = {} 15 | added = {} 16 | 17 | keys_in_common = original.keys & modified.keys 18 | keys_removed = original.keys - modified.keys 19 | keys_added = modified.keys - original.keys 20 | 21 | keys_removed.each{ |key| removed[key] = original[key].safe_dup } 22 | keys_added.each{ |key| added[key] = modified[key].safe_dup } 23 | 24 | keys_in_common.each do |key| 25 | r, a = easy_diff original[key], modified[key] 26 | unless _blank?(r) && _blank?(a) 27 | removed[key] = r 28 | added[key] = a 29 | end 30 | end 31 | 32 | elsif original.is_a?(Array) && modified.is_a?(Array) 33 | removed = subtract_arrays(original, modified) 34 | added = subtract_arrays(modified, original) 35 | 36 | elsif original != modified 37 | removed = original 38 | added = modified 39 | end 40 | 41 | return removed, added 42 | end 43 | 44 | def self.easy_unmerge!(original, removed) 45 | if original.is_a?(Hash) && removed.is_a?(Hash) 46 | keys_in_common = original.keys & removed.keys 47 | keys_in_common.each do |key| 48 | if original[key] == removed[key] 49 | original.delete(key) 50 | else 51 | easy_unmerge!(original[key], removed[key]) 52 | end 53 | end 54 | elsif original.is_a?(Array) && removed.is_a?(Array) 55 | subtract_arrays!(original, removed) 56 | end 57 | original 58 | end 59 | 60 | def self.easy_merge!(original, added) 61 | if original.is_a?(Hash) && added.is_a?(Hash) 62 | added_keys = added.keys 63 | added_keys.each{ |key| original[key] = easy_merge!(original[key], added[key])} 64 | elsif original.is_a?(Array) && added.is_a?(Array) 65 | original += added 66 | else 67 | original = added.safe_dup 68 | end 69 | original 70 | end 71 | 72 | def self.easy_clone(original) 73 | Marshal::load(Marshal.dump(original)) 74 | end 75 | 76 | # Can't use regular empty? because that affects strings. 77 | def self._blank?(obj) 78 | if obj.is_a?(Hash) || obj.is_a?(Array) 79 | obj.empty? 80 | else 81 | obj.nil? 82 | end 83 | end 84 | 85 | # Regular array difference does not handle duplicate values in the way that is needed for this library. 86 | # Examples: 87 | # subtract_arrays([1, 1, 2, 3], [1, 2]) => [1, 3] 88 | # subtract_arrays([3, 3, 3, 4], [3, 4, 5]) => [3, 3] 89 | # Shamelessly stolen from http://stackoverflow.com/questions/3852755/ruby-array-subtraction-without-removing-items-more-than-once 90 | def self.subtract_arrays! arr1, arr2 91 | counts = arr2.inject(Hash.new(0)) { |h, v| h[v] += 1; h } 92 | arr1.reject! { |e| counts[e] -= 1 unless counts[e].zero? } 93 | end 94 | 95 | # Non-destructive version of above method. 96 | def self.subtract_arrays arr1, arr2 97 | cloned_arr1 = easy_clone(arr1) 98 | subtract_arrays!(cloned_arr1, arr2) 99 | 100 | cloned_arr1 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /lib/easy_diff/hash_ext.rb: -------------------------------------------------------------------------------- 1 | module EasyDiff 2 | module HashExt 3 | def easy_diff(other) 4 | EasyDiff::Core.easy_diff self, other 5 | end 6 | 7 | def easy_merge!(other) 8 | EasyDiff::Core.easy_merge! self, other 9 | end 10 | 11 | def easy_unmerge!(other) 12 | EasyDiff::Core.easy_unmerge! self, other 13 | end 14 | 15 | def easy_merge(other) 16 | self.easy_clone.easy_merge!(other) 17 | end 18 | 19 | def easy_unmerge(other) 20 | self.easy_clone.easy_unmerge!(other) 21 | end 22 | 23 | def easy_clone 24 | EasyDiff::Core.easy_clone self 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/easy_diff/safe_dup.rb: -------------------------------------------------------------------------------- 1 | module EasyDiff 2 | module SafeDup 3 | def safe_dup 4 | begin 5 | self.dup 6 | rescue TypeError 7 | self 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/easy_diff_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + "/spec_helper") 2 | require 'yaml' 3 | 4 | describe EasyDiff do 5 | 6 | let(:no_diffs) do 7 | [{},{}] 8 | end 9 | 10 | it "should do a deep clone" do 11 | original = { 12 | "pos" => {:x => 1, :y => 2} 13 | } 14 | 15 | cloned = original.easy_clone 16 | cloned.should == original 17 | 18 | cloned["pos"].delete(:x) 19 | cloned.should_not == original 20 | end 21 | 22 | # Tests that are dependent on the inputted data. Please edit or add 23 | # to the test_data.yml file if you have data that breaks the gem. 24 | 25 | test_data_file = File.expand_path(File.dirname(__FILE__) + "/test_data.yml") 26 | test_data = YAML.load_file test_data_file 27 | 28 | test_data.each do |test_case| 29 | describe "handling #{test_case["name"]}" do 30 | let(:original) do 31 | test_case["original"] 32 | end 33 | 34 | let(:modified) do 35 | test_case["modified"] 36 | end 37 | 38 | it "should compute easy_diff correctly" do 39 | removed, added = original.easy_diff modified 40 | 41 | removed.should == test_case["expected_removed"] 42 | added.should == test_case["expected_added"] 43 | end 44 | 45 | it "should be able to progress original hash to modified hash with no computed diffs" do 46 | removed, added = original.easy_diff modified 47 | 48 | progressed = original.easy_unmerge(removed).easy_merge(added) 49 | 50 | modified.easy_diff(progressed).should == no_diffs 51 | end 52 | 53 | it "should be able to reverse modified hash to original hash with no computed diffs" do 54 | removed, added = original.easy_diff modified 55 | 56 | reversed = modified.easy_unmerge(added).easy_merge(removed) 57 | 58 | original.easy_diff(reversed).should == no_diffs 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | SimpleCov.start 3 | 4 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 5 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 6 | require 'rspec' 7 | require 'easy_diff' 8 | 9 | # Requires supporting files with custom matchers and macros, etc, 10 | # in ./support/ and its subdirectories. 11 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f} 12 | 13 | RSpec.configure do |config| 14 | 15 | end 16 | -------------------------------------------------------------------------------- /spec/test_data.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - # Basic test cases on nested Hashes and Arrays 3 | name: "simple cases with nested Hashes and Arrays" 4 | original: 5 | tags: 6 | - "a" 7 | - "b" 8 | - "c" 9 | pos: 10 | x: "1" 11 | y: "2" 12 | some_str: "bla" 13 | some_int: 1 14 | some_bool: false 15 | extra_removed: "bye" 16 | modified: 17 | tags: 18 | - "b" 19 | - "c" 20 | - "d" 21 | pos: 22 | x: "3" 23 | y: "2" 24 | some_str: "bla" 25 | some_int: 2 26 | some_bool: true 27 | extra_added: "hi" 28 | expected_removed: 29 | tags: 30 | - "a" 31 | pos: 32 | x: "1" 33 | some_int: 1 34 | some_bool: false 35 | extra_removed: "bye" 36 | expected_added: 37 | tags: 38 | - "d" 39 | pos: 40 | x: "3" 41 | some_int: 2 42 | some_bool: true 43 | extra_added: "hi" 44 | 45 | - # Test case where Arrays have duplicate values 46 | name: "Arrays with duplicate values" 47 | original: 48 | key: 49 | - 1 50 | - 1 51 | - 1 52 | - 2 53 | - 3 54 | - 4 55 | modified: 56 | key: 57 | - 1 58 | - 3 59 | - 4 60 | - 4 61 | expected_removed: 62 | key: 63 | - 1 64 | - 1 65 | - 2 66 | expected_added: 67 | key: 68 | - 4 69 | 70 | - # Test case where nil values of Hashes are expected to be preserved 71 | name: "Hashes with nil values" 72 | original: 73 | old_nil: ~ 74 | new_nil: "foo" 75 | still_nil: ~ 76 | removed_nil: ~ 77 | modified: 78 | old_nil: "bar" 79 | new_nil: ~ 80 | still_nil: ~ 81 | added_nil: ~ 82 | expected_removed: 83 | old_nil: ~ 84 | new_nil: "foo" 85 | removed_nil: ~ 86 | expected_added: 87 | old_nil: "bar" 88 | new_nil: ~ 89 | added_nil: ~ 90 | 91 | - # Test case where an empty Hash is compared to a Hash with one nil 92 | # value in it. 93 | name: "an empty Hash being compared to a Hash with a nil value" 94 | original: {} 95 | modified: 96 | key: ~ 97 | expected_removed: {} 98 | expected_added: 99 | key: ~ 100 | 101 | - # Test case where nil values of Arrays are expected to be preserved 102 | name: "Arrays with nil values" 103 | original: 104 | key: 105 | - 5 106 | - "hi" 107 | modified: 108 | key: 109 | - 5 110 | - "hi" 111 | - ~ 112 | expected_removed: 113 | key: [] 114 | expected_added: 115 | key: 116 | - ~ 117 | 118 | - # Test case where an empty Array is compared to an Array with one 119 | # nil value in it. 120 | name: "an empty Array being compared to an Array with a nil value" 121 | original: 122 | key: [] 123 | modified: 124 | key: 125 | - ~ 126 | expected_removed: 127 | key: [] 128 | expected_added: 129 | key: 130 | - ~ 131 | 132 | - # Test case where nested Hashes and Arrays that are identical are 133 | # expected to not show up as empty Hashes and Arrays in diffs. 134 | name: "matching nested Hashes and Arrays" 135 | original: 136 | hash: 137 | foo: "bar" 138 | array: 139 | - 1 140 | - 2 141 | - 3 142 | extra: "apples" 143 | modified: 144 | hash: 145 | foo: "bar" 146 | array: 147 | - 1 148 | - 2 149 | - 3 150 | extra: "oranges" 151 | expected_removed: 152 | extra: "apples" 153 | expected_added: 154 | extra: "oranges" 155 | 156 | - # Test case where empty Hashes and Arrays that are added or removed 157 | # are expected to show up in diffs. 158 | name: "empty Hashes and Arrays" 159 | original: 160 | foo: "bar" 161 | modified: 162 | empty_hash: {} 163 | empty_array: [] 164 | foo: "bar" 165 | expected_removed: {} 166 | expected_added: 167 | empty_hash: {} 168 | empty_array: [] 169 | 170 | - 171 | name: "empty Strings" 172 | original: 173 | possibly_empty_string: "" 174 | modified: 175 | possibly_empty_string: "not empty" 176 | expected_removed: 177 | possibly_empty_string: "" 178 | expected_added: 179 | possibly_empty_string: "not empty" 180 | 181 | - # Test case where Arrays containing Hashes are expected to not 182 | # cause an error. 183 | name: "Arrays containing Hashes" 184 | original: 185 | key: 186 | - 187 | c: "1" 188 | - 189 | a: "2" 190 | - 191 | a: "1" 192 | modified: 193 | key: 194 | - 195 | c: "1" 196 | - 197 | b: "2" 198 | - 199 | a: "1" 200 | expected_removed: 201 | key: 202 | - 203 | a: "2" 204 | expected_added: 205 | key: 206 | - 207 | b: "2" 208 | 209 | - # Test case where Arrays containing Hashes with nils are expected 210 | # to not cause an error. 211 | name: "Arrays containing Hashes with nils" 212 | original: 213 | key: 214 | - 215 | a: "1" 216 | b: "2" 217 | - 218 | a: ~ 219 | c: "2" 220 | - 221 | e: "5" 222 | modified: 223 | key: 224 | - 225 | a: "1" 226 | b: "2" 227 | - 228 | a: ~ 229 | c: "2" 230 | - 231 | d: "4" 232 | expected_removed: 233 | key: 234 | - 235 | e: "5" 236 | expected_added: 237 | key: 238 | - 239 | d: "4" 240 | --------------------------------------------------------------------------------