├── Gemfile ├── .gitignore ├── lib ├── deep_merge.rb └── deep_merge │ ├── rails_compat.rb │ ├── deep_merge_hash.rb │ └── core.rb ├── Rakefile ├── .github └── workflows │ └── ci.yaml ├── deep_merge.gemspec ├── LICENSE ├── CHANGELOG ├── README.md └── test └── test_deep_merge.rb /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pkg/* 2 | .rake_tasks~ 3 | Gemfile.lock 4 | .bundle 5 | -------------------------------------------------------------------------------- /lib/deep_merge.rb: -------------------------------------------------------------------------------- 1 | require 'deep_merge/core' 2 | require 'deep_merge/deep_merge_hash' 3 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake/testtask' 2 | 3 | Rake::TestTask.new do |t| 4 | t.libs << FileList['lib/**.rb'] 5 | t.test_files = FileList['test/test*.rb'] 6 | end 7 | 8 | task :default => :test 9 | 10 | begin 11 | require 'rubygems' 12 | require 'rubygems/package_task' 13 | 14 | gemspec = eval(IO.read('deep_merge.gemspec')) 15 | Gem::PackageTask.new(gemspec).define 16 | rescue LoadError 17 | #okay, then 18 | end 19 | 20 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | ci: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | ruby-version: 11 | - '2.7' 12 | - '3.0' 13 | - '3.1' 14 | - '3.2' 15 | - '3.3' 16 | continue-on-error: [false] 17 | include: 18 | - ruby-version: ruby-head 19 | continue-on-error: true 20 | - ruby-version: jruby-head 21 | continue-on-error: true 22 | name: ruby ${{ matrix.ruby-version }} rake 23 | steps: 24 | - uses: actions/checkout@v2 25 | - name: Set up Ruby 26 | uses: ruby/setup-ruby@v1 27 | with: 28 | ruby-version: ${{ matrix.ruby-version }} 29 | bundler-cache: true 30 | - name: Run tests 31 | run: bundle exec rake 32 | continue-on-error: ${{ matrix.continue-on-error }} 33 | -------------------------------------------------------------------------------- /deep_merge.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = %q{deep_merge} 3 | s.version = "1.2.2" 4 | 5 | s.authors = ["Steve Midgley"] 6 | s.date = %q{2022-01-07} 7 | s.description = %q{Recursively merge hashes.} 8 | s.email = %q{dan@kallistec.com} 9 | s.license = 'MIT' 10 | s.extra_rdoc_files = [ 11 | "LICENSE", 12 | "README.md" 13 | ] 14 | s.files = [ 15 | "CHANGELOG", 16 | "README.md", 17 | "Rakefile", 18 | "lib/deep_merge.rb", 19 | "lib/deep_merge/core.rb", 20 | "lib/deep_merge/deep_merge_hash.rb", 21 | "lib/deep_merge/rails_compat.rb", 22 | "test/test_deep_merge.rb" 23 | ] 24 | s.homepage = %q{https://github.com/danielsdeleo/deep_merge} 25 | s.require_paths = ["lib"] 26 | s.summary = %q{Merge Deeply Nested Hashes} 27 | s.test_files = [ 28 | "test/test_deep_merge.rb" 29 | ] 30 | s.required_ruby_version = '>= 2.7.0' 31 | 32 | s.add_development_dependency "rake" 33 | s.add_development_dependency "test-unit-minitest" 34 | end 35 | -------------------------------------------------------------------------------- /lib/deep_merge/rails_compat.rb: -------------------------------------------------------------------------------- 1 | require 'deep_merge/core' 2 | 3 | module DeepMerge 4 | module RailsCompat 5 | # ko_deeper_merge! will merge and knockout elements prefixed with DEFAULT_FIELD_KNOCKOUT_PREFIX 6 | def ko_deeper_merge!(source, options = {}) 7 | default_opts = {:knockout_prefix => "--", :preserve_unmergeables => false} 8 | DeepMerge::deep_merge!(source, self, default_opts.merge(options)) 9 | end 10 | 11 | # deeper_merge! will merge and overwrite any unmergeables in destination hash 12 | def deeper_merge!(source, options = {}) 13 | default_opts = {:preserve_unmergeables => false} 14 | DeepMerge::deep_merge!(source, self, default_opts.merge(options)) 15 | end 16 | 17 | # deeper_merge will merge and skip any unmergeables in destination hash 18 | def deeper_merge(source, options = {}) 19 | default_opts = {:preserve_unmergeables => true} 20 | DeepMerge::deep_merge!(source, self, default_opts.merge(options)) 21 | end 22 | end 23 | end 24 | 25 | class Hash 26 | include ::DeepMerge::RailsCompat 27 | end 28 | -------------------------------------------------------------------------------- /lib/deep_merge/deep_merge_hash.rb: -------------------------------------------------------------------------------- 1 | require 'deep_merge/core' 2 | 3 | module DeepMerge 4 | module DeepMergeHash 5 | # ko_deep_merge! will merge and knockout elements prefixed with DEFAULT_FIELD_KNOCKOUT_PREFIX 6 | def ko_deep_merge!(source, options = {}) 7 | default_opts = {:knockout_prefix => "--", :preserve_unmergeables => false} 8 | DeepMerge::deep_merge!(source, self, default_opts.merge(options)) 9 | end 10 | 11 | # deep_merge! will merge and overwrite any unmergeables in destination hash 12 | def deep_merge!(source, options = {}) 13 | default_opts = {:preserve_unmergeables => false} 14 | DeepMerge::deep_merge!(source, self, default_opts.merge(options)) 15 | end 16 | 17 | # deep_merge will merge and skip any unmergeables in destination hash 18 | def deep_merge(source, options = {}) 19 | default_opts = {:preserve_unmergeables => true} 20 | DeepMerge::deep_merge!(source, self, default_opts.merge(options)) 21 | end 22 | 23 | end # DeepMergeHashExt 24 | end 25 | 26 | class Hash 27 | include DeepMerge::DeepMergeHash 28 | end 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2008-2016 Steve Midgley, Daniel DeLeo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | * Dropped support for ruby versions before 2.7 2 | 3 | 2022-01-07 Jason Frey 4 | * Ship version 1.2.2 5 | 6 | * Switched to GitHub actions and add testing of Rubies up to Ruby 3.0 7 | * Fixed issue with keep_duplicate_arrays when merging into a nil array. Thanks ALTinners! 8 | * Added documentation for keep_duplicate_arrays 9 | 10 | 2017-11-16 Jason Frey 11 | * Ship version 1.2.1 12 | 13 | * Fixed release date in the gemspec. 14 | 15 | 2017-11-16 Jason Frey 16 | * Ship version 1.2.0 17 | 18 | 2017-04-25 Joe Rafaniello 19 | * Merge nil values or keep the original via an option 20 | 21 | 2016-08-01 Jason Frey 22 | * Ship version 1.1.1 23 | 24 | * Fixed release date in the gemspec. 25 | 26 | 2016-08-01 Jason Frey 27 | * Ship version 1.1.0 28 | 29 | * Add testing for newer Ruby 2.2, 2.3, head, and jruby-head. 30 | 31 | 2016-06-14 Michael Sievers 32 | * Add extend_existing_arrays option 33 | 34 | 2016-06-07 Jason Frey 35 | * Add overwrite_arrays option 36 | 37 | 2016-04-08 Dan Deleo 38 | * Remove support for old Ruby 1.8 and 1.9 39 | 40 | 2014-01-21 Dan DeLeo 41 | * Ship version 1.0.1 42 | 43 | * Update knockout behavior to better handle nil (b7de40b5) 44 | 45 | 2011-08-15 Dan DeLeo 46 | * Document how to use w/ Rails 3 via Bundler 47 | 48 | 2011-07-28 Dan DeLeo 49 | * Use a plain ol' gemspec and Rakefile for gem creation 50 | 51 | * Ship version 1.0.0 52 | 53 | 2011-05-23 Joe Van Dyk 54 | 55 | * Added Changelog 56 | 57 | 2011-05-18 Joe Van Dyk 58 | 59 | * Merging empty strings should work if String#blank? is defined. 60 | 61 | * Use unix line endings 62 | 63 | * Removing extra whitespace 64 | 65 | 2010-01-11 Dan DeLeo 66 | 67 | * fix boolean merging according to mdkent's patch explicitly test 68 | for nils w/ #nil? instead of negating. Thanks mdkent! 69 | 70 | 2009-12-25 Dan DeLeo 71 | 72 | * miscellaneous cleanup 73 | 74 | * make rails/active_support compat optional 75 | 76 | * add jeweler rake task for gemability 77 | 78 | 2009-12-24 Dan DeLeo 79 | 80 | * VERSION: Version bump to 0.0.1 81 | 82 | * VERSION: Version bump to 0.0.0 83 | 84 | 2009-11-06 Jonathan Weiss 85 | 86 | * import 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DeepMerge 2 | 3 | [![Gem Version](https://badge.fury.io/rb/deep_merge.svg)](http://badge.fury.io/rb/deep_merge) 4 | [![CI](https://github.com/danielsdeleo/deep_merge/actions/workflows/ci.yaml/badge.svg)](https://github.com/danielsdeleo/deep_merge/actions/workflows/ci.yaml) 5 | 6 | Deep Merge is a simple set of utility functions for Hash. It permits you to merge elements inside a hash together recursively. The manner by which it does this is somewhat arbitrary (since there is no defining standard for this) but it should end up being pretty intuitive and do what you expect. 7 | 8 | You can learn a lot more about this by reading the test file. It's pretty well documented and has many examples of various merges from very simple to pretty complex. 9 | 10 | The primary need that caused me to write this library is the merging of elements coming from HTTP parameters and related stored parameters in session. This lets a user build up a set of parameters over time, modifying individual items. 11 | 12 | ## DeepMerge Core Documentation 13 | 14 | The `deep_merge!` method permits merging of arbitrary child elements. The two top level elements must be hashes. These hashes can contain unlimited (to stack limit) levels of child elements. These child elements do not have to be of the same type. Where child elements are of the same type, `deep_merge` will attempt to merge them together. Where child elements are not of the same type, `deep_merge` will skip or optionally overwrite the destination element with the contents of the source element at that level. For example, 15 | 16 | ```ruby 17 | source = {:x => [4, 5, '6'], :y => 2} 18 | dest = {:x => [1, 2, 3], :y => [7, 8, 9]} 19 | dest.deep_merge!(source) 20 | # => {:x => [1, 2, 3, 4, 5, '6'], :y => 2} 21 | ``` 22 | 23 | By default, `deep_merge!` will overwrite any unmergeables and merge everything else. To avoid this, use `deep_merge` (no bang/exclamation mark). 24 | 25 | ### Options 26 | 27 | Options are specified in the last parameter passed, which should be in hash format 28 | 29 | ```ruby 30 | hash.deep_merge!({:x => [1, 2]}, {:knockout_prefix => '--'}) 31 | ``` 32 | 33 | | Option | Default | Description | 34 | | ------ | ------- | ----------- | 35 | | `:preserve_unmergeables` | `false` | Set to true to skip any unmergeable elements from source | 36 | | [`:knockout_prefix`](#knockout_prefix) | `nil` | Set to string value to signify prefix which deletes elements from existing element | 37 | | [`:overwrite_arrays`](#overwrite_arrays) | `false` | Set to true if you want to avoid merging arrays | 38 | | [`:unpack_arrays`](#unpack_arrays) | `nil` | Set to string value to run `Array.join` then `String.split` against all arrays | 39 | | `:sort_merged_arrays` | `false` | Set to true to sort all arrays that are merged together | 40 | | [`:merge_hash_arrays`](#merge_hash_arrays) | `false` | Set to true to merge hashes within arrays | 41 | | [`:extend_existing_arrays`](#extend_existing_arrays) | `false` | Set to true to extend existing arrays, instead of overwriting them | 42 | | [`:keep_array_duplicates`](#keep_array_duplicates) | `false` | Set to true to keep duplicate entries in arrays, instead of coalescing them | 43 | | [`:merge_nil_values`](#merge_nil_values) | `false` | Set to true to merge nil hash values, overwriting a possibly non-nil value | 44 | | `:merge_debug` | `false` | Set to true to get console output of merge process for debugging | 45 | 46 | #### `:knockout_prefix` 47 | 48 | Provides a way to remove elements from an existing Hash by specifying them in a special way in the incoming hash. 49 | 50 | ```ruby 51 | source = {:x => ['--1', '3']} 52 | dest = {:x => ['1', '2']} 53 | dest.deep_merge!(source, :knockout_prefix => "--") 54 | # => {:x => ['2', '3']} 55 | ``` 56 | 57 | Additionally, if the knockout_prefix is passed alone as a string, it will cause the entire element to be removed. 58 | 59 | ```ruby 60 | source = {:x => '--'} 61 | dest = {:x => [1, 2, 3]} 62 | dest.deep_merge!(source, :knockout_prefix => "--") 63 | # => {:x => ""} 64 | ``` 65 | 66 | Note that the method `ko_deep_merge!` defaults the knockout prefix to `"--"` for convenience. 67 | 68 | ```ruby 69 | source = {:x => ['--1', '3']} 70 | dest = {:x => ['1', '2']} 71 | dest.ko_deep_merge!(source) 72 | # => {:x => ['2', '3']} 73 | ``` 74 | 75 | #### `:overwrite_arrays` 76 | 77 | Provides a way to replace Arrays instead of having them merge together. 78 | 79 | ```ruby 80 | source = {:x => ['1', '2']} 81 | dest = {:x => ['3', '4']} 82 | dest.deep_merge!(source, :overwrite_arrays => true) 83 | # => {:x => ['1', '2']} 84 | ``` 85 | 86 | #### `:unpack_arrays` 87 | 88 | Permits compound elements to be passed in as strings and to be converted into discrete array elements. 89 | 90 | ```ruby 91 | source = {:x => ['5', '6', '7,8']} 92 | dest = {:x => ['1,2,3', '4']} 93 | dest.deep_merge!(source, :unpack_arrays => ',') 94 | # => {:x => ['1', '2', '3', '4', '5', '6', '7', '8'} 95 | ``` 96 | 97 | The original purpose for this option is when receiving data from an HTML form, this makes it easy for a checkbox to pass multiple values from within a single HTML element. 98 | 99 | #### `:merge_hash_arrays` 100 | 101 | Merges hashes within arrays. 102 | 103 | ```ruby 104 | source = {:x => [{:z => 2}]} 105 | dest = {:x => [{:y => 1}]} 106 | dest.deep_merge!(source, :merge_hash_arrays => true) 107 | # => {:x => [{:y => 1, :z => 2}]} 108 | ``` 109 | 110 | #### `:extend_existing_arrays` 111 | 112 | Adds source elements to existing arrays, instead of overwriting them. 113 | 114 | ```ruby 115 | source = {:x => "4" } 116 | dest = {:x => ["1", "2", "3"]} 117 | dest.deep_merge!(source, :extend_existing_arrays => true) 118 | # => {:x => ["1", "2", "3", "4"]} 119 | ``` 120 | 121 | #### `:keep_array_duplicates` 122 | 123 | Keeps duplicate entries in arrays, instead of coalescing them. 124 | 125 | By default, without this option, duplicate entries are coalesced. 126 | 127 | ```ruby 128 | source = {:x => ["2", "3"]} 129 | dest = {:x => ["1", "2"]} 130 | dest.deep_merge!(source) 131 | # => {:x => ["1", "2", "3"]} 132 | ``` 133 | 134 | With this option they are kept. 135 | 136 | ```ruby 137 | source = {:x => ["2", "3"]} 138 | dest = {:x => ["1", "2"]} 139 | dest.deep_merge!(source, :keep_array_duplicates => true) 140 | # => {:x => ["1", "2", "2", "3"]} 141 | ``` 142 | 143 | #### `:merge_nil_values` 144 | 145 | Allows nil hash values to be merged. 146 | 147 | By default, without this option, nil hash values in the source are ignored. 148 | 149 | ```ruby 150 | source = {:x => nil} 151 | dest = {:x => "1"} 152 | dest.deep_merge!(source) 153 | # => {:x => "1"} 154 | ``` 155 | 156 | With this option, nil values will overwrite existing values. 157 | 158 | ```ruby 159 | source = {:x => nil} 160 | dest = {:x => "1"} 161 | dest.deep_merge!(source, :merge_nil_values => true) 162 | # => {:x => nil} 163 | ``` 164 | 165 | ## Using deep_merge in Rails 166 | 167 | To avoid conflict with ActiveSupport, load deep_merge via: 168 | 169 | require 'deep_merge/rails_compat' 170 | 171 | In a Gemfile: 172 | 173 | gem "deep_merge", :require => 'deep_merge/rails_compat' 174 | 175 | The deep_merge methods will then be defined as 176 | 177 | Hash#deeper_merge 178 | Hash#deeper_merge! 179 | Hash#ko_deeper_merge! 180 | 181 | ## Availability 182 | 183 | `deep_merge` was written by Steve Midgley, and is now maintained by Daniel DeLeo. The official home of `deep_merge` on the internet is now https://github.com/danielsdeleo/deep_merge 184 | 185 | ## License 186 | 187 | Copyright (c) 2008-2016 Steve Midgley, released under the MIT license 188 | -------------------------------------------------------------------------------- /lib/deep_merge/core.rb: -------------------------------------------------------------------------------- 1 | module DeepMerge 2 | 3 | class InvalidParameter < StandardError; end 4 | 5 | DEFAULT_FIELD_KNOCKOUT_PREFIX = '--' 6 | 7 | # Deep Merge core documentation. 8 | # deep_merge! method permits merging of arbitrary child elements. The two top level 9 | # elements must be hashes. These hashes can contain unlimited (to stack limit) levels 10 | # of child elements. These child elements to not have to be of the same types. 11 | # Where child elements are of the same type, deep_merge will attempt to merge them together. 12 | # Where child elements are not of the same type, deep_merge will skip or optionally overwrite 13 | # the destination element with the contents of the source element at that level. 14 | # So if you have two hashes like this: 15 | # source = {:x => [1,2,3], :y => 2} 16 | # dest = {:x => [4,5,'6'], :y => [7,8,9]} 17 | # dest.deep_merge!(source) 18 | # Results: {:x => [1,2,3,4,5,'6'], :y => 2} 19 | # By default, "deep_merge!" will overwrite any unmergeables and merge everything else. 20 | # To avoid this, use "deep_merge" (no bang/exclamation mark) 21 | # 22 | # Options: 23 | # Options are specified in the last parameter passed, which should be in hash format: 24 | # hash.deep_merge!({:x => [1,2]}, {:knockout_prefix => '--'}) 25 | # :preserve_unmergeables DEFAULT: false 26 | # Set to true to skip any unmergeable elements from source 27 | # :knockout_prefix DEFAULT: nil 28 | # Set to string value to signify prefix which deletes elements from existing element 29 | # :overwrite_arrays DEFAULT: false 30 | # Set to true if you want to avoid merging arrays 31 | # :sort_merged_arrays DEFAULT: false 32 | # Set to true to sort all arrays that are merged together 33 | # :unpack_arrays DEFAULT: nil 34 | # Set to string value to run "Array::join" then "String::split" against all arrays 35 | # :merge_hash_arrays DEFAULT: false 36 | # Set to true to merge hashes within arrays 37 | # :keep_array_duplicates DEFAULT: false 38 | # Set to true to preserve duplicate array entries 39 | # :merge_debug DEFAULT: false 40 | # Set to true to get console output of merge process for debugging 41 | # 42 | # Selected Options Details: 43 | # :knockout_prefix => The purpose of this is to provide a way to remove elements 44 | # from existing Hash by specifying them in a special way in incoming hash 45 | # source = {:x => ['--1', '2']} 46 | # dest = {:x => ['1', '3']} 47 | # dest.ko_deep_merge!(source) 48 | # Results: {:x => ['2','3']} 49 | # Additionally, if the knockout_prefix is passed alone as a string, it will cause 50 | # the entire element to be removed: 51 | # source = {:x => '--'} 52 | # dest = {:x => [1,2,3]} 53 | # dest.ko_deep_merge!(source) 54 | # Results: {:x => ""} 55 | # :unpack_arrays => The purpose of this is to permit compound elements to be passed 56 | # in as strings and to be converted into discrete array elements 57 | # irsource = {:x => ['1,2,3', '4']} 58 | # dest = {:x => ['5','6','7,8']} 59 | # dest.deep_merge!(source, {:unpack_arrays => ','}) 60 | # Results: {:x => ['1','2','3','4','5','6','7','8'} 61 | # Why: If receiving data from an HTML form, this makes it easy for a checkbox 62 | # to pass multiple values from within a single HTML element 63 | # 64 | # :merge_hash_arrays => merge hashes within arrays 65 | # source = {:x => [{:y => 1}]} 66 | # dest = {:x => [{:z => 2}]} 67 | # dest.deep_merge!(source, {:merge_hash_arrays => true}) 68 | # Results: {:x => [{:y => 1, :z => 2}]} 69 | # 70 | # :keep_array_duplicates => merges arrays within hashes but keeps duplicate elements 71 | # source = {:x => {:y => [1,2,2,2,3]}} 72 | # dest = {:x => {:y => [4,5,6]}} 73 | # dest.deep_merge!(source, {:keep_array_duplicates => true}) 74 | # Results: {:x => {:y => [1,2,2,2,3,4,5,6]}} 75 | # 76 | # There are many tests for this library - and you can learn more about the features 77 | # and usages of deep_merge! by just browsing the test examples 78 | def self.deep_merge!(source, dest, options = {}) 79 | # turn on this line for stdout debugging text 80 | merge_debug = options[:merge_debug] || false 81 | overwrite_unmergeable = !options[:preserve_unmergeables] 82 | knockout_prefix = options[:knockout_prefix] || nil 83 | raise InvalidParameter, "knockout_prefix cannot be an empty string in deep_merge!" if knockout_prefix == "" 84 | raise InvalidParameter, "overwrite_unmergeable must be true if knockout_prefix is specified in deep_merge!" if knockout_prefix && !overwrite_unmergeable 85 | # if present: we will split and join arrays on this char before merging 86 | array_split_char = options[:unpack_arrays] || false 87 | # request that we avoid merging arrays 88 | overwrite_arrays = options[:overwrite_arrays] || false 89 | # request that we sort together any arrays when they are merged 90 | sort_merged_arrays = options[:sort_merged_arrays] || false 91 | # request that arrays of hashes are merged together 92 | merge_hash_arrays = options[:merge_hash_arrays] || false 93 | # request to extend existing arrays, instead of overwriting them 94 | extend_existing_arrays = options[:extend_existing_arrays] || false 95 | # request that arrays keep duplicate elements 96 | keep_array_duplicates = options[:keep_array_duplicates] || false 97 | # request that nil values are merged or skipped (Skipped/false by default) 98 | merge_nil_values = options[:merge_nil_values] || false 99 | 100 | di = options[:debug_indent] || '' 101 | # do nothing if source is nil 102 | return dest if !merge_nil_values && source.nil? 103 | # if dest doesn't exist, then simply copy source to it 104 | if !(dest) && overwrite_unmergeable 105 | dest = source; return dest 106 | end 107 | 108 | puts "#{di}Source class: #{source.class.inspect} :: Dest class: #{dest.class.inspect}" if merge_debug 109 | if source.kind_of?(Hash) 110 | puts "#{di}Hashes: #{source.inspect} :: #{dest.inspect}" if merge_debug 111 | source.each do |src_key, src_value| 112 | if dest.kind_of?(Hash) 113 | puts "#{di} looping: #{src_key.inspect} => #{src_value.inspect} :: #{dest.inspect}" if merge_debug 114 | if dest[src_key] 115 | puts "#{di} ==>merging: #{src_key.inspect} => #{src_value.inspect} :: #{dest[src_key].inspect}" if merge_debug 116 | dest[src_key] = deep_merge!(src_value, dest[src_key], options.merge(:debug_indent => di + ' ')) 117 | else # dest[src_key] doesn't exist so we want to create and overwrite it (but we do this via deep_merge!) 118 | puts "#{di} ==>merging over: #{src_key.inspect} => #{src_value.inspect}" if merge_debug 119 | # note: we rescue here b/c some classes respond to "dup" but don't implement it (Numeric, TrueClass, FalseClass, NilClass among maybe others) 120 | begin 121 | src_dup = src_value.dup # we dup src_value if possible because we're going to merge into it (since dest is empty) 122 | rescue TypeError 123 | src_dup = src_value 124 | end 125 | if src_dup.kind_of?(Array) && keep_array_duplicates 126 | # note: in this case the merge will be additive, rather than a bounded set, so we can't simply merge src with itself 127 | # We need to merge src with an empty array 128 | src_dup = [] 129 | end 130 | dest[src_key] = deep_merge!(src_value, src_dup, options.merge(:debug_indent => di + ' ')) 131 | end 132 | elsif dest.kind_of?(Array) && extend_existing_arrays 133 | dest.push(source) 134 | else # dest isn't a hash, so we overwrite it completely (if permitted) 135 | if overwrite_unmergeable 136 | puts "#{di} overwriting dest: #{src_key.inspect} => #{src_value.inspect} -over-> #{dest.inspect}" if merge_debug 137 | dest = overwrite_unmergeables(source, dest, options) 138 | end 139 | end 140 | end 141 | elsif source.kind_of?(Array) 142 | puts "#{di}Arrays: #{source.inspect} :: #{dest.inspect}" if merge_debug 143 | if overwrite_arrays 144 | puts "#{di} overwrite arrays" if merge_debug 145 | dest = source 146 | else 147 | # if we are instructed, join/split any source arrays before processing 148 | if array_split_char 149 | puts "#{di} split/join on source: #{source.inspect}" if merge_debug 150 | source = source.join(array_split_char).split(array_split_char) 151 | if dest.kind_of?(Array) 152 | dest = dest.join(array_split_char).split(array_split_char) 153 | end 154 | end 155 | # if there's a naked knockout_prefix in source, that means we are to truncate dest 156 | if knockout_prefix && source.index(knockout_prefix) 157 | dest = clear_or_nil(dest); source.delete(knockout_prefix) 158 | end 159 | if dest.kind_of?(Array) 160 | if knockout_prefix 161 | print "#{di} knocking out: " if merge_debug 162 | # remove knockout prefix items from both source and dest 163 | source.delete_if do |ko_item| 164 | retval = false 165 | item = ko_item.respond_to?(:gsub) ? ko_item.gsub(%r{^#{knockout_prefix}}, "") : ko_item 166 | if item != ko_item 167 | print "#{ko_item} - " if merge_debug 168 | dest.delete(item) 169 | dest.delete(ko_item) 170 | retval = true 171 | end 172 | retval 173 | end 174 | puts if merge_debug 175 | end 176 | puts "#{di} merging arrays: #{source.inspect} :: #{dest.inspect}" if merge_debug 177 | source_all_hashes = source.all? { |i| i.kind_of?(Hash) } 178 | dest_all_hashes = dest.all? { |i| i.kind_of?(Hash) } 179 | if merge_hash_arrays && source_all_hashes && dest_all_hashes 180 | # merge hashes in lists 181 | list = [] 182 | dest.each_index do |i| 183 | list[i] = deep_merge!(source[i] || {}, dest[i], 184 | options.merge(:debug_indent => di + ' ')) 185 | end 186 | list += source[dest.count..-1] if source.count > dest.count 187 | dest = list 188 | elsif keep_array_duplicates 189 | dest = dest.concat(source) 190 | else 191 | dest = dest | source 192 | end 193 | dest.sort! if sort_merged_arrays 194 | elsif overwrite_unmergeable 195 | puts "#{di} overwriting dest: #{source.inspect} -over-> #{dest.inspect}" if merge_debug 196 | dest = overwrite_unmergeables(source, dest, options) 197 | end 198 | end 199 | else # src_hash is not an array or hash, so we'll have to overwrite dest 200 | if dest.kind_of?(Array) && extend_existing_arrays 201 | dest.push(source) 202 | else 203 | puts "#{di}Others: #{source.inspect} :: #{dest.inspect}" if merge_debug 204 | dest = overwrite_unmergeables(source, dest, options) 205 | end 206 | end 207 | puts "#{di}Returning #{dest.inspect}" if merge_debug 208 | dest 209 | end # deep_merge! 210 | 211 | # allows deep_merge! to uniformly handle overwriting of unmergeable entities 212 | def self.overwrite_unmergeables(source, dest, options) 213 | merge_debug = options[:merge_debug] || false 214 | overwrite_unmergeable = !options[:preserve_unmergeables] 215 | knockout_prefix = options[:knockout_prefix] || false 216 | di = options[:debug_indent] || '' 217 | if knockout_prefix && overwrite_unmergeable 218 | if source.kind_of?(String) # remove knockout string from source before overwriting dest 219 | src_tmp = source.gsub(%r{^#{knockout_prefix}},"") 220 | elsif source.kind_of?(Array) # remove all knockout elements before overwriting dest 221 | src_tmp = source.delete_if {|ko_item| ko_item.kind_of?(String) && ko_item.match(%r{^#{knockout_prefix}}) } 222 | else 223 | src_tmp = source 224 | end 225 | if src_tmp == source # if we didn't find a knockout_prefix then we just overwrite dest 226 | puts "#{di}#{src_tmp.inspect} -over-> #{dest.inspect}" if merge_debug 227 | dest = src_tmp 228 | else # if we do find a knockout_prefix, then we just delete dest 229 | puts "#{di}\"\" -over-> #{dest.inspect}" if merge_debug 230 | dest = "" 231 | end 232 | elsif overwrite_unmergeable 233 | dest = source 234 | end 235 | dest 236 | end 237 | 238 | def self.clear_or_nil(obj) 239 | if obj.respond_to?(:clear) 240 | obj.clear 241 | else 242 | obj = nil 243 | end 244 | obj 245 | end 246 | 247 | end # module DeepMerge 248 | -------------------------------------------------------------------------------- /test/test_deep_merge.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | 3 | $:.unshift(File.dirname(__FILE__) + '/../lib/') 4 | require 'deep_merge' 5 | 6 | # Assume strings have a blank? method 7 | # as they do when ActiveSupport is included. 8 | module StringBlank 9 | def blank? 10 | size == 0 11 | end 12 | end 13 | 14 | class TestDeepMerge < Test::Unit::TestCase 15 | 16 | def setup 17 | end 18 | 19 | # show that Hash object has deep merge capabilities in form of three methods: 20 | # ko_deep_merge! # uses '--' knockout and overwrites unmergeable 21 | # deep_merge! # overwrites unmergeable 22 | # deep_merge # skips unmergeable 23 | def test_hash_deep_merge 24 | x = {} 25 | assert x.respond_to?('deep_merge!'.to_sym) 26 | hash_src = {'id' => [3,4,5]} 27 | hash_dest = {'id' => [1,2,3]} 28 | assert hash_dest.ko_deep_merge!(hash_src) 29 | assert_equal({'id' => [1,2,3,4,5]}, hash_dest) 30 | 31 | hash_src = {'id' => [3,4,5]} 32 | hash_dest = {'id' => [1,2,3]} 33 | assert hash_dest.deep_merge!(hash_src) 34 | assert_equal({'id' => [1,2,3,4,5]}, hash_dest) 35 | 36 | hash_src = {'id' => 'xxx'} 37 | hash_dest = {'id' => [1,2,3]} 38 | assert hash_dest.deep_merge(hash_src) 39 | assert_equal({'id' => [1,2,3]}, hash_dest) 40 | end 41 | 42 | FIELD_KNOCKOUT_PREFIX = DeepMerge::DEFAULT_FIELD_KNOCKOUT_PREFIX 43 | 44 | # tests DeepMerge::deep_merge! function 45 | def test_deep_merge 46 | # merge tests (moving from basic to more complex) 47 | 48 | # test merging an hash w/array into blank hash 49 | hash_src = {'id' => '2'} 50 | hash_dst = {} 51 | DeepMerge::deep_merge!(hash_src.dup, hash_dst, {:knockout_prefix => FIELD_KNOCKOUT_PREFIX, :unpack_arrays => ","}) 52 | assert_equal hash_src, hash_dst 53 | 54 | # test merging an hash w/array into blank hash 55 | hash_src = {'region' => {'id' => ['227', '2']}} 56 | hash_dst = {} 57 | DeepMerge::deep_merge!(hash_src, hash_dst, {:knockout_prefix => FIELD_KNOCKOUT_PREFIX, :unpack_arrays => ","}) 58 | assert_equal hash_src, hash_dst 59 | 60 | # merge from empty hash 61 | hash_src = {} 62 | hash_dst = {"property" => ["2","4"]} 63 | DeepMerge::deep_merge!(hash_src, hash_dst) 64 | assert_equal({"property" => ["2","4"]}, hash_dst) 65 | 66 | # merge to empty hash 67 | hash_src = {"property" => ["2","4"]} 68 | hash_dst = {} 69 | DeepMerge::deep_merge!(hash_src, hash_dst) 70 | assert_equal({"property" => ["2","4"]}, hash_dst) 71 | 72 | # simple string overwrite 73 | hash_src = {"name" => "value"} 74 | hash_dst = {"name" => "value1"} 75 | DeepMerge::deep_merge!(hash_src, hash_dst) 76 | assert_equal({"name" => "value"}, hash_dst) 77 | 78 | # simple string overwrite of empty hash 79 | hash_src = {"name" => "value"} 80 | hash_dst = {} 81 | DeepMerge::deep_merge!(hash_src, hash_dst) 82 | assert_equal(hash_src, hash_dst) 83 | 84 | # hashes holding array 85 | hash_src = {"property" => ["1","3"]} 86 | hash_dst = {"property" => ["2","4"]} 87 | DeepMerge::deep_merge!(hash_src, hash_dst) 88 | assert_equal(["2","4","1","3"], hash_dst['property']) 89 | 90 | # hashes holding array (overwrite) 91 | hash_src = {"property" => ["1","3"]} 92 | hash_dst = {"property" => ["2","4"]} 93 | DeepMerge::deep_merge!(hash_src, hash_dst, {:overwrite_arrays => true}) 94 | assert_equal(["1","3"], hash_dst['property']) 95 | 96 | # hashes holding array (sorted) 97 | hash_src = {"property" => ["1","3"]} 98 | hash_dst = {"property" => ["2","4"]} 99 | DeepMerge::deep_merge!(hash_src, hash_dst, {:sort_merged_arrays => true}) 100 | assert_equal(["1","2","3","4"].sort, hash_dst['property']) 101 | 102 | # hashes holding hashes holding arrays (array with duplicate elements is merged with dest then src 103 | hash_src = {"property" => {"bedroom_count" => ["1", "2"], "bathroom_count" => ["1", "4+"]}} 104 | hash_dst = {"property" => {"bedroom_count" => ["3", "2"], "bathroom_count" => ["2"]}} 105 | DeepMerge::deep_merge!(hash_src, hash_dst) 106 | assert_equal({"property" => {"bedroom_count" => ["3","2","1"], "bathroom_count" => ["2", "1", "4+"]}}, hash_dst) 107 | 108 | # hash holding hash holding array v string (string is overwritten by array) 109 | hash_src = {"property" => {"bedroom_count" => ["1", "2"], "bathroom_count" => ["1", "4+"]}} 110 | hash_dst = {"property" => {"bedroom_count" => "3", "bathroom_count" => ["2"]}} 111 | DeepMerge::deep_merge!(hash_src, hash_dst) 112 | assert_equal({"property" => {"bedroom_count" => ["1", "2"], "bathroom_count" => ["2","1","4+"]}}, hash_dst) 113 | 114 | # hash holding hash holding array v string (string is NOT overwritten by array) 115 | hash_src = {"property" => {"bedroom_count" => ["1", "2"], "bathroom_count" => ["1", "4+"]}} 116 | hash_dst = {"property" => {"bedroom_count" => "3", "bathroom_count" => ["2"]}} 117 | DeepMerge::deep_merge!(hash_src, hash_dst, {:preserve_unmergeables => true}) 118 | assert_equal({"property" => {"bedroom_count" => "3", "bathroom_count" => ["2","1","4+"]}}, hash_dst) 119 | 120 | # hash holding hash holding string v array (array is overwritten by string) 121 | hash_src = {"property" => {"bedroom_count" => "3", "bathroom_count" => ["1", "4+"]}} 122 | hash_dst = {"property" => {"bedroom_count" => ["1", "2"], "bathroom_count" => ["2"]}} 123 | DeepMerge::deep_merge!(hash_src, hash_dst) 124 | assert_equal({"property" => {"bedroom_count" => "3", "bathroom_count" => ["2","1","4+"]}}, hash_dst) 125 | 126 | # hash holding hash holding string v array (array does NOT overwrite string) 127 | hash_src = {"property" => {"bedroom_count" => "3", "bathroom_count" => ["1", "4+"]}} 128 | hash_dst = {"property" => {"bedroom_count" => ["1", "2"], "bathroom_count" => ["2"]}} 129 | DeepMerge::deep_merge!(hash_src, hash_dst, {:preserve_unmergeables => true}) 130 | assert_equal({"property" => {"bedroom_count" => ["1", "2"], "bathroom_count" => ["2","1","4+"]}}, hash_dst) 131 | 132 | # hash holding hash holding hash v array (array is overwritten by hash) 133 | hash_src = {"property" => {"bedroom_count" => {"king_bed" => 3, "queen_bed" => 1}, "bathroom_count" => ["1", "4+"]}} 134 | hash_dst = {"property" => {"bedroom_count" => ["1", "2"], "bathroom_count" => ["2"]}} 135 | DeepMerge::deep_merge!(hash_src, hash_dst) 136 | assert_equal({"property" => {"bedroom_count" => {"king_bed" => 3, "queen_bed" => 1}, "bathroom_count" => ["2","1","4+"]}}, hash_dst) 137 | 138 | # hash holding hash holding hash v array (array is NOT overwritten by hash) 139 | hash_src = {"property" => {"bedroom_count" => {"king_bed" => 3, "queen_bed" => 1}, "bathroom_count" => ["1", "4+"]}} 140 | hash_dst = {"property" => {"bedroom_count" => ["1", "2"], "bathroom_count" => ["2"]}} 141 | DeepMerge::deep_merge!(hash_src, hash_dst, {:preserve_unmergeables => true}) 142 | assert_equal({"property" => {"bedroom_count" => ["1", "2"], "bathroom_count" => ["2","1","4+"]}}, hash_dst) 143 | 144 | # 3 hash layers holding integers (integers are overwritten by source) 145 | hash_src = {"property" => {"bedroom_count" => {"king_bed" => 3, "queen_bed" => 1}, "bathroom_count" => ["1", "4+"]}} 146 | hash_dst = {"property" => {"bedroom_count" => {"king_bed" => 2, "queen_bed" => 4}, "bathroom_count" => ["2"]}} 147 | DeepMerge::deep_merge!(hash_src, hash_dst) 148 | assert_equal({"property" => {"bedroom_count" => {"king_bed" => 3, "queen_bed" => 1}, "bathroom_count" => ["2","1","4+"]}}, hash_dst) 149 | 150 | # 3 hash layers holding arrays of int (arrays are merged) 151 | hash_src = {"property" => {"bedroom_count" => {"king_bed" => [3], "queen_bed" => [1]}, "bathroom_count" => ["1", "4+"]}} 152 | hash_dst = {"property" => {"bedroom_count" => {"king_bed" => [2], "queen_bed" => [4]}, "bathroom_count" => ["2"]}} 153 | DeepMerge::deep_merge!(hash_src, hash_dst) 154 | assert_equal({"property" => {"bedroom_count" => {"king_bed" => [2,3], "queen_bed" => [4,1]}, "bathroom_count" => ["2","1","4+"]}}, hash_dst) 155 | 156 | # 1 hash overwriting 3 hash layers holding arrays of int 157 | hash_src = {"property" => "1"} 158 | hash_dst = {"property" => {"bedroom_count" => {"king_bed" => [2], "queen_bed" => [4]}, "bathroom_count" => ["2"]}} 159 | DeepMerge::deep_merge!(hash_src, hash_dst) 160 | assert_equal({"property" => "1"}, hash_dst) 161 | 162 | # 1 hash NOT overwriting 3 hash layers holding arrays of int 163 | hash_src = {"property" => "1"} 164 | hash_dst = {"property" => {"bedroom_count" => {"king_bed" => [2], "queen_bed" => [4]}, "bathroom_count" => ["2"]}} 165 | DeepMerge::deep_merge!(hash_src, hash_dst, {:preserve_unmergeables => true}) 166 | assert_equal({"property" => {"bedroom_count" => {"king_bed" => [2], "queen_bed" => [4]}, "bathroom_count" => ["2"]}}, hash_dst) 167 | 168 | # 3 hash layers holding arrays of int (arrays are merged) but second hash's array is overwritten 169 | hash_src = {"property" => {"bedroom_count" => {"king_bed" => [3], "queen_bed" => [1]}, "bathroom_count" => "1"}} 170 | hash_dst = {"property" => {"bedroom_count" => {"king_bed" => [2], "queen_bed" => [4]}, "bathroom_count" => ["2"]}} 171 | DeepMerge::deep_merge!(hash_src, hash_dst) 172 | assert_equal({"property" => {"bedroom_count" => {"king_bed" => [2,3], "queen_bed" => [4,1]}, "bathroom_count" => "1"}}, hash_dst) 173 | 174 | # 3 hash layers holding arrays of int (arrays are merged) but second hash's array is NOT overwritten 175 | hash_src = {"property" => {"bedroom_count" => {"king_bed" => [3], "queen_bed" => [1]}, "bathroom_count" => "1"}} 176 | hash_dst = {"property" => {"bedroom_count" => {"king_bed" => [2], "queen_bed" => [4]}, "bathroom_count" => ["2"]}} 177 | DeepMerge::deep_merge!(hash_src, hash_dst, {:preserve_unmergeables => true}) 178 | assert_equal({"property" => {"bedroom_count" => {"king_bed" => [2,3], "queen_bed" => [4,1]}, "bathroom_count" => ["2"]}}, hash_dst) 179 | 180 | # 3 hash layers holding arrays of int, but one holds int. This one overwrites, but the rest merge 181 | hash_src = {"property" => {"bedroom_count" => {"king_bed" => 3, "queen_bed" => [1]}, "bathroom_count" => ["1"]}} 182 | hash_dst = {"property" => {"bedroom_count" => {"king_bed" => [2], "queen_bed" => [4]}, "bathroom_count" => ["2"]}} 183 | DeepMerge::deep_merge!(hash_src, hash_dst) 184 | assert_equal({"property" => {"bedroom_count" => {"king_bed" => 3, "queen_bed" => [4,1]}, "bathroom_count" => ["2","1"]}}, hash_dst) 185 | 186 | # 3 hash layers holding arrays of int, but source is incomplete. 187 | hash_src = {"property" => {"bedroom_count" => {"king_bed" => [3]}, "bathroom_count" => ["1"]}} 188 | hash_dst = {"property" => {"bedroom_count" => {"king_bed" => [2], "queen_bed" => [4]}, "bathroom_count" => ["2"]}} 189 | DeepMerge::deep_merge!(hash_src, hash_dst) 190 | assert_equal({"property" => {"bedroom_count" => {"king_bed" => [2,3], "queen_bed" => [4]}, "bathroom_count" => ["2","1"]}}, hash_dst) 191 | 192 | # 3 hash layers holding arrays of int, but source is shorter and has new 2nd level ints. 193 | hash_src = {"property" => {"bedroom_count" => {2=>3, "king_bed" => [3]}, "bathroom_count" => ["1"]}} 194 | hash_dst = {"property" => {"bedroom_count" => {"king_bed" => [2], "queen_bed" => [4]}, "bathroom_count" => ["2"]}} 195 | DeepMerge::deep_merge!(hash_src, hash_dst) 196 | assert_equal({"property" => {"bedroom_count" => {2=>3, "king_bed" => [2,3], "queen_bed" => [4]}, "bathroom_count" => ["2","1"]}}, hash_dst) 197 | 198 | # 3 hash layers holding arrays of int, but source is empty 199 | hash_src = {} 200 | hash_dst = {"property" => {"bedroom_count" => {"king_bed" => [2], "queen_bed" => [4]}, "bathroom_count" => ["2"]}} 201 | DeepMerge::deep_merge!(hash_src, hash_dst) 202 | assert_equal({"property" => {"bedroom_count" => {"king_bed" => [2], "queen_bed" => [4]}, "bathroom_count" => ["2"]}}, hash_dst) 203 | 204 | # 3 hash layers holding arrays of int, but dest is empty 205 | hash_src = {"property" => {"bedroom_count" => {2=>3, "king_bed" => [3]}, "bathroom_count" => ["1"]}} 206 | hash_dst = {} 207 | DeepMerge::deep_merge!(hash_src, hash_dst) 208 | assert_equal({"property" => {"bedroom_count" => {2=>3, "king_bed" => [3]}, "bathroom_count" => ["1"]}}, hash_dst) 209 | 210 | # 3 hash layers holding arrays of int, but source includes a nil in the array 211 | hash_src = {"property" => {"bedroom_count" => {"king_bed" => [nil], "queen_bed" => [1, nil]}, "bathroom_count" => [nil, "1"]}} 212 | hash_dst = {"property" => {"bedroom_count" => {"king_bed" => [2], "queen_bed" => [4]}, "bathroom_count" => ["2"]}} 213 | DeepMerge::deep_merge!(hash_src, hash_dst) 214 | assert_equal({"property" => {"bedroom_count" => {"king_bed" => [2,nil], "queen_bed" => [4, 1, nil]}, "bathroom_count" => ["2", nil, "1"]}}, hash_dst) 215 | 216 | # 3 hash layers holding arrays of int, but destination includes a nil in the array 217 | hash_src = {"property" => {"bedroom_count" => {"king_bed" => [3], "queen_bed" => [1]}, "bathroom_count" => ["1"]}} 218 | hash_dst = {"property" => {"bedroom_count" => {"king_bed" => [nil], "queen_bed" => [4, nil]}, "bathroom_count" => [nil,"2"]}} 219 | DeepMerge::deep_merge!(hash_src, hash_dst) 220 | assert_equal({"property" => {"bedroom_count" => {"king_bed" => [nil, 3], "queen_bed" => [4, nil, 1]}, "bathroom_count" => [nil, "2", "1"]}}, hash_dst) 221 | 222 | # if extend_existig_arrays == true && destination.kind_of?(Array) && source element is neither array nor hash, push source to destionation 223 | hash_src = { "property" => "4" } 224 | hash_dst = { "property" => ["1", "2", "3"] } 225 | DeepMerge::deep_merge!(hash_src, hash_dst, :extend_existing_arrays => true) 226 | assert_equal({"property" => ["1", "2", "3", "4"]}, hash_dst) 227 | 228 | # if extend_existig_arrays == true && destination.kind_of?(Array) && source.kind_of(Hash), push source to destionation 229 | hash_src = { "property" => {:number => "3"} } 230 | hash_dst = { "property" => [{:number => "1"}, {:number => "2"}] } 231 | DeepMerge::deep_merge!(hash_src, hash_dst, :extend_existing_arrays => true) 232 | assert_equal({"property"=>[{:number=>"1"}, {:number=>"2"}, {:number=>"3"}]}, hash_dst) 233 | 234 | # test parameter management for knockout_prefix and overwrite unmergable 235 | assert_raise(DeepMerge::InvalidParameter) {DeepMerge::deep_merge!(hash_src, hash_dst, {:knockout_prefix => ""})} 236 | assert_raise(DeepMerge::InvalidParameter) {DeepMerge::deep_merge!(hash_src, hash_dst, {:preserve_unmergeables => true, :knockout_prefix => ""})} 237 | assert_raise(DeepMerge::InvalidParameter) {DeepMerge::deep_merge!(hash_src, hash_dst, {:preserve_unmergeables => true, :knockout_prefix => "--"})} 238 | assert_nothing_raised(DeepMerge::InvalidParameter) {DeepMerge::deep_merge!(hash_src, hash_dst, {:knockout_prefix => "--"})} 239 | assert_nothing_raised(DeepMerge::InvalidParameter) {DeepMerge::deep_merge!(hash_src, hash_dst)} 240 | assert_nothing_raised(DeepMerge::InvalidParameter) {DeepMerge::deep_merge!(hash_src, hash_dst, {:preserve_unmergeables => true})} 241 | 242 | # hash holding arrays of arrays 243 | hash_src = {["1", "2", "3"] => ["1", "2"]} 244 | hash_dst = {["4", "5"] => ["3"]} 245 | DeepMerge::deep_merge!(hash_src, hash_dst) 246 | assert_equal({["1","2","3"] => ["1", "2"], ["4", "5"] => ["3"]}, hash_dst) 247 | 248 | # test merging of hash with blank hash, and make sure that source array split still functions 249 | hash_src = {'property' => {'bedroom_count' => ["1","2,3"]}} 250 | hash_dst = {} 251 | DeepMerge::deep_merge!(hash_src, hash_dst, {:knockout_prefix => FIELD_KNOCKOUT_PREFIX, :unpack_arrays => ","}) 252 | assert_equal({'property' => {'bedroom_count' => ["1","2","3"]}}, hash_dst) 253 | 254 | # test merging of hash with blank hash, and make sure that source array split does not function when turned off 255 | hash_src = {'property' => {'bedroom_count' => ["1","2,3"]}} 256 | hash_dst = {} 257 | DeepMerge::deep_merge!(hash_src, hash_dst, {:knockout_prefix => FIELD_KNOCKOUT_PREFIX}) 258 | assert_equal({'property' => {'bedroom_count' => ["1","2,3"]}}, hash_dst) 259 | 260 | # test merging into a blank hash with overwrite_unmergeables turned on 261 | hash_src = {"action"=>"browse", "controller"=>"results"} 262 | hash_dst = {} 263 | DeepMerge::deep_merge!(hash_src, hash_dst, {:overwrite_unmergeables => true, :knockout_prefix => FIELD_KNOCKOUT_PREFIX, :unpack_arrays => ","}) 264 | assert_equal hash_src, hash_dst 265 | 266 | # KNOCKOUT_PREFIX testing 267 | # the next few tests are looking for correct behavior from specific real-world params/session merges 268 | # using the custom modifiers built for param/session merges 269 | 270 | [nil, ","].each do |ko_split| 271 | # typical params/session style hash with knockout_merge elements 272 | hash_params = {"property"=>{"bedroom_count"=>[FIELD_KNOCKOUT_PREFIX+"1", "2", "3"]}} 273 | hash_session = {"property"=>{"bedroom_count"=>["1", "2", "3"]}} 274 | DeepMerge::deep_merge!(hash_params, hash_session, {:knockout_prefix => FIELD_KNOCKOUT_PREFIX, :unpack_arrays => ko_split}) 275 | assert_equal({"property"=>{"bedroom_count"=>["2", "3"]}}, hash_session) 276 | 277 | # typical params/session style hash with knockout_merge elements 278 | hash_params = {"property"=>{"bedroom_count"=>[FIELD_KNOCKOUT_PREFIX+"1", "2", "3"]}} 279 | hash_session = {"property"=>{"bedroom_count"=>["3"]}} 280 | DeepMerge::deep_merge!(hash_params, hash_session, {:knockout_prefix => FIELD_KNOCKOUT_PREFIX, :unpack_arrays => ko_split}) 281 | assert_equal({"property"=>{"bedroom_count"=>["3","2"]}}, hash_session) 282 | 283 | # typical params/session style hash with knockout_merge elements 284 | hash_params = {"property"=>{"bedroom_count"=>[FIELD_KNOCKOUT_PREFIX+"1", "2", "3"]}} 285 | hash_session = {"property"=>{"bedroom_count"=>["4"]}} 286 | DeepMerge::deep_merge!(hash_params, hash_session, {:knockout_prefix => FIELD_KNOCKOUT_PREFIX, :unpack_arrays => ko_split}) 287 | assert_equal({"property"=>{"bedroom_count"=>["4","2","3"]}}, hash_session) 288 | 289 | # typical params/session style hash with knockout_merge elements 290 | hash_params = {"property"=>{"bedroom_count"=>[FIELD_KNOCKOUT_PREFIX+"1", "2", "3"]}} 291 | hash_session = {"property"=>{"bedroom_count"=>[FIELD_KNOCKOUT_PREFIX+"1", "4"]}} 292 | DeepMerge::deep_merge!(hash_params, hash_session, {:knockout_prefix => FIELD_KNOCKOUT_PREFIX, :unpack_arrays => ko_split}) 293 | assert_equal({"property"=>{"bedroom_count"=>["4","2","3"]}}, hash_session) 294 | 295 | # typical params/session style hash with knockout_merge elements 296 | hash_params = {"amenity"=>{"id"=>[FIELD_KNOCKOUT_PREFIX+"1", FIELD_KNOCKOUT_PREFIX+"2", "3", "4"]}} 297 | hash_session = {"amenity"=>{"id"=>["1", "2"]}} 298 | DeepMerge::deep_merge!(hash_params, hash_session, {:knockout_prefix => FIELD_KNOCKOUT_PREFIX, :unpack_arrays => ko_split}) 299 | assert_equal({"amenity"=>{"id"=>["3","4"]}}, hash_session) 300 | end 301 | 302 | # special params/session style hash with knockout_merge elements in form src: ["1","2"] dest:["--1,--2", "3,4"] 303 | hash_params = {"amenity"=>{"id"=>[FIELD_KNOCKOUT_PREFIX+"1,"+FIELD_KNOCKOUT_PREFIX+"2", "3,4"]}} 304 | hash_session = {"amenity"=>{"id"=>["1", "2"]}} 305 | DeepMerge::deep_merge!(hash_params, hash_session, {:knockout_prefix => FIELD_KNOCKOUT_PREFIX, :unpack_arrays => ","}) 306 | assert_equal({"amenity"=>{"id"=>["3","4"]}}, hash_session) 307 | 308 | # same as previous but without ko_split value, this merge should fail 309 | hash_params = {"amenity"=>{"id"=>[FIELD_KNOCKOUT_PREFIX+"1,"+FIELD_KNOCKOUT_PREFIX+"2", "3,4"]}} 310 | hash_session = {"amenity"=>{"id"=>["1", "2"]}} 311 | DeepMerge::deep_merge!(hash_params, hash_session, {:knockout_prefix => FIELD_KNOCKOUT_PREFIX}) 312 | assert_equal({"amenity"=>{"id"=>["1","2","3,4"]}}, hash_session) 313 | 314 | # special params/session style hash with knockout_merge elements in form src: ["1","2"] dest:["--1,--2", "3,4"] 315 | hash_params = {"amenity"=>{"id"=>[FIELD_KNOCKOUT_PREFIX+"1,2", "3,4", "--5", "6"]}} 316 | hash_session = {"amenity"=>{"id"=>["1", "2"]}} 317 | DeepMerge::deep_merge!(hash_params, hash_session, {:knockout_prefix => FIELD_KNOCKOUT_PREFIX, :unpack_arrays => ","}) 318 | assert_equal({"amenity"=>{"id"=>["2","3","4","6"]}}, hash_session) 319 | 320 | # special params/session style hash with knockout_merge elements in form src: ["--1,--2", "3,4", "--5", "6"] dest:["1,2", "3,4"] 321 | hash_params = {"amenity"=>{"id"=>["#{FIELD_KNOCKOUT_PREFIX}1,#{FIELD_KNOCKOUT_PREFIX}2", "3,4", "#{FIELD_KNOCKOUT_PREFIX}5", "6"]}} 322 | hash_session = {"amenity"=>{"id"=>["1", "2", "3", "4"]}} 323 | DeepMerge::deep_merge!(hash_params, hash_session, {:knockout_prefix => FIELD_KNOCKOUT_PREFIX, :unpack_arrays => ","}) 324 | assert_equal({"amenity"=>{"id"=>["3","4","6"]}}, hash_session) 325 | 326 | 327 | hash_src = {"url_regions"=>[], "region"=>{"ids"=>["227,233"]}, "action"=>"browse", "task"=>"browse", "controller"=>"results"} 328 | hash_dst = {"region"=>{"ids"=>["227"]}} 329 | DeepMerge::deep_merge!(hash_src.dup, hash_dst, {:overwrite_unmergeables => true, :knockout_prefix => FIELD_KNOCKOUT_PREFIX, :unpack_arrays => ","}) 330 | assert_equal({"url_regions"=>[], "region"=>{"ids"=>["227","233"]}, "action"=>"browse", "task"=>"browse", "controller"=>"results"}, hash_dst) 331 | 332 | hash_src = {"region"=>{"ids"=>["--","227"], "id"=>"230"}} 333 | hash_dst = {"region"=>{"ids"=>["227", "233", "324", "230", "230"], "id"=>"230"}} 334 | DeepMerge::deep_merge!(hash_src, hash_dst, {:overwrite_unmergeables => true, :knockout_prefix => FIELD_KNOCKOUT_PREFIX, :unpack_arrays => ","}) 335 | assert_equal({"region"=>{"ids"=>["227"], "id"=>"230"}}, hash_dst) 336 | 337 | hash_src = {"region"=>{"ids"=>["--","227", "232", "233"], "id"=>"232"}} 338 | hash_dst = {"region"=>{"ids"=>["227", "233", "324", "230", "230"], "id"=>"230"}} 339 | DeepMerge::deep_merge!(hash_src, hash_dst, {:overwrite_unmergeables => true, :knockout_prefix => FIELD_KNOCKOUT_PREFIX, :unpack_arrays => ","}) 340 | assert_equal({"region"=>{"ids"=>["227", "232", "233"], "id"=>"232"}}, hash_dst) 341 | 342 | hash_src = {"region"=>{"ids"=>["--,227,232,233"], "id"=>"232"}} 343 | hash_dst = {"region"=>{"ids"=>["227", "233", "324", "230", "230"], "id"=>"230"}} 344 | DeepMerge::deep_merge!(hash_src, hash_dst, {:overwrite_unmergeables => true, :knockout_prefix => FIELD_KNOCKOUT_PREFIX, :unpack_arrays => ","}) 345 | assert_equal({"region"=>{"ids"=>["227", "232", "233"], "id"=>"232"}}, hash_dst) 346 | 347 | hash_src = {"region"=>{"ids"=>["--,227,232","233"], "id"=>"232"}} 348 | hash_dst = {"region"=>{"ids"=>["227", "233", "324", "230", "230"], "id"=>"230"}} 349 | DeepMerge::deep_merge!(hash_src, hash_dst, {:overwrite_unmergeables => true, :knockout_prefix => FIELD_KNOCKOUT_PREFIX, :unpack_arrays => ","}) 350 | assert_equal({"region"=>{"ids"=>["227", "232", "233"], "id"=>"232"}}, hash_dst) 351 | 352 | hash_src = {"region"=>{"ids"=>["--,227"], "id"=>"230"}} 353 | hash_dst = {"region"=>{"ids"=>["227", "233", "324", "230", "230"], "id"=>"230"}} 354 | DeepMerge::deep_merge!(hash_src, hash_dst, {:overwrite_unmergeables => true, :knockout_prefix => FIELD_KNOCKOUT_PREFIX, :unpack_arrays => ","}) 355 | assert_equal({"region"=>{"ids"=>["227"], "id"=>"230"}}, hash_dst) 356 | 357 | hash_src = {"region"=>{"ids"=>["--,227"], "id"=>"230"}} 358 | hash_dst = {"region"=>{"ids"=>["227", "233", "324", "230", "230"], "id"=>"230"}, "action"=>"browse", "task"=>"browse", "controller"=>"results", "property_order_by"=>"property_type.descr"} 359 | DeepMerge::deep_merge!(hash_src, hash_dst, {:overwrite_unmergeables => true, :knockout_prefix => FIELD_KNOCKOUT_PREFIX, :unpack_arrays => ","}) 360 | assert_equal({"region"=>{"ids"=>["227"], "id"=>"230"}, "action"=>"browse", "task"=>"browse", 361 | "controller"=>"results", "property_order_by"=>"property_type.descr"}, hash_dst) 362 | 363 | hash_src = {"query_uuid"=>"6386333d-389b-ab5c-8943-6f3a2aa914d7", "region"=>{"ids"=>["--,227"], "id"=>"230"}} 364 | hash_dst = {"query_uuid"=>"6386333d-389b-ab5c-8943-6f3a2aa914d7", "url_regions"=>[], "region"=>{"ids"=>["227", "233", "324", "230", "230"], "id"=>"230"}, "action"=>"browse", "task"=>"browse", "controller"=>"results", "property_order_by"=>"property_type.descr"} 365 | DeepMerge::deep_merge!(hash_src, hash_dst, {:overwrite_unmergeables => true, :knockout_prefix => FIELD_KNOCKOUT_PREFIX, :unpack_arrays => ","}) 366 | assert_equal({"query_uuid" => "6386333d-389b-ab5c-8943-6f3a2aa914d7", "url_regions"=>[], 367 | "region"=>{"ids"=>["227"], "id"=>"230"}, "action"=>"browse", "task"=>"browse", 368 | "controller"=>"results", "property_order_by"=>"property_type.descr"}, hash_dst) 369 | 370 | # knock out entire dest hash if "--" is passed for source 371 | hash_params = {'amenity' => "--"} 372 | hash_session = {"amenity" => "1"} 373 | DeepMerge::deep_merge!(hash_params, hash_session, {:knockout_prefix => "--", :unpack_arrays => ","}) 374 | assert_equal({'amenity' => ""}, hash_session) 375 | 376 | # knock out entire dest hash if "--" is passed for source 377 | hash_params = {'amenity' => ["--"]} 378 | hash_session = {"amenity" => "1"} 379 | DeepMerge::deep_merge!(hash_params, hash_session, {:knockout_prefix => "--", :unpack_arrays => ","}) 380 | assert_equal({'amenity' => []}, hash_session) 381 | 382 | # knock out entire dest hash if "--" is passed for source 383 | hash_params = {'amenity' => "--"} 384 | hash_session = {"amenity" => ["1"]} 385 | DeepMerge::deep_merge!(hash_params, hash_session, {:knockout_prefix => "--", :unpack_arrays => ","}) 386 | assert_equal({'amenity' => ""}, hash_session) 387 | 388 | # knock out entire dest hash if "--" is passed for source 389 | hash_params = {'amenity' => ["--"]} 390 | hash_session = {"amenity" => ["1"]} 391 | DeepMerge::deep_merge!(hash_params, hash_session, {:knockout_prefix => "--", :unpack_arrays => ","}) 392 | assert_equal({'amenity' => []}, hash_session) 393 | 394 | # knock out entire dest hash if "--" is passed for source 395 | hash_params = {'amenity' => ["--"]} 396 | hash_session = {"amenity" => "1"} 397 | DeepMerge::deep_merge!(hash_params, hash_session, {:knockout_prefix => "--", :unpack_arrays => ","}) 398 | assert_equal({'amenity' => []}, hash_session) 399 | 400 | # knock out entire dest hash if "--" is passed for source 401 | hash_params = {'amenity' => ["--", "2"]} 402 | hash_session = {'amenity' => ["1", "3", "7+"]} 403 | DeepMerge::deep_merge!(hash_params, hash_session, {:knockout_prefix => "--", :unpack_arrays => ","}) 404 | assert_equal({'amenity' => ["2"]}, hash_session) 405 | 406 | # knock out entire dest hash if "--" is passed for source 407 | hash_params = {'amenity' => ["--", "2"]} 408 | hash_session = {'amenity' => "5"} 409 | DeepMerge::deep_merge!(hash_params, hash_session, {:knockout_prefix => "--", :unpack_arrays => ","}) 410 | assert_equal({'amenity' => ['2']}, hash_session) 411 | 412 | # knock out entire dest hash if "--" is passed for source 413 | hash_params = {'amenity' => "--"} 414 | hash_session = {"amenity"=>{"id"=>["1", "2", "3", "4"]}} 415 | DeepMerge::deep_merge!(hash_params, hash_session, {:knockout_prefix => "--", :unpack_arrays => ","}) 416 | assert_equal({'amenity' => ""}, hash_session) 417 | 418 | # knock out entire dest hash if "--" is passed for source 419 | hash_params = {'amenity' => ["--"]} 420 | hash_session = {"amenity"=>{"id"=>["1", "2", "3", "4"]}} 421 | DeepMerge::deep_merge!(hash_params, hash_session, {:knockout_prefix => "--", :unpack_arrays => ","}) 422 | assert_equal({'amenity' => []}, hash_session) 423 | 424 | # knock out dest array if "--" is passed for source 425 | hash_params = {"region" => {'ids' => FIELD_KNOCKOUT_PREFIX}} 426 | hash_session = {"region"=>{"ids"=>["1", "2", "3", "4"]}} 427 | DeepMerge::deep_merge!(hash_params, hash_session, {:knockout_prefix => FIELD_KNOCKOUT_PREFIX, :unpack_arrays => ","}) 428 | assert_equal({'region' => {'ids' => ""}}, hash_session) 429 | 430 | # knock out dest array but leave other elements of hash intact 431 | hash_params = {"region" => {'ids' => FIELD_KNOCKOUT_PREFIX}} 432 | hash_session = {"region"=>{"ids"=>["1", "2", "3", "4"], 'id'=>'11'}} 433 | DeepMerge::deep_merge!(hash_params, hash_session, {:knockout_prefix => FIELD_KNOCKOUT_PREFIX, :unpack_arrays => ","}) 434 | assert_equal({'region' => {'ids' => "", 'id'=>'11'}}, hash_session) 435 | 436 | # knock out entire tree of dest hash 437 | hash_params = {"region" => FIELD_KNOCKOUT_PREFIX} 438 | hash_session = {"region"=>{"ids"=>["1", "2", "3", "4"], 'id'=>'11'}} 439 | DeepMerge::deep_merge!(hash_params, hash_session, {:knockout_prefix => FIELD_KNOCKOUT_PREFIX, :unpack_arrays => ","}) 440 | assert_equal({'region' => ""}, hash_session) 441 | 442 | # knock out entire tree of dest hash - retaining array format 443 | hash_params = {"region" => {'ids' => [FIELD_KNOCKOUT_PREFIX]}} 444 | hash_session = {"region"=>{"ids"=>["1", "2", "3", "4"], 'id'=>'11'}} 445 | DeepMerge::deep_merge!(hash_params, hash_session, {:knockout_prefix => FIELD_KNOCKOUT_PREFIX, :unpack_arrays => ","}) 446 | assert_equal({'region' => {'ids' => [], 'id'=>'11'}}, hash_session) 447 | 448 | # knock out entire tree of dest hash & replace with new content 449 | hash_params = {"region" => {'ids' => ["2", FIELD_KNOCKOUT_PREFIX, "6"]}} 450 | hash_session = {"region"=>{"ids"=>["1", "2", "3", "4"], 'id'=>'11'}} 451 | DeepMerge::deep_merge!(hash_params, hash_session, {:knockout_prefix => FIELD_KNOCKOUT_PREFIX, :unpack_arrays => ","}) 452 | assert_equal({'region' => {'ids' => ["2", "6"], 'id'=>'11'}}, hash_session) 453 | 454 | # knock out entire tree of dest hash & replace with new content 455 | hash_params = {"region" => {'ids' => ["7", FIELD_KNOCKOUT_PREFIX, "6"]}} 456 | hash_session = {"region"=>{"ids"=>["1", "2", "3", "4"], 'id'=>'11'}} 457 | DeepMerge::deep_merge!(hash_params, hash_session, {:knockout_prefix => FIELD_KNOCKOUT_PREFIX, :unpack_arrays => ","}) 458 | assert_equal({'region' => {'ids' => ["7", "6"], 'id'=>'11'}}, hash_session) 459 | 460 | # edge test: make sure that when we turn off knockout_prefix that all values are processed correctly 461 | hash_params = {"region" => {'ids' => ["7", "--", "2", "6,8"]}} 462 | hash_session = {"region"=>{"ids"=>["1", "2", "3", "4"], 'id'=>'11'}} 463 | DeepMerge::deep_merge!(hash_params, hash_session, {:unpack_arrays => ","}) 464 | assert_equal({'region' => {'ids' => ["1", "2", "3", "4", "7", "--", "6", "8"], 'id'=>'11'}}, hash_session) 465 | 466 | # edge test 2: make sure that when we turn off source array split that all values are processed correctly 467 | hash_params = {"region" => {'ids' => ["7", "3", "--", "6,8"]}} 468 | hash_session = {"region"=>{"ids"=>["1", "2", "3", "4"], 'id'=>'11'}} 469 | DeepMerge::deep_merge!(hash_params, hash_session) 470 | assert_equal({'region' => {'ids' => ["1", "2", "3", "4", "7", "--", "6,8"], 'id'=>'11'}}, hash_session) 471 | 472 | # Example: src = {'key' => "--1"}, dst = {'key' => "1"} -> merges to {'key' => ""} 473 | hash_params = {"amenity"=>"--1"} 474 | hash_session = {"amenity"=>"1"} 475 | DeepMerge::deep_merge!(hash_params, hash_session, {:knockout_prefix => FIELD_KNOCKOUT_PREFIX}) 476 | assert_equal({"amenity"=>""}, hash_session) 477 | 478 | # Example: src = {'key' => "--1"}, dst = {'key' => "2"} -> merges to {'key' => ""} 479 | hash_params = {"amenity"=>"--1"} 480 | hash_session = {"amenity"=>"2"} 481 | DeepMerge::deep_merge!(hash_params, hash_session, {:knockout_prefix => FIELD_KNOCKOUT_PREFIX}) 482 | assert_equal({"amenity"=>""}, hash_session) 483 | 484 | # Example: src = {'key' => "--1"}, dst = {'key' => "1"} -> merges to {'key' => ""} 485 | hash_params = {"amenity"=>["--1"]} 486 | hash_session = {"amenity"=>"1"} 487 | DeepMerge::deep_merge!(hash_params, hash_session, {:knockout_prefix => FIELD_KNOCKOUT_PREFIX}) 488 | assert_equal({"amenity"=>[]}, hash_session) 489 | 490 | # Example: src = {'key' => "--1"}, dst = {'key' => "1"} -> merges to {'key' => ""} 491 | hash_params = {"amenity"=>["--1"]} 492 | hash_session = {"amenity"=>["1"]} 493 | DeepMerge::deep_merge!(hash_params, hash_session, {:knockout_prefix => FIELD_KNOCKOUT_PREFIX}) 494 | assert_equal({"amenity"=>[]}, hash_session) 495 | 496 | # Example: src = {'key' => "--1"}, dst = {'key' => "1"} -> merges to {'key' => ""} 497 | hash_params = {"amenity"=>"--1"} 498 | hash_session = {} 499 | DeepMerge::deep_merge!(hash_params, hash_session, {:knockout_prefix => FIELD_KNOCKOUT_PREFIX}) 500 | assert_equal({"amenity"=>""}, hash_session) 501 | 502 | 503 | # Example: src = {'key' => "--1"}, dst = {'key' => "1"} -> merges to {'key' => ""} 504 | hash_params = {"amenity"=>"--1"} 505 | hash_session = {"amenity"=>["1"]} 506 | DeepMerge::deep_merge!(hash_params, hash_session, {:knockout_prefix => FIELD_KNOCKOUT_PREFIX}) 507 | assert_equal({"amenity"=>""}, hash_session) 508 | 509 | #are unmerged hashes passed unmodified w/out :unpack_arrays? 510 | hash_params = {"amenity"=>{"id"=>["26,27"]}} 511 | hash_session = {} 512 | DeepMerge::deep_merge!(hash_params, hash_session, {:knockout_prefix => FIELD_KNOCKOUT_PREFIX}) 513 | assert_equal({"amenity"=>{"id"=>["26,27"]}}, hash_session) 514 | 515 | #hash should be merged 516 | hash_params = {"amenity"=>{"id"=>["26,27"]}} 517 | hash_session = {} 518 | DeepMerge::deep_merge!(hash_params, hash_session, {:knockout_prefix => FIELD_KNOCKOUT_PREFIX, :unpack_arrays => ","}) 519 | assert_equal({"amenity"=>{"id"=>["26","27"]}}, hash_session) 520 | 521 | # second merge of same values should result in no change in output 522 | hash_params = {"amenity"=>{"id"=>["26,27"]}} 523 | DeepMerge::deep_merge!(hash_params, hash_session, {:knockout_prefix => FIELD_KNOCKOUT_PREFIX, :unpack_arrays => ","}) 524 | assert_equal({"amenity"=>{"id"=>["26","27"]}}, hash_session) 525 | 526 | #hashes with knockout values are suppressed 527 | hash_params = {"amenity"=>{"id"=>["#{FIELD_KNOCKOUT_PREFIX}26,#{FIELD_KNOCKOUT_PREFIX}27,28"]}} 528 | hash_session = {} 529 | DeepMerge::deep_merge!(hash_params, hash_session, {:knockout_prefix => FIELD_KNOCKOUT_PREFIX, :unpack_arrays => ","}) 530 | assert_equal({"amenity"=>{"id"=>["28"]}}, hash_session) 531 | 532 | hash_src= {'region' =>{'ids'=>['--']}, 'query_uuid' => 'zzz'} 533 | hash_dst= {'region' =>{'ids'=>['227','2','3','3']}, 'query_uuid' => 'zzz'} 534 | DeepMerge::deep_merge!(hash_src, hash_dst, {:knockout_prefix => '--', :unpack_arrays => ","}) 535 | assert_equal({'region' =>{'ids'=>[]}, 'query_uuid' => 'zzz'}, hash_dst) 536 | 537 | hash_src= {'region' =>{'ids'=>['--']}, 'query_uuid' => 'zzz'} 538 | hash_dst= {'region' =>{'ids'=>['227','2','3','3'], 'id' => '3'}, 'query_uuid' => 'zzz'} 539 | DeepMerge::deep_merge!(hash_src, hash_dst, {:knockout_prefix => '--', :unpack_arrays => ","}) 540 | assert_equal({'region' =>{'ids'=>[], 'id'=>'3'}, 'query_uuid' => 'zzz'}, hash_dst) 541 | 542 | hash_src= {'region' =>{'ids'=>['--']}, 'query_uuid' => 'zzz'} 543 | hash_dst= {'region' =>{'muni_city_id' => '2244', 'ids'=>['227','2','3','3'], 'id'=>'3'}, 'query_uuid' => 'zzz'} 544 | DeepMerge::deep_merge!(hash_src, hash_dst, {:knockout_prefix => '--', :unpack_arrays => ","}) 545 | assert_equal({'region' =>{'muni_city_id' => '2244', 'ids'=>[], 'id'=>'3'}, 'query_uuid' => 'zzz'}, hash_dst) 546 | 547 | hash_src= {'region' =>{'ids'=>['--'], 'id' => '5'}, 'query_uuid' => 'zzz'} 548 | hash_dst= {'region' =>{'muni_city_id' => '2244', 'ids'=>['227','2','3','3'], 'id'=>'3'}, 'query_uuid' => 'zzz'} 549 | DeepMerge::deep_merge!(hash_src, hash_dst, {:knockout_prefix => '--', :unpack_arrays => ","}) 550 | assert_equal({'region' =>{'muni_city_id' => '2244', 'ids'=>[], 'id'=>'5'}, 'query_uuid' => 'zzz'}, hash_dst) 551 | 552 | hash_src= {'region' =>{'ids'=>['--', '227'], 'id' => '5'}, 'query_uuid' => 'zzz'} 553 | hash_dst= {'region' =>{'muni_city_id' => '2244', 'ids'=>['227','2','3','3'], 'id'=>'3'}, 'query_uuid' => 'zzz'} 554 | DeepMerge::deep_merge!(hash_src, hash_dst, {:knockout_prefix => '--', :unpack_arrays => ","}) 555 | assert_equal({'region' =>{'muni_city_id' => '2244', 'ids'=>['227'], 'id'=>'5'}, 'query_uuid' => 'zzz'}, hash_dst) 556 | 557 | hash_src= {'region' =>{'muni_city_id' => '--', 'ids'=>'--', 'id'=>'5'}, 'query_uuid' => 'zzz'} 558 | hash_dst= {'region' =>{'muni_city_id' => '2244', 'ids'=>['227','2','3','3'], 'id'=>'3'}, 'query_uuid' => 'zzz'} 559 | DeepMerge::deep_merge!(hash_src, hash_dst, {:knockout_prefix => '--', :unpack_arrays => ","}) 560 | assert_equal({'region' =>{'muni_city_id' => '', 'ids'=>'', 'id'=>'5'}, 'query_uuid' => 'zzz'}, hash_dst) 561 | 562 | hash_src= {'region' =>{'muni_city_id' => '--', 'ids'=>['--'], 'id'=>'5'}, 'query_uuid' => 'zzz'} 563 | hash_dst= {'region' =>{'muni_city_id' => '2244', 'ids'=>['227','2','3','3'], 'id'=>'3'}, 'query_uuid' => 'zzz'} 564 | DeepMerge::deep_merge!(hash_src, hash_dst, {:knockout_prefix => '--', :unpack_arrays => ","}) 565 | assert_equal({'region' =>{'muni_city_id' => '', 'ids'=>[], 'id'=>'5'}, 'query_uuid' => 'zzz'}, hash_dst) 566 | 567 | hash_src= {'region' =>{'muni_city_id' => '--', 'ids'=>['--','227'], 'id'=>'5'}, 'query_uuid' => 'zzz'} 568 | hash_dst= {'region' =>{'muni_city_id' => '2244', 'ids'=>['227','2','3','3'], 'id'=>'3'}, 'query_uuid' => 'zzz'} 569 | DeepMerge::deep_merge!(hash_src, hash_dst, {:knockout_prefix => '--', :unpack_arrays => ","}) 570 | assert_equal({'region' =>{'muni_city_id' => '', 'ids'=>['227'], 'id'=>'5'}, 'query_uuid' => 'zzz'}, hash_dst) 571 | 572 | hash_src = {"muni_city_id"=>"--", "id"=>""} 573 | hash_dst = {"muni_city_id"=>"", "id"=>""} 574 | DeepMerge::deep_merge!(hash_src, hash_dst, {:knockout_prefix => '--', :unpack_arrays => ","}) 575 | assert_equal({"muni_city_id"=>"", "id"=>""}, hash_dst) 576 | 577 | hash_src = {"region"=>{"muni_city_id"=>"--", "id"=>""}} 578 | hash_dst = {"region"=>{"muni_city_id"=>"", "id"=>""}} 579 | DeepMerge::deep_merge!(hash_src, hash_dst, {:knockout_prefix => '--', :unpack_arrays => ","}) 580 | assert_equal({"region"=>{"muni_city_id"=>"", "id"=>""}}, hash_dst) 581 | 582 | hash_src = {"query_uuid"=>"a0dc3c84-ec7f-6756-bdb0-fff9157438ab", "url_regions"=>[], "region"=>{"muni_city_id"=>"--", "id"=>""}, "property"=>{"property_type_id"=>"", "search_rate_min"=>"", "search_rate_max"=>""}, "task"=>"search", "run_query"=>"Search"} 583 | hash_dst = {"query_uuid"=>"a0dc3c84-ec7f-6756-bdb0-fff9157438ab", "url_regions"=>[], "region"=>{"muni_city_id"=>"", "id"=>""}, "property"=>{"property_type_id"=>"", "search_rate_min"=>"", "search_rate_max"=>""}, "task"=>"search", "run_query"=>"Search"} 584 | DeepMerge::deep_merge!(hash_src, hash_dst, {:knockout_prefix => '--', :unpack_arrays => ","}) 585 | assert_equal({"query_uuid"=>"a0dc3c84-ec7f-6756-bdb0-fff9157438ab", "url_regions"=>[], "region"=>{"muni_city_id"=>"", "id"=>""}, "property"=>{"property_type_id"=>"", "search_rate_min"=>"", "search_rate_max"=>""}, "task"=>"search", "run_query"=>"Search"}, hash_dst) 586 | 587 | # hash of array of hashes 588 | hash_src = {"item" => [{"1" => "3"}, {"2" => "4"}]} 589 | hash_dst = {"item" => [{"3" => "5"}]} 590 | DeepMerge::deep_merge!(hash_src, hash_dst) 591 | assert_equal({"item" => [{"3" => "5"}, {"1" => "3"}, {"2" => "4"}]}, hash_dst) 592 | 593 | ###################################### 594 | # tests for "merge_hash_arrays" option 595 | 596 | hash_src = {"item" => [{"1" => "3"}]} 597 | hash_dst = {"item" => [{"3" => "5"}]} 598 | DeepMerge::deep_merge!(hash_src, hash_dst, {:merge_hash_arrays => true}) 599 | assert_equal({"item" => [{"3" => "5", "1" => "3"}]}, hash_dst) 600 | 601 | hash_src = {"item" => [{"1" => "3"}, {"2" => "4"}]} 602 | hash_dst = {"item" => [{"3" => "5"}]} 603 | DeepMerge::deep_merge!(hash_src, hash_dst, {:merge_hash_arrays => true}) 604 | assert_equal({"item" => [{"3" => "5", "1" => "3"}, {"2" => "4"}]}, hash_dst) 605 | 606 | hash_src = {"item" => [{"1" => "3"}]} 607 | hash_dst = {"item" => [{"3" => "5"}, {"2" => "4"}]} 608 | DeepMerge::deep_merge!(hash_src, hash_dst, {:merge_hash_arrays => true}) 609 | assert_equal({"item" => [{"3" => "5", "1" => "3"}, {"2" => "4"}]}, hash_dst) 610 | 611 | # if arrays contain non-hash objects, the :merge_hash_arrays option has 612 | # no effect. 613 | hash_src = {"item" => [{"1" => "3"}, "str"]} # contains "str", non-hash 614 | hash_dst = {"item" => [{"3" => "5"}]} 615 | DeepMerge::deep_merge!(hash_src, hash_dst, {:merge_hash_arrays => true}) 616 | assert_equal({"item" => [{"3" => "5"}, {"1" => "3"}, "str"]}, hash_dst) 617 | 618 | # Merging empty strings 619 | s1, s2 = "hello", "" 620 | [s1, s2].each { |s| s.extend StringBlank } 621 | hash_dst = {"item" => s1 } 622 | hash_src = {"item" => s2 } 623 | DeepMerge::deep_merge!(hash_src, hash_dst) 624 | assert_equal({"item" => ""}, hash_dst) 625 | 626 | ###################################### 627 | # tests for "keep_array_duplicates" option 628 | hash_src = {"item" => ["2", "3"]} 629 | hash_dst = {"item" => ["1", "2"]} 630 | DeepMerge::deep_merge!(hash_src, hash_dst, {:keep_array_duplicates => true}) 631 | assert_equal({"item" => ["1", "2", "2", "3"]}, hash_dst) 632 | 633 | # For Issue 34 - keep_array_duplicates against a nil src doesn't do a recursive merge 634 | hash_src = {"item" => ["2", "3"]} 635 | hash_dst = { } 636 | DeepMerge::deep_merge!(hash_src, hash_dst, {:keep_array_duplicates => true}) 637 | assert_equal({"item" => ["2", "3"]}, hash_dst) 638 | 639 | # Don't merge nil values by default 640 | hash_src = {"item" => nil} 641 | hash_dst = {"item" => "existing"} 642 | DeepMerge::deep_merge!(hash_src, hash_dst) 643 | assert_equal({"item" => "existing"}, hash_dst) 644 | 645 | # Merge nil values via an explicit: :merge_nil_values => true 646 | hash_src = {"item" => nil} 647 | hash_dst = {"item" => "existing"} 648 | DeepMerge::deep_merge!(hash_src, hash_dst, {:merge_nil_values => true}) 649 | assert_equal({"item" => nil}, hash_dst) 650 | end # test_deep_merge 651 | end 652 | --------------------------------------------------------------------------------