deep_cloneable 1.4.0 → 3.2.1
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 +7 -0
- data/Appraisals +57 -0
- data/CHANGELOG.md +350 -0
- data/Gemfile +8 -7
- data/Gemfile.lock +51 -91
- data/LICENSE +1 -1
- data/Rakefile +8 -33
- data/deep_cloneable.gemspec +18 -55
- 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 +13 -124
- data/readme.md +242 -0
- metadata +33 -67
- data/.document +0 -5
- data/README.rdoc +0 -78
- data/VERSION +0 -1
- data/test/database.yml +0 -6
- data/test/schema.rb +0 -52
- data/test/test_deep_cloneable.rb +0 -162
- data/test/test_helper.rb +0 -79
data/deep_cloneable.gemspec
CHANGED
@@ -1,58 +1,21 @@
|
|
1
|
-
#
|
2
|
-
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
-
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
|
-
# -*- encoding: utf-8 -*-
|
1
|
+
# frozen_string_literal: true
|
5
2
|
|
6
|
-
|
7
|
-
|
8
|
-
s.version = "1.4.0"
|
9
|
-
|
10
|
-
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
-
s.authors = ["Reinier de Lange"]
|
12
|
-
s.date = "2012-04-02"
|
13
|
-
s.description = "Extends the functionality of ActiveRecord::Base#clone to perform a deep clone that includes user specified associations. "
|
14
|
-
s.email = "r.j.delange@nedforce.nl"
|
15
|
-
s.extra_rdoc_files = [
|
16
|
-
"LICENSE",
|
17
|
-
"README.rdoc"
|
18
|
-
]
|
19
|
-
s.files = [
|
20
|
-
".document",
|
21
|
-
"Gemfile",
|
22
|
-
"Gemfile.lock",
|
23
|
-
"LICENSE",
|
24
|
-
"README.rdoc",
|
25
|
-
"Rakefile",
|
26
|
-
"VERSION",
|
27
|
-
"deep_cloneable.gemspec",
|
28
|
-
"init.rb",
|
29
|
-
"lib/deep_cloneable.rb",
|
30
|
-
"test/database.yml",
|
31
|
-
"test/schema.rb",
|
32
|
-
"test/test_deep_cloneable.rb",
|
33
|
-
"test/test_helper.rb"
|
34
|
-
]
|
35
|
-
s.homepage = "http://github.com/moiristo/deep_cloneable"
|
36
|
-
s.require_paths = ["lib"]
|
37
|
-
s.rubygems_version = "1.8.21"
|
38
|
-
s.summary = "This gem gives every ActiveRecord::Base object the possibility to do a deep clone."
|
39
|
-
|
40
|
-
if s.respond_to? :specification_version then
|
41
|
-
s.specification_version = 3
|
3
|
+
$:.unshift File.expand_path('../lib', __FILE__)
|
4
|
+
require 'deep_cloneable/version'
|
42
5
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = 'deep_cloneable'
|
8
|
+
s.version = DeepCloneable::VERSION
|
9
|
+
s.authors = ['Reinier de Lange']
|
10
|
+
s.description = 'Extends the functionality of ActiveRecord::Base#dup to perform a deep clone that includes user specified associations.'
|
11
|
+
s.summary = 'This gem gives every ActiveRecord::Base object the possibility to do a deep clone.'
|
12
|
+
s.email = 'rjdelange@icloud.com'
|
13
|
+
s.extra_rdoc_files = ['LICENSE']
|
14
|
+
s.files = Dir.glob('{bin/*,lib/**/*,[A-Z]*}')
|
15
|
+
s.homepage = 'https://github.com/moiristo/deep_cloneable'
|
16
|
+
s.licenses = ['MIT']
|
17
|
+
s.platform = Gem::Platform::RUBY
|
18
|
+
s.required_ruby_version = '>= 1.9.3'
|
19
|
+
s.require_paths = ['lib']
|
20
|
+
s.add_runtime_dependency('activerecord', ['>= 3.1.0', '< 9'])
|
57
21
|
end
|
58
|
-
|
data/init.rb
CHANGED
@@ -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,129 +1,18 @@
|
|
1
|
-
|
2
|
-
module DeepCloneable
|
3
|
-
@@rails31 = ActiveRecord::VERSION::MAJOR >= 3 && ActiveRecord::VERSION::MINOR > 0
|
1
|
+
# frozen_string_literal: true
|
4
2
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
if @@rails31 and !Object.respond_to? :initialize_dup
|
9
|
-
ActiveRecord::Base.class_eval do
|
10
|
-
module Dup
|
11
|
-
def dup
|
12
|
-
copy = super
|
13
|
-
copy.initialize_dup(self)
|
14
|
-
copy
|
15
|
-
end
|
16
|
-
end
|
17
|
-
remove_method :dup
|
18
|
-
include Dup
|
19
|
-
end
|
20
|
-
end
|
3
|
+
require 'active_record'
|
4
|
+
require 'active_support/lazy_load_hooks'
|
5
|
+
require 'active_support/core_ext/array/wrap'
|
21
6
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
#
|
26
|
-
# === Usage:
|
27
|
-
#
|
28
|
-
# ==== Cloning one single association
|
29
|
-
# pirate.clone :include => :mateys
|
30
|
-
#
|
31
|
-
# ==== Cloning multiple associations
|
32
|
-
# pirate.clone :include => [:mateys, :treasures]
|
33
|
-
#
|
34
|
-
# ==== Cloning really deep
|
35
|
-
# pirate.clone :include => {:treasures => :gold_pieces}
|
36
|
-
#
|
37
|
-
# ==== Cloning really deep with multiple associations
|
38
|
-
# pirate.clone :include => [:mateys, {:treasures => :gold_pieces}]
|
39
|
-
#
|
40
|
-
# ==== Cloning really deep with multiple associations and a dictionary
|
41
|
-
#
|
42
|
-
# A dictionary ensures that models are not cloned multiple times when it is associated to nested models.
|
43
|
-
# When using a dictionary, ensure recurring associations are cloned first:
|
44
|
-
#
|
45
|
-
# pirate.clone :include => [:mateys, {:treasures => [:matey, :gold_pieces], :use_dictionary => true }]
|
46
|
-
#
|
47
|
-
# If this is not an option for you, it is also possible to populate the dictionary manually in advance:
|
48
|
-
#
|
49
|
-
# dict = { :mateys => {} }
|
50
|
-
# pirate.mateys.each{|m| dict[:mateys][m] = m.clone }
|
51
|
-
# pirate.clone :include => [:mateys, {:treasures => [:matey, :gold_pieces], :dictionary => dict }]
|
52
|
-
#
|
53
|
-
# ==== Cloning a model without an attribute
|
54
|
-
# pirate.clone :except => :name
|
55
|
-
#
|
56
|
-
# ==== Cloning a model without multiple attributes
|
57
|
-
# pirate.clone :except => [:name, :nick_name]
|
58
|
-
#
|
59
|
-
# ==== Cloning a model without an attribute or nested multiple attributes
|
60
|
-
# pirate.clone :include => :parrot, :except => [:name, { :parrot => [:name] }]
|
61
|
-
#
|
62
|
-
define_method (@@rails31 ? :dup : :clone) do |*args, &block|
|
63
|
-
options = args[0] || {}
|
64
|
-
|
65
|
-
dict = options[:dictionary]
|
66
|
-
dict ||= {} if options.delete(:use_dictionary)
|
67
|
-
|
68
|
-
kopy = unless dict
|
69
|
-
super()
|
70
|
-
else
|
71
|
-
tableized_class = self.class.name.tableize.to_sym
|
72
|
-
dict[tableized_class] ||= {}
|
73
|
-
dict[tableized_class][self] ||= super()
|
74
|
-
end
|
7
|
+
require 'deep_cloneable/association_not_found_exception'
|
8
|
+
require 'deep_cloneable/skip_validations'
|
9
|
+
require 'deep_cloneable/deep_clone'
|
75
10
|
|
76
|
-
|
11
|
+
module DeepCloneable
|
12
|
+
end
|
77
13
|
|
78
|
-
|
79
|
-
|
80
|
-
exceptions = options[:except].nil? ? [] : [options[:except]].flatten
|
81
|
-
exceptions.each do |attribute|
|
82
|
-
kopy.send(:write_attribute, attribute, self.class.column_defaults.dup[attribute.to_s]) unless attribute.kind_of?(Hash)
|
83
|
-
end
|
84
|
-
deep_exceptions = exceptions.select{|e| e.kind_of?(Hash) }.inject({}){|m,h| m.merge(h) }
|
85
|
-
end
|
86
|
-
|
87
|
-
if options[:include]
|
88
|
-
Array(options[:include]).each do |association, deep_associations|
|
89
|
-
if (association.kind_of? Hash)
|
90
|
-
deep_associations = association[association.keys.first]
|
91
|
-
association = association.keys.first
|
92
|
-
end
|
93
|
-
|
94
|
-
opts = deep_associations.blank? ? {} : {:include => deep_associations}
|
95
|
-
opts.merge!(:except => deep_exceptions[association]) if deep_exceptions[association]
|
96
|
-
opts.merge!(:dictionary => dict) if dict
|
97
|
-
|
98
|
-
association_reflection = self.class.reflect_on_association(association)
|
99
|
-
raise AssociationNotFoundException.new("#{self.class}##{association}") if association_reflection.nil?
|
100
|
-
|
101
|
-
cloned_object = case association_reflection.macro
|
102
|
-
when :belongs_to, :has_one
|
103
|
-
self.send(association) && self.send(association).send(__method__, opts, &block)
|
104
|
-
when :has_many, :has_and_belongs_to_many
|
105
|
-
primary_key_name = (@@rails31 ? association_reflection.foreign_key : association_reflection.primary_key_name).to_s
|
106
|
-
|
107
|
-
reverse_association_name = association_reflection.klass.reflect_on_all_associations.detect do |a|
|
108
|
-
a.send(@@rails31 ? :foreign_key : :primary_key_name).to_s == primary_key_name
|
109
|
-
end.try(:name)
|
110
|
-
|
111
|
-
self.send(association).collect do |obj|
|
112
|
-
tmp = obj.send(__method__, opts, &block)
|
113
|
-
tmp.send("#{primary_key_name}=", nil)
|
114
|
-
tmp.send("#{reverse_association_name.to_s}=", kopy) if reverse_association_name
|
115
|
-
tmp
|
116
|
-
end
|
117
|
-
end
|
118
|
-
kopy.send("#{association}=", cloned_object)
|
119
|
-
end
|
120
|
-
end
|
14
|
+
ActiveSupport.on_load :active_record do
|
15
|
+
protected :initialize_dup if ActiveRecord::VERSION::MAJOR == 3 && ActiveRecord::VERSION::MINOR == 1
|
121
16
|
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
class AssociationNotFoundException < StandardError; end
|
126
|
-
end
|
127
|
-
|
128
|
-
include DeepCloneable
|
129
|
-
end
|
17
|
+
include DeepCloneable::DeepClone
|
18
|
+
end
|