deep_cloneable 2.3.2 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Appraisals +12 -14
- data/CHANGELOG.md +157 -1
- data/Gemfile +7 -11
- data/Gemfile.lock +7 -55
- data/LICENSE +1 -1
- data/Rakefile +5 -19
- data/deep_cloneable.gemspec +18 -70
- data/init.rb +3 -1
- data/lib/deep_cloneable/association_not_found_exception.rb +6 -0
- data/lib/deep_cloneable/deep_clone.rb +207 -0
- data/lib/deep_cloneable/skip_validations.rb +10 -0
- data/lib/deep_cloneable/version.rb +3 -0
- data/lib/deep_cloneable.rb +12 -195
- data/readme.md +113 -18
- metadata +15 -36
- data/.document +0 -5
- data/.travis.yml +0 -102
- data/VERSION +0 -1
- data/gemfiles/3.1.gemfile +0 -20
- data/gemfiles/3.1.gemfile.lock +0 -86
- data/gemfiles/3.2.gemfile +0 -17
- data/gemfiles/3.2.gemfile.lock +0 -86
- data/gemfiles/4.0.gemfile +0 -17
- data/gemfiles/4.0.gemfile.lock +0 -93
- data/gemfiles/4.1.gemfile +0 -17
- data/gemfiles/4.1.gemfile.lock +0 -93
- data/gemfiles/4.2.gemfile +0 -17
- data/gemfiles/4.2.gemfile.lock +0 -91
- data/gemfiles/5.0.gemfile +0 -16
- data/gemfiles/5.0.gemfile.lock +0 -91
- data/gemfiles/5.1.gemfile +0 -16
- data/gemfiles/5.1.gemfile.lock +0 -91
- data/gemfiles/5.2.gemfile +0 -15
- data/gemfiles/5.2.gemfile.lock +0 -91
- data/test/database.yml +0 -6
- data/test/models.rb +0 -142
- data/test/schema.rb +0 -146
- data/test/test_deep_cloneable.rb +0 -476
- data/test/test_helper.rb +0 -36
@@ -0,0 +1,207 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeepCloneable
|
4
|
+
module DeepClone
|
5
|
+
# Deep dups an ActiveRecord model. See README.rdoc
|
6
|
+
def deep_clone(*args, &block)
|
7
|
+
options = args[0] || {}
|
8
|
+
|
9
|
+
dictionary = options[:dictionary]
|
10
|
+
dictionary ||= {} if options[:use_dictionary]
|
11
|
+
|
12
|
+
kopy = if dictionary
|
13
|
+
find_in_dictionary_or_dup(dictionary)
|
14
|
+
else
|
15
|
+
dup
|
16
|
+
end
|
17
|
+
|
18
|
+
options[:preprocessor].call(self, kopy) if options.key?(:preprocessor)
|
19
|
+
|
20
|
+
deep_exceptions = {}
|
21
|
+
if options[:except]
|
22
|
+
exceptions = Array.wrap(options[:except])
|
23
|
+
exceptions.each do |attribute|
|
24
|
+
dup_default_attribute_value_to(kopy, attribute, self) unless attribute.is_a?(Hash)
|
25
|
+
end
|
26
|
+
deep_exceptions = exceptions.select { |e| e.is_a?(Hash) }.inject({}) { |m, h| m.merge(h) }
|
27
|
+
end
|
28
|
+
|
29
|
+
deep_onlinesses = {}
|
30
|
+
if options[:only]
|
31
|
+
onlinesses = Array.wrap(options[:only])
|
32
|
+
object_attrs = kopy.attributes.keys.collect(&:to_sym)
|
33
|
+
exceptions = object_attrs - onlinesses
|
34
|
+
exceptions.each do |attribute|
|
35
|
+
dup_default_attribute_value_to(kopy, attribute, self) unless attribute.is_a?(Hash)
|
36
|
+
end
|
37
|
+
deep_onlinesses = onlinesses.select { |e| e.is_a?(Hash) }.inject({}) { |m, h| m.merge(h) }
|
38
|
+
end
|
39
|
+
|
40
|
+
kopy.instance_eval { extend ::DeepCloneable::SkipValidations } if options[:validate] == false
|
41
|
+
|
42
|
+
if options[:include]
|
43
|
+
normalized_includes_list(options[:include]).each do |association, conditions_or_deep_associations|
|
44
|
+
conditions = {}
|
45
|
+
|
46
|
+
if association.is_a? Hash
|
47
|
+
conditions_or_deep_associations = association[association.keys.first]
|
48
|
+
association = association.keys.first
|
49
|
+
end
|
50
|
+
|
51
|
+
case conditions_or_deep_associations
|
52
|
+
when Hash
|
53
|
+
conditions_or_deep_associations = conditions_or_deep_associations.dup
|
54
|
+
conditions[:if] = conditions_or_deep_associations.delete(:if) if conditions_or_deep_associations[:if]
|
55
|
+
conditions[:unless] = conditions_or_deep_associations.delete(:unless) if conditions_or_deep_associations[:unless]
|
56
|
+
when Array
|
57
|
+
conditions_or_deep_associations = conditions_or_deep_associations.map { |entry| entry.is_a?(Hash) ? entry.dup : entry }
|
58
|
+
conditions_or_deep_associations.each_with_index do |entry, index|
|
59
|
+
if entry.is_a?(Hash)
|
60
|
+
conditions[:if] = entry.delete(:if) if entry[:if]
|
61
|
+
conditions[:unless] = entry.delete(:unless) if entry[:unless]
|
62
|
+
end
|
63
|
+
|
64
|
+
conditions_or_deep_associations.delete_at(index) if entry.empty?
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
dup_options = {}
|
69
|
+
dup_options[:include] = conditions_or_deep_associations if conditions_or_deep_associations.present?
|
70
|
+
dup_options[:except] = deep_exceptions[association] if deep_exceptions[association]
|
71
|
+
dup_options[:only] = deep_onlinesses[association] if deep_onlinesses[association]
|
72
|
+
dup_options[:dictionary] = dictionary if dictionary
|
73
|
+
|
74
|
+
[:skip_missing_associations, :validate, :preprocessor, :postprocessor].each do |option|
|
75
|
+
dup_options[option] = options[option] if options.key?(option)
|
76
|
+
end
|
77
|
+
|
78
|
+
if (association_reflection = self.class.reflect_on_association(association))
|
79
|
+
association_type = association_reflection.macro
|
80
|
+
association_type = "#{association_type}_through" if association_reflection.is_a?(ActiveRecord::Reflection::ThroughReflection)
|
81
|
+
|
82
|
+
duped_object = send(
|
83
|
+
"dup_#{association_type}_association",
|
84
|
+
{ :reflection => association_reflection, :association => association, :copy => kopy, :conditions => conditions, :dup_options => dup_options },
|
85
|
+
&block
|
86
|
+
)
|
87
|
+
|
88
|
+
kopy.send("#{association}=", duped_object)
|
89
|
+
elsif !options[:skip_missing_associations]
|
90
|
+
raise ::DeepCloneable::AssociationNotFoundException, "#{self.class}##{association}"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
yield(self, kopy) if block
|
96
|
+
options[:postprocessor].call(self, kopy) if options.key?(:postprocessor)
|
97
|
+
|
98
|
+
kopy
|
99
|
+
end
|
100
|
+
|
101
|
+
protected
|
102
|
+
|
103
|
+
def find_in_dictionary_or_dup(dictionary, dup_on_miss = true)
|
104
|
+
tableized_class = self.class.name.tableize.to_sym
|
105
|
+
dictionary[tableized_class] ||= {}
|
106
|
+
dict_val = dictionary[tableized_class][self]
|
107
|
+
dict_val.nil? && dup_on_miss ? dictionary[tableized_class][self] = dup : dict_val
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
def dup_belongs_to_association(options, &block)
|
113
|
+
object = deep_cloneable_object_for(options[:association], options[:conditions])
|
114
|
+
object && object.deep_clone(options[:dup_options], &block)
|
115
|
+
end
|
116
|
+
|
117
|
+
def dup_has_one_association(options, &block)
|
118
|
+
dup_belongs_to_association options, &block
|
119
|
+
end
|
120
|
+
|
121
|
+
def dup_has_many_association(options, &block)
|
122
|
+
foreign_key = options[:reflection].foreign_key.to_s
|
123
|
+
reverse_association = find_reverse_association(options[:reflection], foreign_key, :belongs_to)
|
124
|
+
objects = deep_cloneable_objects_for(options[:association], options[:conditions])
|
125
|
+
|
126
|
+
objects.map do |object|
|
127
|
+
object = object.deep_clone(options[:dup_options], &block)
|
128
|
+
object.send("#{foreign_key}=", nil)
|
129
|
+
object.send("#{reverse_association.name}=", options[:copy]) if reverse_association
|
130
|
+
object
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def dup_has_one_through_association(options, &block)
|
135
|
+
foreign_key = options[:reflection].through_reflection.foreign_key.to_s
|
136
|
+
reverse_association = find_reverse_association(options[:reflection], foreign_key, :has_one, :association_foreign_key)
|
137
|
+
|
138
|
+
object = deep_cloneable_object_for(options[:association], options[:conditions])
|
139
|
+
object && process_joined_object_for_deep_clone(object, options.merge(:reverse_association => reverse_association), &block)
|
140
|
+
end
|
141
|
+
|
142
|
+
def dup_has_many_through_association(options, &block)
|
143
|
+
foreign_key = options[:reflection].through_reflection.foreign_key.to_s
|
144
|
+
reverse_association = find_reverse_association(options[:reflection], foreign_key, :has_many, :association_foreign_key)
|
145
|
+
|
146
|
+
objects = deep_cloneable_objects_for(options[:association], options[:conditions])
|
147
|
+
objects.map { |object| process_joined_object_for_deep_clone(object, options.merge(:reverse_association => reverse_association), &block) }
|
148
|
+
end
|
149
|
+
|
150
|
+
def dup_has_and_belongs_to_many_association(options, &block)
|
151
|
+
foreign_key = options[:reflection].foreign_key.to_s
|
152
|
+
reverse_association = find_reverse_association(options[:reflection], foreign_key, :has_and_belongs_to_many, :association_foreign_key)
|
153
|
+
|
154
|
+
objects = deep_cloneable_objects_for(options[:association], options[:conditions])
|
155
|
+
objects.map { |object| process_joined_object_for_deep_clone(object, options.merge(:reverse_association => reverse_association), &block) }
|
156
|
+
end
|
157
|
+
|
158
|
+
def find_reverse_association(source_reflection, primary_key_name, macro, matcher = :foreign_key)
|
159
|
+
if source_reflection.inverse_of.present?
|
160
|
+
source_reflection.inverse_of
|
161
|
+
else
|
162
|
+
source_reflection.klass.reflect_on_all_associations.detect do |reflection|
|
163
|
+
reflection != source_reflection && (macro.nil? || reflection.macro == macro) && (reflection.send(matcher).to_s == primary_key_name)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def deep_cloneable_object_for(single_association, conditions)
|
169
|
+
object = send(single_association)
|
170
|
+
evaluate_conditions(object, conditions) ? object : nil
|
171
|
+
end
|
172
|
+
|
173
|
+
def deep_cloneable_objects_for(many_association, conditions)
|
174
|
+
send(many_association).select { |object| evaluate_conditions(object, conditions) }
|
175
|
+
end
|
176
|
+
|
177
|
+
def process_joined_object_for_deep_clone(object, options, &block)
|
178
|
+
if (dictionary = options[:dup_options][:dictionary]) && object.find_in_dictionary_or_dup(dictionary, false)
|
179
|
+
object = object.deep_clone(options[:dup_options], &block)
|
180
|
+
elsif options[:reverse_association]
|
181
|
+
object.send(options[:reverse_association].name).target << options[:copy]
|
182
|
+
end
|
183
|
+
object
|
184
|
+
end
|
185
|
+
|
186
|
+
def evaluate_conditions(object, conditions)
|
187
|
+
conditions.none? || (conditions[:if] && conditions[:if].call(object)) || (conditions[:unless] && !conditions[:unless].call(object))
|
188
|
+
end
|
189
|
+
|
190
|
+
def dup_default_attribute_value_to(kopy, attribute, origin)
|
191
|
+
kopy[attribute] = origin.class.column_defaults.dup[attribute.to_s]
|
192
|
+
end
|
193
|
+
|
194
|
+
def normalized_includes_list(includes)
|
195
|
+
list = []
|
196
|
+
Array(includes).each do |item|
|
197
|
+
if item.is_a?(Hash) && item.size > 1
|
198
|
+
item.each { |key, value| list << { key => value } }
|
199
|
+
else
|
200
|
+
list << item
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
list
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
data/lib/deep_cloneable.rb
CHANGED
@@ -1,201 +1,18 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
require 'active_record'
|
4
|
+
require 'active_support/lazy_load_hooks'
|
5
|
+
require 'active_support/core_ext/array/wrap'
|
5
6
|
|
6
|
-
|
7
|
-
|
8
|
-
|
7
|
+
require 'deep_cloneable/association_not_found_exception'
|
8
|
+
require 'deep_cloneable/skip_validations'
|
9
|
+
require 'deep_cloneable/deep_clone'
|
9
10
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
kopy = unless dict
|
14
|
-
dup()
|
15
|
-
else
|
16
|
-
find_in_dict_or_dup(dict)
|
17
|
-
end
|
18
|
-
|
19
|
-
block.call(self, kopy) if block
|
20
|
-
|
21
|
-
deep_exceptions = {}
|
22
|
-
if options[:except]
|
23
|
-
exceptions = options[:except].nil? ? [] : [options[:except]].flatten
|
24
|
-
exceptions.each do |attribute|
|
25
|
-
dup_default_attribute_value_to(kopy, attribute, self) unless attribute.kind_of?(Hash)
|
26
|
-
end
|
27
|
-
deep_exceptions = exceptions.select{|e| e.kind_of?(Hash) }.inject({}){|m,h| m.merge(h) }
|
28
|
-
end
|
29
|
-
|
30
|
-
deep_onlinesses = {}
|
31
|
-
if options[:only]
|
32
|
-
onlinesses = options[:only].nil? ? [] : [options[:only]].flatten
|
33
|
-
object_attrs = kopy.attributes.keys.collect{ |s| s.to_sym }
|
34
|
-
exceptions = object_attrs - onlinesses
|
35
|
-
exceptions.each do |attribute|
|
36
|
-
dup_default_attribute_value_to(kopy, attribute, self) unless attribute.kind_of?(Hash)
|
37
|
-
end
|
38
|
-
deep_onlinesses = onlinesses.select{|e| e.kind_of?(Hash) }.inject({}){|m,h| m.merge(h) }
|
39
|
-
end
|
40
|
-
|
41
|
-
if options[:include]
|
42
|
-
normalized_includes_list(options[:include]).each do |association, conditions_or_deep_associations|
|
43
|
-
conditions = {}
|
44
|
-
|
45
|
-
if association.kind_of? Hash
|
46
|
-
conditions_or_deep_associations = association[association.keys.first]
|
47
|
-
association = association.keys.first
|
48
|
-
end
|
49
|
-
|
50
|
-
if conditions_or_deep_associations.kind_of?(Hash)
|
51
|
-
conditions_or_deep_associations = conditions_or_deep_associations.dup
|
52
|
-
conditions[:if] = conditions_or_deep_associations.delete(:if) if conditions_or_deep_associations[:if]
|
53
|
-
conditions[:unless] = conditions_or_deep_associations.delete(:unless) if conditions_or_deep_associations[:unless]
|
54
|
-
elsif conditions_or_deep_associations.kind_of?(Array)
|
55
|
-
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)) }
|
57
|
-
end
|
58
|
-
|
59
|
-
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]
|
65
|
-
|
66
|
-
if association_reflection = self.class.reflect_on_association(association)
|
67
|
-
if options[:validate] == false
|
68
|
-
kopy.instance_eval do
|
69
|
-
# Force :validate => false on all saves.
|
70
|
-
def perform_validations(options={})
|
71
|
-
options[:validate] = false
|
72
|
-
super(options)
|
73
|
-
end
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
association_type = association_reflection.macro
|
78
|
-
association_type = "#{association_type}_through" if association_reflection.is_a?(ActiveRecord::Reflection::ThroughReflection)
|
79
|
-
|
80
|
-
duped_object = send(
|
81
|
-
"dup_#{association_type}_association",
|
82
|
-
{ :reflection => association_reflection, :association => association, :copy => kopy, :conditions => conditions, :dup_options => dup_options },
|
83
|
-
&block
|
84
|
-
)
|
85
|
-
|
86
|
-
kopy.send("#{association}=", duped_object)
|
87
|
-
elsif !options[:skip_missing_associations]
|
88
|
-
raise AssociationNotFoundException.new("#{self.class}##{association}")
|
89
|
-
end
|
90
|
-
end
|
91
|
-
end
|
92
|
-
|
93
|
-
return kopy
|
94
|
-
end
|
95
|
-
|
96
|
-
protected
|
97
|
-
|
98
|
-
def find_in_dict_or_dup(dict, dup_on_miss = true)
|
99
|
-
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
|
103
|
-
end
|
104
|
-
|
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
|
110
|
-
|
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])
|
114
|
-
object && object.deep_clone(options[:dup_options], &block)
|
115
|
-
end
|
116
|
-
|
117
|
-
def dup_has_one_association options, &block
|
118
|
-
dup_belongs_to_association options, &block
|
119
|
-
end
|
120
|
-
|
121
|
-
def dup_has_many_association options, &block
|
122
|
-
primary_key_name = options[:reflection].foreign_key.to_s
|
123
|
-
|
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)
|
130
|
-
end
|
131
|
-
|
132
|
-
objects = self.send(options[:association])
|
133
|
-
objects = objects.select{|object| evaluate_conditions(object, options[:conditions]) } if options[:conditions].any?
|
134
|
-
|
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
|
141
|
-
end
|
142
|
-
|
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)
|
147
|
-
end
|
148
|
-
|
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)
|
153
|
-
end
|
154
|
-
|
155
|
-
def dup_join_association options, &block
|
156
|
-
if options[:reflection].inverse_of.present?
|
157
|
-
reverse_association_name = options[:reflection].inverse_of.name
|
158
|
-
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)
|
162
|
-
end
|
163
|
-
|
164
|
-
objects = self.send(options[:association])
|
165
|
-
objects = objects.select{|object| evaluate_conditions(object, options[:conditions]) } if options[:conditions].any?
|
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
|
175
|
-
end
|
176
|
-
end
|
177
|
-
|
178
|
-
def evaluate_conditions object, conditions
|
179
|
-
(conditions[:if] && conditions[:if].call(object)) || (conditions[:unless] && !conditions[:unless].call(object))
|
180
|
-
end
|
181
|
-
|
182
|
-
def normalized_includes_list includes
|
183
|
-
list = []
|
184
|
-
Array(includes).each do |item|
|
185
|
-
if item.is_a?(Hash) && item.size > 1
|
186
|
-
item.each{|key, value| list << { key => value } }
|
187
|
-
else
|
188
|
-
list << item
|
189
|
-
end
|
190
|
-
end
|
191
|
-
|
192
|
-
list
|
193
|
-
end
|
194
|
-
|
195
|
-
class AssociationNotFoundException < StandardError; end
|
11
|
+
module DeepCloneable
|
12
|
+
end
|
196
13
|
|
197
|
-
|
198
|
-
|
14
|
+
ActiveSupport.on_load :active_record do
|
15
|
+
protected :initialize_dup if ActiveRecord::VERSION::MAJOR == 3 && ActiveRecord::VERSION::MINOR == 1
|
199
16
|
|
200
|
-
include DeepCloneable
|
17
|
+
include DeepCloneable::DeepClone
|
201
18
|
end
|
data/readme.md
CHANGED
@@ -1,30 +1,40 @@
|
|
1
1
|
# deep_cloneable
|
2
2
|
|
3
|
-
|
3
|
+

|
4
4
|
|
5
5
|
This gem gives every ActiveRecord::Base object the possibility to do a deep clone that includes user specified associations. It is a rails 3+ upgrade of the [deep_cloning plugin](http://github.com/openminds/deep_cloning).
|
6
6
|
|
7
7
|
## Requirements
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
- Ruby 2.3.0, 2.4.4, 2.5.5, 2.6.3, 2.7.5 (tested)
|
10
|
+
- TruffleRuby 21.3.0
|
11
|
+
- Activerecord 3.2, 4.0, 4.1, 4.2, 5.0, 5.1, 5.2, 6.0, 7.0 (tested)
|
12
|
+
- Rails 2.x/3.0 users, please check out the 'rails2.x-3.0' branch
|
12
13
|
|
13
14
|
## Installation
|
14
15
|
|
15
|
-
|
16
|
+
- Add deep_cloneable to your Gemfile:
|
16
17
|
|
17
18
|
```ruby
|
18
|
-
gem 'deep_cloneable', '~>
|
19
|
+
gem 'deep_cloneable', '~> 3.2.0'
|
19
20
|
```
|
20
21
|
|
21
|
-
##
|
22
|
+
## Upgrade details
|
23
|
+
|
24
|
+
### Upgrading from v2
|
25
|
+
|
26
|
+
There are two breaking changes that you might need to pay attention to:
|
27
|
+
|
28
|
+
- When using an optional block (see below), the block used to be evaluated before `deep_cloneable` had performed its changes (inclusions, exclusions, includes). In v3, the block is evaluated after all processing has been done, just before the copy is about to be returned.
|
29
|
+
- When a defined association is not found, `deep_cloneable` raises an exception. The exception class has changed namespace: the class definition used to be `ActiveRecord::Base::DeepCloneable::AssociationNotFoundException` and this has changed to `DeepCloneable::AssociationNotFoundException`.
|
30
|
+
|
31
|
+
### Upgrading from v1
|
22
32
|
|
23
33
|
The `dup` method with arguments has been replaced in deep_cloneable 2 by the method `deep_clone`. Please update your sources accordingly.
|
24
34
|
|
25
35
|
## Usage
|
26
36
|
|
27
|
-
The `deep_clone` method supports a couple options that can be specified by passing an options hash. Without options, the behaviour is the same as ActiveRecord's
|
37
|
+
The `deep_clone` method supports a couple options that can be specified by passing an options hash. Without options, the behaviour is the same as ActiveRecord's [`dup`](http://apidock.com/rails/ActiveRecord/Core/dup) method.
|
28
38
|
|
29
39
|
### Association inclusion
|
30
40
|
|
@@ -76,6 +86,7 @@ pirate.deep_clone include: [ :mateys, { treasures: [ :matey, :gold_pieces ] } ],
|
|
76
86
|
The `deep_clone` method supports both `except` and `only` for specifying which attributes should be duped:
|
77
87
|
|
78
88
|
#### Exceptions
|
89
|
+
|
79
90
|
```ruby
|
80
91
|
# Single exception
|
81
92
|
pirate.deep_clone except: :name
|
@@ -88,6 +99,7 @@ pirate.deep_clone include: :parrot, except: [ :name, { parrot: [ :name ] } ]
|
|
88
99
|
```
|
89
100
|
|
90
101
|
#### Inclusions
|
102
|
+
|
91
103
|
```ruby
|
92
104
|
# Single attribute inclusion
|
93
105
|
pirate.deep_clone only: :name
|
@@ -100,6 +112,19 @@ pirate.deep_clone include: :parrot, only: [ :name, { parrot: [ :name ] } ]
|
|
100
112
|
|
101
113
|
```
|
102
114
|
|
115
|
+
### Pre- and postprocessor
|
116
|
+
|
117
|
+
You can specify a pre- and/or a postprocessor to modify a duped object after duplication:
|
118
|
+
|
119
|
+
```ruby
|
120
|
+
pirate.deep_clone(include: :parrot, preprocessor: ->(original, kopy) { kopy.cloned_from_id = original.id if kopy.respond_to?(:cloned_from_id) })
|
121
|
+
pirate.deep_clone(include: :parrot, postprocessor: ->(original, kopy) { kopy.cloned_from_id = original.id if kopy.respond_to?(:cloned_from_id) })
|
122
|
+
```
|
123
|
+
|
124
|
+
_Note_: Specifying a postprocessor is essentially the same as specifying an optional block (see below).
|
125
|
+
|
126
|
+
_Note_: Using `deep_clone` with a processors will pass all associated objects that are being cloned to the processor, so be sure to check whether the object actually responds to your method of choice.
|
127
|
+
|
103
128
|
### Optional Block
|
104
129
|
|
105
130
|
Pass a block to `deep_clone` to modify a duped object after duplication:
|
@@ -110,24 +135,94 @@ pirate.deep_clone include: :parrot do |original, kopy|
|
|
110
135
|
end
|
111
136
|
```
|
112
137
|
|
113
|
-
|
138
|
+
_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.
|
114
139
|
|
115
|
-
|
140
|
+
### Cloning models with files
|
116
141
|
|
117
|
-
|
142
|
+
#### Carrierwave
|
118
143
|
|
119
144
|
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
145
|
|
121
146
|
Easiest solution is to add the code in a clone block as described above.
|
147
|
+
|
122
148
|
```ruby
|
123
149
|
pirate.deep_clone include: :parrot do |original, kopy|
|
124
150
|
kopy.thumbnail = original.thumbnail
|
125
151
|
end
|
126
152
|
```
|
127
153
|
|
154
|
+
#### ActiveStorage
|
155
|
+
|
156
|
+
For ActiveStorage, you have two options: you can either make a full copy, or share data blobs between two records.
|
157
|
+
|
158
|
+
##### Full copy example
|
159
|
+
|
160
|
+
```ruby
|
161
|
+
# Rails 5.2, has_one_attached example 1
|
162
|
+
pirate.deep_clone include: [:parrot, :avatar_attachment, :avatar_blob]
|
163
|
+
|
164
|
+
# Rails 5.2, has_one_attached example 2
|
165
|
+
pirate.deep_clone include: :parrot do |original, kopy|
|
166
|
+
if kopy.is_a?(Pirate) && original.avatar.attached?
|
167
|
+
attachment = original.avatar
|
168
|
+
kopy.avatar.attach \
|
169
|
+
:io => StringIO.new(attachment.download),
|
170
|
+
:filename => attachment.filename,
|
171
|
+
:content_type => attachment.content_type
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
# Rails 5.2, has_many_attached example 1 (attach one by one)
|
176
|
+
pirate.deep_clone include: :parrot do |original, kopy|
|
177
|
+
if kopy.is_a?(Pirate) && original.crew_members_images.attached?
|
178
|
+
original.crew_members_images.each do |attachment|
|
179
|
+
kopy.crew_members_images.attach \
|
180
|
+
:io => StringIO.new(attachment.download),
|
181
|
+
:filename => attachment.filename,
|
182
|
+
:content_type => attachment.content_type
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
# Rails 5.2, has_many_attached example 2 (attach bulk)
|
188
|
+
pirate.deep_clone include: :parrot do |original, kopy|
|
189
|
+
if kopy.is_a?(Pirate) && original.crew_members_images.attached?
|
190
|
+
all_attachments_arr = original.crew_members_images.map do |attachment|
|
191
|
+
{
|
192
|
+
:io => StringIO.new(attachment.download),
|
193
|
+
:filename => attachment.filename,
|
194
|
+
:content_type => attachment.content_type
|
195
|
+
}
|
196
|
+
end
|
197
|
+
kopy.crew_members_images.attach(all_attachments_arr) # attach all at once
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
# Rails 6
|
202
|
+
pirate.deep_clone include: :parrot do |original, kopy|
|
203
|
+
if kopy.is_a?(Pirate) && original.avatar.attached?
|
204
|
+
original.avatar.open do |tempfile|
|
205
|
+
kopy.avatar.attach({
|
206
|
+
io: File.open(tempfile.path),
|
207
|
+
filename: original.avatar.blob.filename,
|
208
|
+
content_type: original.avatar.blob.content_type
|
209
|
+
})
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
```
|
214
|
+
|
215
|
+
##### Shallow copy example
|
216
|
+
|
217
|
+
```ruby
|
218
|
+
pirate.deep_clone include: :parrot do |original, kopy|
|
219
|
+
kopy.avatar.attach(original.avatar.blob) if kopy.is_a?(Pirate) && original.avatar.attached?
|
220
|
+
end
|
221
|
+
```
|
222
|
+
|
128
223
|
### Skipping missing associations
|
129
224
|
|
130
|
-
By default, deep_cloneable will throw a `
|
225
|
+
By default, deep_cloneable will throw a `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:
|
131
226
|
|
132
227
|
```ruby
|
133
228
|
pirate.deep_clone include: [:parrot, :rum], skip_missing_associations: true
|
@@ -135,14 +230,14 @@ pirate.deep_clone include: [:parrot, :rum], skip_missing_associations: true
|
|
135
230
|
|
136
231
|
### Note on Patches/Pull Requests
|
137
232
|
|
138
|
-
|
139
|
-
|
140
|
-
|
233
|
+
- Fork the project.
|
234
|
+
- Make your feature addition or bug fix.
|
235
|
+
- Add tests for it. This is important so I don't break it in a
|
141
236
|
future version unintentionally.
|
142
|
-
|
237
|
+
- Commit, do not mess with rakefile, version, or history.
|
143
238
|
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
144
|
-
|
239
|
+
- Send me a pull request. Bonus points for topic branches.
|
145
240
|
|
146
241
|
### Copyright
|
147
242
|
|
148
|
-
Copyright ©
|
243
|
+
Copyright © 2021 Reinier de Lange. See LICENSE for details.
|