deep_cloneable 2.3.2 → 3.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeepCloneable
4
+ module SkipValidations
5
+ def perform_validations(options = {})
6
+ options[:validate] = false
7
+ super(options)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,3 @@
1
+ module DeepCloneable
2
+ VERSION = '3.2.0'
3
+ end
@@ -1,201 +1,18 @@
1
- require "active_record"
1
+ # frozen_string_literal: true
2
2
 
3
- class ActiveRecord::Base
4
- module DeepCloneable
3
+ require 'active_record'
4
+ require 'active_support/lazy_load_hooks'
5
+ require 'active_support/core_ext/array/wrap'
5
6
 
6
- # Deep dups an ActiveRecord model. See README.rdoc
7
- def deep_clone *args, &block
8
- options = args[0] || {}
7
+ require 'deep_cloneable/association_not_found_exception'
8
+ require 'deep_cloneable/skip_validations'
9
+ require 'deep_cloneable/deep_clone'
9
10
 
10
- dict = options[:dictionary]
11
- dict ||= {} if options.delete(:use_dictionary)
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
- ActiveRecord::Base.class_eval { protected :initialize_dup } if ActiveRecord::VERSION::MAJOR == 3 && ActiveRecord::VERSION::MINOR == 1
198
- end
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
- [![Build Status](https://travis-ci.org/moiristo/deep_cloneable.svg?branch=master)](https://travis-ci.org/moiristo/deep_cloneable)
3
+ ![Build Status](https://github.com/moiristo/deep_cloneable/actions/workflows/ruby.yml/badge.svg)
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
- * Ruby 1.8.7, 1.9.2, 1.9.3, 2.0.0, 2.1.5, 2.2.2, 2.3.0, 2.4.4, 2.5.1 (tested)
10
- * Activerecord 3.1, 3.2, 4.0, 4.1, 4.2, 5.0, 5.1, 5.2 (tested)
11
- * Rails 2.x/3.0 users, please check out the 'rails2.x-3.0' branch
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
- * Add deep_cloneable to your Gemfile:
16
+ - Add deep_cloneable to your Gemfile:
16
17
 
17
18
  ```ruby
18
- gem 'deep_cloneable', '~> 2.3.2'
19
+ gem 'deep_cloneable', '~> 3.2.0'
19
20
  ```
20
21
 
21
- ## Upgrading from v1
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 [`dup`](http://apidock.com/rails/ActiveRecord/Core/dup) method.
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
- *Note*: The block is invoked before attributes are excluded.
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
- *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.
140
+ ### Cloning models with files
116
141
 
117
- ### Cloning models with files associated through Carrierwave
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 `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:
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
- * Fork the project.
139
- * Make your feature addition or bug fix.
140
- * Add tests for it. This is important so I don't break it in a
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
- * Commit, do not mess with rakefile, version, or history.
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
- * Send me a pull request. Bonus points for topic branches.
239
+ - Send me a pull request. Bonus points for topic branches.
145
240
 
146
241
  ### Copyright
147
242
 
148
- Copyright &copy; 2018 Reinier de Lange. See LICENSE for details.
243
+ Copyright &copy; 2021 Reinier de Lange. See LICENSE for details.