deep_cloneable 2.3.2 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0ff9ad4fd2ffe0fcf8495c86114392032080de83d306325f9932e7752459be90
4
- data.tar.gz: fcba14c118c7a7b2144f0d25f61cbf4c8a55098e92930a260284aba2eacd8f1b
3
+ metadata.gz: c9bc79180e45ac09676614be579f12550f6e9e4e123a320bb42ce7675dfe95eb
4
+ data.tar.gz: 39db7d4d43ee2dee821d16dce99feb1fcd70b20878d6bb37b9283b51213ed9b3
5
5
  SHA512:
6
- metadata.gz: d390f573c9730d707e841aa7ef658a39ac70de947af7aa9d750e98f0e9087e835288f471c9ed9599b783e3cd9726ed62c7e7c3f1986f6c92e3044f51f74575b2
7
- data.tar.gz: ecc8d7314a3d745d619de830b479931d646924682939cf387f24b5557061703ba6644d1623c56f6b21b3d82237e9188546bfe8b6f54b186cb7f738e338e2f286
6
+ metadata.gz: f4908868fbfd325c6953c8c68fbce7ce935349c9eedf206579b3f6433bfa1073f1f2c2c10cff81ce2ccd212c6db921d0be904784062318d98d1fd69d03eb21f4
7
+ data.tar.gz: f8fd74ad2f5c95d9cecbd5df6450b9bf551a0aba708096b6cfbbee777d3ba43e6b1f64447c8cac03c1fdacf8867880a7ac8f2f2297fe17471b9881ea88097afb
@@ -0,0 +1,20 @@
1
+ Metrics:
2
+ Enabled: false
3
+
4
+ Security/YAMLLoad:
5
+ Enabled: false
6
+
7
+ Style/ClassAndModuleChildren:
8
+ Enabled: false
9
+
10
+ Style/Documentation:
11
+ Enabled: false
12
+
13
+ Style/HashSyntax:
14
+ EnforcedStyle: hash_rockets
15
+
16
+ Style/Lambda:
17
+ EnforcedStyle: lambda
18
+
19
+ Style/SymbolArray:
20
+ EnforcedStyle: brackets
@@ -15,8 +15,8 @@ rvm:
15
15
  - 2.5.1
16
16
 
17
17
  before_install:
18
- - gem update --system
19
- - gem install bundler
18
+ - gem update --system 2.7.8
19
+ - gem install bundler -v 1.17.3
20
20
 
21
21
  gemfile:
22
22
  - gemfiles/3.1.gemfile
@@ -5,6 +5,16 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
5
5
  and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
+
9
+ ## [2.4.0] - 2019-01-10
10
+ ### Added
11
+ - Support `has_one` and `has_one - through`
12
+ ### Changed
13
+ - Lint code via Rubocop
14
+ - Refactor code to improve readability
15
+ - Use HTTPS in the gemspec website URL
16
+
17
+ ## [2.3.2] - 2018-04-11
8
18
  ### Added
9
19
  - Support for Ruby 2.4 and 2.5
10
20
  - Support for Rails 5.1 and 5.2
@@ -156,7 +166,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
156
166
  ### Fixed
157
167
  - Convert existing code to a gem
158
168
 
159
- [Unreleased]: https://github.com/moiristo/deep_cloneable/compare/v2.3.1...HEAD
169
+ [Unreleased]: https://github.com/moiristo/deep_cloneable/compare/v2.4.0...HEAD
170
+ [2.4.0]: https://github.com/moiristo/deep_cloneable/compare/v2.3.2...v2.4.0
171
+ [2.3.2]: https://github.com/moiristo/deep_cloneable/compare/v2.3.1...v2.3.2
160
172
  [2.3.1]: https://github.com/moiristo/deep_cloneable/compare/v2.3.0...v2.3.1
161
173
  [2.3.0]: https://github.com/moiristo/deep_cloneable/compare/v2.2.2...v2.3.0
162
174
  [2.2.2]: https://github.com/moiristo/deep_cloneable/compare/v2.2.1...v2.2.2
data/Gemfile CHANGED
@@ -2,14 +2,14 @@ source 'http://rubygems.org'
2
2
 
3
3
  gem 'activerecord', '>= 3.1.0', '< 6'
4
4
 
5
+ gem 'git', '~> 1.2.9', :group => :test
5
6
  gem 'highline', '~> 1.6.0', :group => :test
6
- gem 'rake', '~> 10.4', :group => :test
7
7
  gem 'rack', '~> 1.6', :group => :test
8
- gem 'git', '~> 1.2.9', :group => :test
8
+ gem 'rake', '~> 10.4', :group => :test
9
9
 
10
- gem 'minitest', :group => :test
11
10
  gem 'appraisal', :group => :test
12
- gem 'sqlite3', :group => :test
13
- gem 'rdoc', '>= 2.4.2', '< 6', :group => :test
14
- gem 'nokogiri', '~> 1.5.0', :group => :test
15
11
  gem 'jeweler', :group => :test
12
+ gem 'minitest', :group => :test
13
+ gem 'nokogiri', '~> 1.5.0', :group => :test
14
+ gem 'rdoc', '>= 2.4.2', '< 6', :group => :test
15
+ gem 'sqlite3', :group => :test
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2018 Reinier de Lange
1
+ Copyright (c) 2019 Reinier de Lange
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.3.2
1
+ 2.4.0
@@ -2,16 +2,16 @@
2
2
  # DO NOT EDIT THIS FILE DIRECTLY
3
3
  # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
4
  # -*- encoding: utf-8 -*-
5
- # stub: deep_cloneable 2.3.2 ruby lib
5
+ # stub: deep_cloneable 2.4.0 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "deep_cloneable".freeze
9
- s.version = "2.3.2"
9
+ s.version = "2.4.0"
10
10
 
11
11
  s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
12
12
  s.require_paths = ["lib".freeze]
13
13
  s.authors = ["Reinier de Lange".freeze]
14
- s.date = "2018-04-11"
14
+ s.date = "2019-01-10"
15
15
  s.description = "Extends the functionality of ActiveRecord::Base#dup to perform a deep clone that includes user specified associations. ".freeze
16
16
  s.email = "rjdelange@icloud.com".freeze
17
17
  s.extra_rdoc_files = [
@@ -19,6 +19,7 @@ Gem::Specification.new do |s|
19
19
  ]
20
20
  s.files = [
21
21
  ".document",
22
+ ".rubocop.yml",
22
23
  ".travis.yml",
23
24
  "Appraisals",
24
25
  "CHANGELOG.md",
@@ -1,73 +1,72 @@
1
- require "active_record"
1
+ require 'active_record'
2
2
 
3
3
  class ActiveRecord::Base
4
4
  module DeepCloneable
5
-
6
5
  # Deep dups an ActiveRecord model. See README.rdoc
7
- def deep_clone *args, &block
6
+ def deep_clone(*args, &block)
8
7
  options = args[0] || {}
9
8
 
10
- dict = options[:dictionary]
11
- dict ||= {} if options.delete(:use_dictionary)
9
+ dictionary = options[:dictionary]
10
+ dictionary ||= {} if options.delete(:use_dictionary)
12
11
 
13
- kopy = unless dict
14
- dup()
15
- else
16
- find_in_dict_or_dup(dict)
17
- end
12
+ kopy = if dictionary
13
+ find_in_dictionary_or_dup(dictionary)
14
+ else
15
+ dup
16
+ end
18
17
 
19
- block.call(self, kopy) if block
18
+ yield(self, kopy) if block
20
19
 
21
20
  deep_exceptions = {}
22
21
  if options[:except]
23
- exceptions = options[:except].nil? ? [] : [options[:except]].flatten
22
+ exceptions = array_wrap(options[:except])
24
23
  exceptions.each do |attribute|
25
- dup_default_attribute_value_to(kopy, attribute, self) unless attribute.kind_of?(Hash)
24
+ dup_default_attribute_value_to(kopy, attribute, self) unless attribute.is_a?(Hash)
26
25
  end
27
- deep_exceptions = exceptions.select{|e| e.kind_of?(Hash) }.inject({}){|m,h| m.merge(h) }
26
+ deep_exceptions = exceptions.select { |e| e.is_a?(Hash) }.inject({}) { |m, h| m.merge(h) }
28
27
  end
29
28
 
30
29
  deep_onlinesses = {}
31
30
  if options[:only]
32
- onlinesses = options[:only].nil? ? [] : [options[:only]].flatten
33
- object_attrs = kopy.attributes.keys.collect{ |s| s.to_sym }
31
+ onlinesses = array_wrap(options[:only])
32
+ object_attrs = kopy.attributes.keys.collect(&:to_sym)
34
33
  exceptions = object_attrs - onlinesses
35
34
  exceptions.each do |attribute|
36
- dup_default_attribute_value_to(kopy, attribute, self) unless attribute.kind_of?(Hash)
35
+ dup_default_attribute_value_to(kopy, attribute, self) unless attribute.is_a?(Hash)
37
36
  end
38
- deep_onlinesses = onlinesses.select{|e| e.kind_of?(Hash) }.inject({}){|m,h| m.merge(h) }
37
+ deep_onlinesses = onlinesses.select { |e| e.is_a?(Hash) }.inject({}) { |m, h| m.merge(h) }
39
38
  end
40
39
 
41
40
  if options[:include]
42
41
  normalized_includes_list(options[:include]).each do |association, conditions_or_deep_associations|
43
42
  conditions = {}
44
43
 
45
- if association.kind_of? Hash
44
+ if association.is_a? Hash
46
45
  conditions_or_deep_associations = association[association.keys.first]
47
46
  association = association.keys.first
48
47
  end
49
48
 
50
- if conditions_or_deep_associations.kind_of?(Hash)
49
+ if conditions_or_deep_associations.is_a?(Hash)
51
50
  conditions_or_deep_associations = conditions_or_deep_associations.dup
52
51
  conditions[:if] = conditions_or_deep_associations.delete(:if) if conditions_or_deep_associations[:if]
53
52
  conditions[:unless] = conditions_or_deep_associations.delete(:unless) if conditions_or_deep_associations[:unless]
54
- elsif conditions_or_deep_associations.kind_of?(Array)
53
+ elsif conditions_or_deep_associations.is_a?(Array)
55
54
  conditions_or_deep_associations = conditions_or_deep_associations.dup
56
- conditions_or_deep_associations.delete_if {|entry| conditions.merge!(entry) if entry.is_a?(Hash) && (entry.key?(:if) || entry.key?(:unless)) }
55
+ conditions_or_deep_associations.delete_if { |entry| conditions.merge!(entry) if entry.is_a?(Hash) && (entry.key?(:if) || entry.key?(:unless)) }
57
56
  end
58
57
 
59
58
  dup_options = {}
60
- dup_options.merge!(:include => conditions_or_deep_associations) if conditions_or_deep_associations.present?
61
- dup_options.merge!(:except => deep_exceptions[association]) if deep_exceptions[association]
62
- dup_options.merge!(:only => deep_onlinesses[association]) if deep_onlinesses[association]
63
- dup_options.merge!(:dictionary => dict) if dict
64
- dup_options.merge!(:skip_missing_associations => options[:skip_missing_associations]) if options[:skip_missing_associations]
59
+ dup_options[:include] = conditions_or_deep_associations if conditions_or_deep_associations.present?
60
+ dup_options[:except] = deep_exceptions[association] if deep_exceptions[association]
61
+ dup_options[:only] = deep_onlinesses[association] if deep_onlinesses[association]
62
+ dup_options[:dictionary] = dictionary if dictionary
63
+ dup_options[:skip_missing_associations] = options[:skip_missing_associations] if options[:skip_missing_associations]
65
64
 
66
- if association_reflection = self.class.reflect_on_association(association)
65
+ if (association_reflection = self.class.reflect_on_association(association))
67
66
  if options[:validate] == false
68
67
  kopy.instance_eval do
69
68
  # Force :validate => false on all saves.
70
- def perform_validations(options={})
69
+ def perform_validations(options = {})
71
70
  options[:validate] = false
72
71
  super(options)
73
72
  end
@@ -85,105 +84,112 @@ class ActiveRecord::Base
85
84
 
86
85
  kopy.send("#{association}=", duped_object)
87
86
  elsif !options[:skip_missing_associations]
88
- raise AssociationNotFoundException.new("#{self.class}##{association}")
87
+ raise AssociationNotFoundException, "#{self.class}##{association}"
89
88
  end
90
89
  end
91
90
  end
92
91
 
93
- return kopy
92
+ kopy
94
93
  end
95
94
 
96
- protected
95
+ protected
97
96
 
98
- def find_in_dict_or_dup(dict, dup_on_miss = true)
97
+ def find_in_dictionary_or_dup(dictionary, dup_on_miss = true)
99
98
  tableized_class = self.class.name.tableize.to_sym
100
- dict[tableized_class] ||= {}
101
- dict_val = dict[tableized_class][self]
102
- dict_val.nil? && dup_on_miss ? dict[tableized_class][self] = dup() : dict_val
99
+ dictionary[tableized_class] ||= {}
100
+ dict_val = dictionary[tableized_class][self]
101
+ dict_val.nil? && dup_on_miss ? dictionary[tableized_class][self] = dup : dict_val
103
102
  end
104
103
 
105
- private
106
-
107
- def dup_default_attribute_value_to(kopy, attribute, origin)
108
- kopy[attribute] = origin.class.column_defaults.dup[attribute.to_s]
109
- end
104
+ private
110
105
 
111
- def dup_belongs_to_association options, &block
112
- object = self.send(options[:association])
113
- object = nil if options[:conditions].any? && evaluate_conditions(object, options[:conditions])
106
+ def dup_belongs_to_association(options, &block)
107
+ object = deep_cloneable_object_for(options[:association], options[:conditions])
114
108
  object && object.deep_clone(options[:dup_options], &block)
115
109
  end
116
110
 
117
- def dup_has_one_association options, &block
111
+ def dup_has_one_association(options, &block)
118
112
  dup_belongs_to_association options, &block
119
113
  end
120
114
 
121
- def dup_has_many_association options, &block
122
- primary_key_name = options[:reflection].foreign_key.to_s
115
+ def dup_has_many_association(options, &block)
116
+ foreign_key = options[:reflection].foreign_key.to_s
117
+ reverse_association = find_reverse_association(options[:reflection], foreign_key, :belongs_to)
118
+ objects = deep_cloneable_objects_for(options[:association], options[:conditions])
123
119
 
124
- if options[:reflection].inverse_of.present?
125
- reverse_association_name = options[:reflection].inverse_of.name
126
- else
127
- reverse_association_name = options[:reflection].klass.reflect_on_all_associations.detect do |reflection|
128
- reflection.foreign_key.to_s == primary_key_name && reflection != options[:reflection]
129
- end.try(:name)
120
+ objects.map do |object|
121
+ object = object.deep_clone(options[:dup_options], &block)
122
+ object.send("#{foreign_key}=", nil)
123
+ object.send("#{reverse_association.name}=", options[:copy]) if reverse_association
124
+ object
130
125
  end
126
+ end
131
127
 
132
- objects = self.send(options[:association])
133
- objects = objects.select{|object| evaluate_conditions(object, options[:conditions]) } if options[:conditions].any?
128
+ def dup_has_one_through_association(options, &block)
129
+ foreign_key = options[:reflection].through_reflection.foreign_key.to_s
130
+ reverse_association = find_reverse_association(options[:reflection], foreign_key, :has_one, :association_foreign_key)
134
131
 
135
- objects.collect do |object|
136
- tmp = object.deep_clone(options[:dup_options], &block)
137
- tmp.send("#{primary_key_name}=", nil)
138
- tmp.send("#{reverse_association_name.to_s}=", options[:copy]) if reverse_association_name
139
- tmp
140
- end
132
+ object = deep_cloneable_object_for(options[:association], options[:conditions])
133
+ object && process_joined_object_for_deep_clone(object, options.merge(:reverse_association => reverse_association), &block)
141
134
  end
142
135
 
143
- def dup_has_many_through_association options, &block
144
- dup_join_association(
145
- options.merge(:macro => :has_many, :primary_key_name => options[:reflection].through_reflection.foreign_key.to_s),
146
- &block)
136
+ def dup_has_many_through_association(options, &block)
137
+ foreign_key = options[:reflection].through_reflection.foreign_key.to_s
138
+ reverse_association = find_reverse_association(options[:reflection], foreign_key, :has_many, :association_foreign_key)
139
+
140
+ objects = deep_cloneable_objects_for(options[:association], options[:conditions])
141
+ objects.map { |object| process_joined_object_for_deep_clone(object, options.merge(:reverse_association => reverse_association), &block) }
147
142
  end
148
143
 
149
- def dup_has_and_belongs_to_many_association options, &block
150
- dup_join_association(
151
- options.merge(:macro => :has_and_belongs_to_many, :primary_key_name => options[:reflection].foreign_key.to_s),
152
- &block)
144
+ def dup_has_and_belongs_to_many_association(options, &block)
145
+ foreign_key = options[:reflection].foreign_key.to_s
146
+ reverse_association = find_reverse_association(options[:reflection], foreign_key, :has_and_belongs_to_many, :association_foreign_key)
147
+
148
+ objects = deep_cloneable_objects_for(options[:association], options[:conditions])
149
+ objects.map { |object| process_joined_object_for_deep_clone(object, options.merge(:reverse_association => reverse_association), &block) }
153
150
  end
154
151
 
155
- def dup_join_association options, &block
156
- if options[:reflection].inverse_of.present?
157
- reverse_association_name = options[:reflection].inverse_of.name
152
+ def find_reverse_association(source_reflection, primary_key_name, macro, matcher = :foreign_key)
153
+ if source_reflection.inverse_of.present?
154
+ source_reflection.inverse_of
158
155
  else
159
- reverse_association_name = options[:reflection].klass.reflect_on_all_associations.detect do |reflection|
160
- (reflection.macro == options[:macro]) && (reflection.association_foreign_key.to_s == options[:primary_key_name])
161
- end.try(:name)
156
+ source_reflection.klass.reflect_on_all_associations.detect do |reflection|
157
+ reflection != source_reflection && (macro.nil? || reflection.macro == macro) && (reflection.send(matcher).to_s == primary_key_name)
158
+ end
162
159
  end
160
+ end
163
161
 
164
- objects = self.send(options[:association])
165
- objects = objects.select{|object| evaluate_conditions(object, options[:conditions]) } if options[:conditions].any?
162
+ def deep_cloneable_object_for(single_association, conditions)
163
+ object = send(single_association)
164
+ evaluate_conditions(object, conditions) && object
165
+ end
166
166
 
167
- objects.collect do |object|
168
- dict = options[:dup_options][:dictionary]
169
- if(dict && object.find_in_dict_or_dup(dict, false))
170
- object = object.deep_clone(options[:dup_options], &block)
171
- else
172
- object.send(reverse_association_name).target << options[:copy] if reverse_association_name
173
- end
174
- object
167
+ def deep_cloneable_objects_for(many_association, conditions)
168
+ send(many_association).select { |object| evaluate_conditions(object, conditions) }
169
+ end
170
+
171
+ def process_joined_object_for_deep_clone(object, options, &block)
172
+ if (dictionary = options[:dup_options][:dictionary]) && object.find_in_dictionary_or_dup(dictionary, false)
173
+ object = object.deep_clone(options[:dup_options], &block)
174
+ elsif options[:reverse_association]
175
+ object.send(options[:reverse_association].name).target << options[:copy]
175
176
  end
177
+ object
178
+ end
179
+
180
+ def evaluate_conditions(object, conditions)
181
+ conditions.none? || (conditions[:if] && conditions[:if].call(object)) || (conditions[:unless] && !conditions[:unless].call(object))
176
182
  end
177
183
 
178
- def evaluate_conditions object, conditions
179
- (conditions[:if] && conditions[:if].call(object)) || (conditions[:unless] && !conditions[:unless].call(object))
184
+ def dup_default_attribute_value_to(kopy, attribute, origin)
185
+ kopy[attribute] = origin.class.column_defaults.dup[attribute.to_s]
180
186
  end
181
187
 
182
- def normalized_includes_list includes
188
+ def normalized_includes_list(includes)
183
189
  list = []
184
190
  Array(includes).each do |item|
185
191
  if item.is_a?(Hash) && item.size > 1
186
- item.each{|key, value| list << { key => value } }
192
+ item.each { |key, value| list << { key => value } }
187
193
  else
188
194
  list << item
189
195
  end
@@ -192,6 +198,14 @@ class ActiveRecord::Base
192
198
  list
193
199
  end
194
200
 
201
+ def array_wrap(object)
202
+ if object.respond_to?(:to_ary)
203
+ object.to_ary || [object]
204
+ else
205
+ [object]
206
+ end
207
+ end
208
+
195
209
  class AssociationNotFoundException < StandardError; end
196
210
 
197
211
  ActiveRecord::Base.class_eval { protected :initialize_dup } if ActiveRecord::VERSION::MAJOR == 3 && ActiveRecord::VERSION::MINOR == 1
data/readme.md CHANGED
@@ -15,7 +15,7 @@ This gem gives every ActiveRecord::Base object the possibility to do a deep clon
15
15
  * Add deep_cloneable to your Gemfile:
16
16
 
17
17
  ```ruby
18
- gem 'deep_cloneable', '~> 2.3.2'
18
+ gem 'deep_cloneable', '~> 2.4.0'
19
19
  ```
20
20
 
21
21
  ## Upgrading from v1
@@ -114,7 +114,9 @@ end
114
114
 
115
115
  *Note*: Using `deep_clone` with a block will also pass the associated objects that are being cloned to the block, so be sure to check whether the object actually responds to your method of choice.
116
116
 
117
- ### Cloning models with files associated through Carrierwave
117
+ ### Cloning models with files
118
+
119
+ #### Carrierwave
118
120
 
119
121
  If you are cloning models that have associated files through Carrierwave these will not get transferred automatically. To overcome the issue you need to explicitly set the file attribute.
120
122
 
@@ -125,6 +127,35 @@ pirate.deep_clone include: :parrot do |original, kopy|
125
127
  end
126
128
  ```
127
129
 
130
+ #### ActiveStorage
131
+
132
+ For ActiveStorage, you have two options: you can either make a full copy, or share data blobs between two records.
133
+
134
+ ##### Full copy example
135
+
136
+ ```ruby
137
+ pirate.deep_clone include: :parrot do |original, kopy|
138
+ if kopy.is_a?(Pirate) && original.avatar.attached?
139
+ ActiveStorage::Downloader.new(original.avatar).download_blob_to_tempfile do |tempfile|
140
+ kopy.avatar.attach({
141
+ io: tempfile,
142
+ filename: original.avatar.blob.filename,
143
+ content_type: original.avatar.blob.content_type
144
+ })
145
+ end
146
+ end
147
+ end
148
+ ```
149
+
150
+
151
+ ##### Shallow copy example
152
+
153
+ ```ruby
154
+ pirate.deep_clone include: :parrot do |original, kopy|
155
+ kopy.avatar.attach(original.avatar.blob) if kopy.is_a?(Pirate) && original.avatar.attached?
156
+ end
157
+ ```
158
+
128
159
  ### Skipping missing associations
129
160
 
130
161
  By default, deep_cloneable will throw a `ActiveRecord::Base::DeepCloneable::AssociationNotFoundException` error when an association cannot be found. You can also skip missing associations by specifying `skip_missing_associations` if needed, for example when you have associations on some (but not all) subclasses of an STI model:
@@ -145,4 +176,4 @@ pirate.deep_clone include: [:parrot, :rum], skip_missing_associations: true
145
176
 
146
177
  ### Copyright
147
178
 
148
- Copyright &copy; 2018 Reinier de Lange. See LICENSE for details.
179
+ Copyright &copy; 2019 Reinier de Lange. See LICENSE for details.
@@ -26,14 +26,12 @@ module Animal
26
26
  has_many :birds
27
27
  end
28
28
 
29
-
30
29
  class Ownership < ActiveRecord::Base
31
30
  belongs_to :human
32
31
  belongs_to :chicken
33
32
 
34
33
  validates_uniqueness_of :chicken_id, :scope => :human_id
35
34
  end
36
-
37
35
  end
38
36
 
39
37
  class GoldPiece < ActiveRecord::Base; belongs_to :treasure end
@@ -89,7 +87,7 @@ class ChildWithValidation < ActiveRecord::Base
89
87
  end
90
88
 
91
89
  class ParentWithValidation < ActiveRecord::Base
92
- has_many :children, :class_name => 'ChildWithValidation'
90
+ has_many :children, :class_name => 'ChildWithValidation', :foreign_key => 'parent_id'
93
91
  validates :name, :presence => true
94
92
  end
95
93
 
@@ -126,6 +124,7 @@ end
126
124
  class Contractor < ActiveRecord::Base
127
125
  belongs_to :building
128
126
  has_and_belongs_to_many :apartments
127
+ has_one :contract
129
128
  end
130
129
 
131
130
  class User < ActiveRecord::Base
@@ -140,3 +139,13 @@ end
140
139
  class Product < ActiveRecord::Base
141
140
  belongs_to :order
142
141
  end
142
+
143
+ class Contract < ActiveRecord::Base
144
+ belongs_to :contractor
145
+ belongs_to :organization
146
+ end
147
+
148
+ class Organization < ActiveRecord::Base
149
+ has_one :contract
150
+ has_one :contractor, :through => :contract
151
+ end
@@ -90,7 +90,7 @@ ActiveRecord::Schema.define(:version => 1) do
90
90
 
91
91
  create_table :child_with_validations, :force => true do |t|
92
92
  t.column :name, :string
93
- t.column :parent_with_validation_id, :integer
93
+ t.column :parent_id, :integer
94
94
  end
95
95
 
96
96
  create_table :parts, :force => true do |t|
@@ -143,4 +143,14 @@ ActiveRecord::Schema.define(:version => 1) do
143
143
  t.column :name, :string
144
144
  t.column :order_id, :integer
145
145
  end
146
+
147
+ create_table :organizations, :force => true do |t|
148
+ t.column :name, :string
149
+ end
150
+
151
+ create_table :contracts, :force => true do |t|
152
+ t.column :number, :string
153
+ t.column :contractor_id, :integer
154
+ t.column :organization_id, :integer
155
+ end
146
156
  end
@@ -1,7 +1,6 @@
1
1
  require 'test_helper'
2
2
 
3
3
  class TestDeepCloneable < MiniTest::Unit::TestCase
4
-
5
4
  def setup
6
5
  @jack = Pirate.create(:name => 'Jack Sparrow', :nick_name => 'Captain Jack', :age => 30)
7
6
  @polly = Parrot.create(:name => 'Polly', :age => 2, :pirate => @jack)
@@ -84,7 +83,7 @@ class TestDeepCloneable < MiniTest::Unit::TestCase
84
83
  end
85
84
 
86
85
  def test_deep_include_association
87
- deep_clone = @jack.deep_clone(:include => {:treasures => :gold_pieces})
86
+ deep_clone = @jack.deep_clone(:include => { :treasures => :gold_pieces })
88
87
  assert deep_clone.new_record?
89
88
  assert deep_clone.save
90
89
  assert_equal 1, deep_clone.treasures.size
@@ -110,7 +109,7 @@ class TestDeepCloneable < MiniTest::Unit::TestCase
110
109
  end
111
110
 
112
111
  def test_multiple_and_deep_include_association
113
- deep_clone = @jack.deep_clone(:include => {:treasures => :gold_pieces, :mateys => {}})
112
+ deep_clone = @jack.deep_clone(:include => { :treasures => :gold_pieces, :mateys => {} })
114
113
  assert deep_clone.new_record?
115
114
  assert deep_clone.save
116
115
  assert_equal 1, deep_clone.treasures.size
@@ -119,7 +118,7 @@ class TestDeepCloneable < MiniTest::Unit::TestCase
119
118
  end
120
119
 
121
120
  def test_multiple_and_deep_include_association_with_array
122
- deep_clone = @jack.deep_clone(:include => [{:treasures => :gold_pieces}, :mateys])
121
+ deep_clone = @jack.deep_clone(:include => [{ :treasures => :gold_pieces }, :mateys])
123
122
  assert deep_clone.new_record?
124
123
  assert deep_clone.save
125
124
  assert_equal 1, deep_clone.treasures.size
@@ -128,7 +127,7 @@ class TestDeepCloneable < MiniTest::Unit::TestCase
128
127
  end
129
128
 
130
129
  def test_multiple_and_deep_include_association_with_array_and_multikey_hash
131
- deep_clone = @jack.deep_clone(:include => [:parrot, {:treasures => :gold_pieces, :mateys => {}}])
130
+ deep_clone = @jack.deep_clone(:include => [:parrot, { :treasures => :gold_pieces, :mateys => {} }])
132
131
  assert deep_clone.new_record?
133
132
  assert deep_clone.save
134
133
  assert_equal 1, deep_clone.treasures.size
@@ -153,6 +152,16 @@ class TestDeepCloneable < MiniTest::Unit::TestCase
153
152
  assert_nil deep_clone.parrot.name
154
153
  end
155
154
 
155
+ def test_should_pass_nested_exception_with_hash_value
156
+ deep_clone = @jack.deep_clone(:include => :parrot, :except => { :parrot => [:name] })
157
+ assert deep_clone.new_record?
158
+ assert deep_clone.save
159
+ refute_equal deep_clone.parrot, @jack.parrot
160
+ assert_equal deep_clone.parrot.age, @jack.parrot.age
161
+ refute_nil @jack.parrot.name
162
+ assert_nil deep_clone.parrot.name
163
+ end
164
+
156
165
  def test_should_pass_nested_onlinesses
157
166
  deep_clone = @jack.deep_clone(:include => :parrot, :only => [:name, { :parrot => [:name] }])
158
167
  assert deep_clone.new_record?
@@ -163,6 +172,16 @@ class TestDeepCloneable < MiniTest::Unit::TestCase
163
172
  assert_nil deep_clone.parrot.age
164
173
  end
165
174
 
175
+ def test_should_pass_nested_onliness_with_hash_value
176
+ deep_clone = @jack.deep_clone(:include => :parrot, :only => { :parrot => [:name] })
177
+ assert deep_clone.new_record?
178
+ assert deep_clone.piastres, []
179
+ assert deep_clone.save
180
+ refute_equal deep_clone.parrot, @jack.parrot
181
+ assert_equal deep_clone.parrot.name, @jack.parrot.name
182
+ assert_nil deep_clone.parrot.age
183
+ end
184
+
166
185
  def test_should_not_double_deep_clone_when_using_dictionary
167
186
  current_matey_count = Matey.count
168
187
  deep_clone = @jack.deep_clone(:include => [:mateys, { :treasures => :matey }], :use_dictionary => true)
@@ -176,7 +195,7 @@ class TestDeepCloneable < MiniTest::Unit::TestCase
176
195
  current_matey_count = Matey.count
177
196
 
178
197
  dict = { :mateys => {} }
179
- @jack.mateys.each{|m| dict[:mateys][m] = m.deep_clone }
198
+ @jack.mateys.each { |m| dict[:mateys][m] = m.deep_clone }
180
199
 
181
200
  deep_clone = @jack.deep_clone(:include => [:mateys, { :treasures => :matey }], :dictionary => dict)
182
201
  assert deep_clone.new_record?
@@ -186,7 +205,7 @@ class TestDeepCloneable < MiniTest::Unit::TestCase
186
205
  end
187
206
 
188
207
  def test_should_support_ar_class_under_module
189
- @human = Animal::Human.create :name => "Michael"
208
+ @human = Animal::Human.create :name => 'Michael'
190
209
  @pig = Animal::Pig.create :human => @human, :name => 'big pig'
191
210
 
192
211
  deep_clone_human = @human.deep_clone(:include => [:pigs])
@@ -194,18 +213,18 @@ class TestDeepCloneable < MiniTest::Unit::TestCase
194
213
  assert deep_clone_human.save
195
214
  assert_equal 1, deep_clone_human.pigs.count
196
215
 
197
- @human2 = Animal::Human.create :name => "John"
216
+ @human2 = Animal::Human.create :name => 'John'
198
217
  @pig2 = @human2.pigs.create :name => 'small pig'
199
218
 
200
- deep_clone_human_2 = @human.deep_clone(:include => [:pigs])
201
- assert deep_clone_human_2.new_record?
202
- assert deep_clone_human_2.save
203
- assert_equal 1, deep_clone_human_2.pigs.count
219
+ deep_clone_human2 = @human.deep_clone(:include => [:pigs])
220
+ assert deep_clone_human2.new_record?
221
+ assert deep_clone_human2.save
222
+ assert_equal 1, deep_clone_human2.pigs.count
204
223
  end
205
224
 
206
225
  def test_should_deep_clone_many_to_many_associations
207
- @human = Animal::Human.create :name => "Michael"
208
- @human2 = Animal::Human.create :name => "Jack"
226
+ @human = Animal::Human.create :name => 'Michael'
227
+ @human2 = Animal::Human.create :name => 'Jack'
209
228
  @chicken1 = Animal::Chicken.create :name => 'Chick1'
210
229
  @chicken2 = Animal::Chicken.create :name => 'Chick2'
211
230
  @human.chickens << [@chicken1, @chicken2]
@@ -219,8 +238,8 @@ class TestDeepCloneable < MiniTest::Unit::TestCase
219
238
 
220
239
  def test_should_skip_missing_associations
221
240
  @earth = Animal::Planet.create :name => 'Earth'
222
- @human = Animal::Human.create :name => "Michael"
223
- @chicken = Animal::Chicken.create :name => 'Chick', :humans => [@human], :planet => @earth, :humans => [@human]
241
+ @human = Animal::Human.create :name => 'Michael'
242
+ @chicken = Animal::Chicken.create :name => 'Chick', :humans => [@human], :planet => @earth
224
243
  @dove = Animal::Dove.create :name => 'Dovey', :planet => @earth
225
244
 
226
245
  assert_raises ActiveRecord::Base::DeepCloneable::AssociationNotFoundException do
@@ -229,7 +248,7 @@ class TestDeepCloneable < MiniTest::Unit::TestCase
229
248
 
230
249
  deep_clone_earth = @earth.deep_clone(:include => { :birds => :ownerships }, :skip_missing_associations => true)
231
250
  assert_equal 2, deep_clone_earth.birds.size
232
- assert deep_clone_earth.birds.detect{|bird| bird.is_a?(Animal::Chicken) }.ownerships.any?
251
+ assert deep_clone_earth.birds.detect { |bird| bird.is_a?(Animal::Chicken) }.ownerships.any?
233
252
  end
234
253
 
235
254
  def test_should_deep_clone_with_block
@@ -244,8 +263,8 @@ class TestDeepCloneable < MiniTest::Unit::TestCase
244
263
  end
245
264
 
246
265
  def test_should_deep_clone_habtm_associations
247
- @person1 = Person.create :name => "Bill"
248
- @person2 = Person.create :name => "Ted"
266
+ @person1 = Person.create :name => 'Bill'
267
+ @person2 = Person.create :name => 'Ted'
249
268
  @car1 = Car.create :name => 'Mustang'
250
269
  @car2 = Car.create :name => 'Camaro'
251
270
  @person1.cars << [@car1, @car2]
@@ -269,7 +288,7 @@ class TestDeepCloneable < MiniTest::Unit::TestCase
269
288
 
270
289
  def test_should_deep_clone_habtm_associations_with_missing_reverse_association
271
290
  @coin = Coin.create :value => 1
272
- @person = Person.create :name => "Bill"
291
+ @person = Person.create :name => 'Bill'
273
292
  @coin.people << @person
274
293
 
275
294
  deep_clone = @coin.deep_clone :include => :people
@@ -286,7 +305,7 @@ class TestDeepCloneable < MiniTest::Unit::TestCase
286
305
  deep_clone = student.deep_clone :include => { :student_assignments => :subject }
287
306
  deep_clone.save # Subjects will have been set after save
288
307
  assert_equal 2, deep_clone.subjects.size
289
- [subject1, subject2].each{|subject| assert !deep_clone.subjects.include?(subject) }
308
+ [subject1, subject2].each { |subject| assert !deep_clone.subjects.include?(subject) }
290
309
  end
291
310
 
292
311
  def test_parent_validations_run_on_save_after_clone
@@ -328,7 +347,7 @@ class TestDeepCloneable < MiniTest::Unit::TestCase
328
347
  assert deep_clone_parent.new_record?
329
348
  assert !deep_clone_parent.valid?
330
349
  assert !deep_clone_parent.children.first.valid?
331
- assert_equal deep_clone_parent.errors.messages, :children => ["is invalid"]
350
+ assert_equal deep_clone_parent.errors.messages, :children => ['is invalid']
332
351
  end
333
352
 
334
353
  def test_child_validations_run_on_save_after_clone_without_validation
@@ -342,13 +361,13 @@ class TestDeepCloneable < MiniTest::Unit::TestCase
342
361
  assert !deep_clone_parent.new_record?
343
362
  assert !deep_clone_parent.valid?
344
363
  assert !deep_clone_parent.children.first.valid?
345
- assert_equal deep_clone_parent.errors.messages, :children => ["is invalid"]
364
+ assert_equal deep_clone_parent.errors.messages, :children => ['is invalid']
346
365
  end
347
366
 
348
367
  def test_self_join_has_many
349
368
  parent_part = Part.create(:name => 'Parent')
350
- child1 = Part.create(:name => 'Child 1', :parent_part_id => parent_part.id)
351
- child2 = Part.create(:name => 'Child 2', :parent_part_id => parent_part.id)
369
+ Part.create(:name => 'Child 1', :parent_part_id => parent_part.id)
370
+ Part.create(:name => 'Child 2', :parent_part_id => parent_part.id)
352
371
 
353
372
  deep_clone_part = parent_part.deep_clone :include => :child_parts
354
373
  assert deep_clone_part.save
@@ -362,7 +381,16 @@ class TestDeepCloneable < MiniTest::Unit::TestCase
362
381
 
363
382
  deep_clone = student.deep_clone :include => :subjects
364
383
  assert_equal 2, deep_clone.subjects.size
365
- assert_equal [[student, deep_clone],[student, deep_clone]], deep_clone.subjects.map{|subject| subject.students }
384
+ assert_equal [[student, deep_clone], [student, deep_clone]], deep_clone.subjects.map(&:students)
385
+ end
386
+
387
+ def test_should_include_has_one_through_associations
388
+ organization = Organization.create(:name => 'organization')
389
+ contractor = Contractor.create(:name => 'contractor')
390
+ Contract.create(:number => 12_345, :contractor => contractor, :organization => organization)
391
+
392
+ deep_clone = organization.deep_clone(:include => :contractor)
393
+ assert_equal organization.contractor, deep_clone.contractor
366
394
  end
367
395
 
368
396
  def test_should_deep_clone_unsaved_objects
@@ -380,14 +408,14 @@ class TestDeepCloneable < MiniTest::Unit::TestCase
380
408
  subject2 = Subject.create(:name => 'subject 2')
381
409
  student = Student.create(:name => 'Parent', :subjects => [subject1, subject2])
382
410
 
383
- deep_clone = student.deep_clone :include => { :subjects => { :if => lambda{|subject| subject.name == 'subject 2' } } }
411
+ deep_clone = student.deep_clone :include => { :subjects => { :if => lambda { |subject| subject.name == 'subject 2' } } }
384
412
  assert_equal 1, deep_clone.subjects.size
385
413
  assert_equal 'subject 2', deep_clone.subjects.first.name
386
414
 
387
415
  deep_clone = @jack.deep_clone(:include => {
388
- :treasures => { :gold_pieces => { :unless => lambda{|piece| piece.is_a?(Parrot) } } },
389
- :mateys => { :if => lambda{|matey| matey.is_a?(GoldPiece) } }
390
- })
416
+ :treasures => { :gold_pieces => { :unless => lambda { |piece| piece.is_a?(Parrot) } } },
417
+ :mateys => { :if => lambda { |matey| matey.is_a?(GoldPiece) } }
418
+ })
391
419
 
392
420
  assert deep_clone.new_record?
393
421
  assert deep_clone.save
@@ -401,21 +429,21 @@ class TestDeepCloneable < MiniTest::Unit::TestCase
401
429
  subject2 = Subject.create(:name => 'subject 2')
402
430
  student = Student.create(:name => 'Parent', :subjects => [subject1, subject2])
403
431
 
404
- deep_clone = student.deep_clone(:include => [:subjects => [:if => lambda{|subject| false }] ])
432
+ deep_clone = student.deep_clone(:include => [:subjects => [:if => lambda { |_subject| false }]])
405
433
  assert deep_clone.subjects.none?
406
434
 
407
- deep_clone = student.deep_clone(:include => [:subjects => [:if => lambda{|subject| true }] ])
435
+ deep_clone = student.deep_clone(:include => [:subjects => [:if => lambda { |_subject| true }]])
408
436
  assert_equal 2, deep_clone.subjects.size
409
437
  end
410
438
 
411
439
  def test_should_reject_copies_if_conditionals_are_passed_with_associations
412
- deep_clone = @ship.deep_clone(:include => [:pirates => [:treasures, :mateys, { :unless => lambda {|pirate| pirate.name == 'Jack Sparrow'} }]])
440
+ deep_clone = @ship.deep_clone(:include => [:pirates => [:treasures, :mateys, { :unless => lambda { |pirate| pirate.name == 'Jack Sparrow' } }]])
413
441
 
414
442
  assert deep_clone.new_record?
415
443
  assert deep_clone.save
416
444
  assert_equal 0, deep_clone.pirates.size
417
445
 
418
- deep_clone = @ship.deep_clone(:include => [:pirates => [:treasures, :mateys, { :if => lambda {|pirate| pirate.name == 'Jack Sparrow'} }]])
446
+ deep_clone = @ship.deep_clone(:include => [:pirates => [:treasures, :mateys, { :if => lambda { |pirate| pirate.name == 'Jack Sparrow' } }]])
419
447
  assert deep_clone.new_record?
420
448
  assert deep_clone.save
421
449
  assert_equal 1, deep_clone.pirates.size
@@ -430,36 +458,36 @@ class TestDeepCloneable < MiniTest::Unit::TestCase
430
458
  @order2.products << [@product1, @product2]
431
459
  @user.orders << [@order1, @order2]
432
460
 
433
- deep_clone = @user.deep_clone(:include => [:orders => [:products => [{ :unless => lambda {|product| product.name == 'Ink' }}]]])
461
+ deep_clone = @user.deep_clone(:include => [:orders => [:products => [{ :unless => lambda { |product| product.name == 'Ink' } }]]])
434
462
 
435
463
  assert deep_clone.new_record?
436
464
  assert deep_clone.save
437
465
  assert_equal 1, deep_clone.orders.second.products.size
438
466
 
439
- deep_clone = @user.deep_clone(:include => [:orders => [:products => [{ :if => lambda {|product| product.name == 'Ink'}}]]])
467
+ deep_clone = @user.deep_clone(:include => [:orders => [:products => [{ :if => lambda { |product| product.name == 'Ink' } }]]])
440
468
  assert deep_clone.new_record?
441
469
  assert deep_clone.save
442
470
  assert_equal 1, deep_clone.orders.second.products.size
443
471
  end
444
472
 
445
473
  def test_should_find_in_dict_for_habtm
446
- apt = Apartment.create(:number => "101")
447
- contractor = Contractor.create(:name => "contractor", :apartments => [apt])
474
+ apt = Apartment.create(:number => '101')
475
+ contractor = Contractor.create(:name => 'contractor', :apartments => [apt])
448
476
 
449
477
  apt.contractors = [contractor]
450
478
  apt.save!
451
479
 
452
- building = Building.create(:name => "Tall Building", :contractors => [contractor], :apartments => [apt])
480
+ building = Building.create(:name => 'Tall Building', :contractors => [contractor], :apartments => [apt])
453
481
 
454
482
  deep_clone = building.deep_clone(:include => [
455
- :apartments,
456
- {
457
- :contractors => [
458
- :apartments
459
- ]
460
- }
461
- ],
462
- :use_dictionary => true)
483
+ :apartments,
484
+ {
485
+ :contractors => [
486
+ :apartments
487
+ ]
488
+ }
489
+ ],
490
+ :use_dictionary => true)
463
491
 
464
492
  deep_clone.save!
465
493
 
@@ -472,5 +500,4 @@ class TestDeepCloneable < MiniTest::Unit::TestCase
472
500
  assert_nil deep_clone.name
473
501
  refute deep_clone.name_changed?
474
502
  end
475
-
476
503
  end
@@ -13,24 +13,24 @@ require 'active_record'
13
13
  I18n.enforce_available_locales = true
14
14
 
15
15
  def load_schema
16
- config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
16
+ config = YAML.load(IO.read(File.dirname(__FILE__) + '/database.yml'))
17
17
 
18
- if defined?(ActiveSupport::BufferedLogger)
19
- ActiveRecord::Base.logger = ActiveSupport::BufferedLogger.new(File.dirname(__FILE__) + "/debug.log")
20
- else
21
- ActiveRecord::Base.logger = ActiveSupport::Logger.new(File.dirname(__FILE__) + "/debug.log")
22
- end
18
+ ActiveRecord::Base.logger = if defined?(ActiveSupport::BufferedLogger)
19
+ ActiveSupport::BufferedLogger.new(File.dirname(__FILE__) + '/debug.log')
20
+ else
21
+ ActiveSupport::Logger.new(File.dirname(__FILE__) + '/debug.log')
22
+ end
23
23
 
24
24
  db_adapter = ENV['DB']
25
25
  db_adapter ||= 'sqlite3'
26
26
 
27
27
  if db_adapter.nil?
28
- raise "No DB Adapter selected. Pass the DB= option to pick one, or install Sqlite or Sqlite3."
28
+ raise 'No DB Adapter selected. Pass the DB= option to pick one, or install Sqlite or Sqlite3.'
29
29
  end
30
30
  ActiveRecord::Base.establish_connection(config[db_adapter])
31
- load(File.dirname(__FILE__) + "/schema.rb")
31
+ load(File.dirname(__FILE__) + '/schema.rb')
32
32
  end
33
33
 
34
34
  load_schema
35
35
  require File.dirname(__FILE__) + '/../init.rb'
36
- require 'models'
36
+ require 'models'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: deep_cloneable
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.3.2
4
+ version: 2.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Reinier de Lange
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-04-11 00:00:00.000000000 Z
11
+ date: 2019-01-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -39,6 +39,7 @@ extra_rdoc_files:
39
39
  - LICENSE
40
40
  files:
41
41
  - ".document"
42
+ - ".rubocop.yml"
42
43
  - ".travis.yml"
43
44
  - Appraisals
44
45
  - CHANGELOG.md