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.
@@ -1,58 +1,21 @@
1
- # Generated by jeweler
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
- Gem::Specification.new do |s|
7
- s.name = "deep_cloneable"
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
- if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
44
- s.add_runtime_dependency(%q<deep_cloneable>, [">= 0"])
45
- s.add_runtime_dependency(%q<rails>, [">= 0"])
46
- s.add_runtime_dependency(%q<jeweler>, [">= 0"])
47
- else
48
- s.add_dependency(%q<deep_cloneable>, [">= 0"])
49
- s.add_dependency(%q<rails>, [">= 0"])
50
- s.add_dependency(%q<jeweler>, [">= 0"])
51
- end
52
- else
53
- s.add_dependency(%q<deep_cloneable>, [">= 0"])
54
- s.add_dependency(%q<rails>, [">= 0"])
55
- s.add_dependency(%q<jeweler>, [">= 0"])
56
- end
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
@@ -1 +1,3 @@
1
- require 'deep_cloneable'
1
+ # frozen_string_literal: true
2
+
3
+ require 'deep_cloneable'
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeepCloneable
4
+ class AssociationNotFoundException < StandardError
5
+ end
6
+ end
@@ -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.1'
3
+ end
@@ -1,129 +1,18 @@
1
- class ActiveRecord::Base
2
- module DeepCloneable
3
- @@rails31 = ActiveRecord::VERSION::MAJOR >= 3 && ActiveRecord::VERSION::MINOR > 0
1
+ # frozen_string_literal: true
4
2
 
5
- # ActiveRecord::Base has its own dup method for Ruby 1.8.7. We have to
6
- # redefine it and put it in a module so that we can override it in a
7
- # module and call the original with super().
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
- # clones an ActiveRecord model.
23
- # if passed the :include option, it will deep clone the given associations
24
- # if passed the :except option, it won't clone the given attributes
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
- block.call(self, kopy) if block
11
+ module DeepCloneable
12
+ end
77
13
 
78
- deep_exceptions = {}
79
- if options[:except]
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
- return kopy
123
- end
124
-
125
- class AssociationNotFoundException < StandardError; end
126
- end
127
-
128
- include DeepCloneable
129
- end
17
+ include DeepCloneable::DeepClone
18
+ end