deep_cloneable 2.0.2 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2014 Reinier de Lange
1
+ Copyright (c) 2021 Reinier de Lange
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/Rakefile CHANGED
@@ -1,24 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rubygems'
2
4
  require 'bundler/setup'
3
5
  require 'appraisal'
4
-
5
- begin
6
- require 'jeweler'
7
- Jeweler::Tasks.new do |gem|
8
- gem.name = "deep_cloneable"
9
- gem.summary = %Q{This gem gives every ActiveRecord::Base object the possibility to do a deep clone.}
10
- gem.description = %Q{Extends the functionality of ActiveRecord::Base#clone to perform a deep clone that includes user specified associations. }
11
- gem.email = "r.j.delange@nedforce.nl"
12
- gem.homepage = "http://github.com/moiristo/deep_cloneable"
13
- gem.authors = ["Reinier de Lange"]
14
- # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
15
- end
16
- Jeweler::GemcutterTasks.new
17
- rescue LoadError
18
- puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
19
- end
20
-
21
6
  require 'rake/testtask'
7
+
22
8
  Rake::TestTask.new(:test) do |test|
23
9
  test.libs << 'lib' << 'test'
24
10
  test.pattern = 'test/**/test_*.rb'
@@ -29,7 +15,7 @@ task :default => :test
29
15
 
30
16
  require 'rdoc/task'
31
17
  Rake::RDocTask.new do |rdoc|
32
- version = File.exist?('VERSION') ? File.read('VERSION') : ""
18
+ version = File.exist?('VERSION') ? File.read('VERSION') : ''
33
19
 
34
20
  rdoc.rdoc_dir = 'rdoc'
35
21
  rdoc.title = "deep_cloneable #{version}"
@@ -1,66 +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 -*-
5
- # stub: deep_cloneable 2.0.2 ruby lib
1
+ # frozen_string_literal: true
6
2
 
7
- Gem::Specification.new do |s|
8
- s.name = "deep_cloneable"
9
- s.version = "2.0.2"
10
-
11
- s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
12
- s.require_paths = ["lib"]
13
- s.authors = ["Reinier de Lange"]
14
- s.date = "2014-12-17"
15
- s.description = "Extends the functionality of ActiveRecord::Base#clone to perform a deep clone that includes user specified associations. "
16
- s.email = "r.j.delange@nedforce.nl"
17
- s.extra_rdoc_files = [
18
- "LICENSE",
19
- "README.rdoc"
20
- ]
21
- s.files = [
22
- ".document",
23
- ".travis.yml",
24
- "Appraisals",
25
- "Gemfile",
26
- "Gemfile.lock",
27
- "LICENSE",
28
- "README.rdoc",
29
- "Rakefile",
30
- "VERSION",
31
- "deep_cloneable.gemspec",
32
- "gemfiles/3.1.gemfile",
33
- "gemfiles/3.1.gemfile.lock",
34
- "gemfiles/3.2.gemfile",
35
- "gemfiles/3.2.gemfile.lock",
36
- "gemfiles/4.0.gemfile",
37
- "gemfiles/4.0.gemfile.lock",
38
- "gemfiles/4.1.gemfile",
39
- "gemfiles/4.1.gemfile.lock",
40
- "gemfiles/4.2.gemfile",
41
- "gemfiles/4.2.gemfile.lock",
42
- "init.rb",
43
- "lib/deep_cloneable.rb",
44
- "test/database.yml",
45
- "test/models.rb",
46
- "test/schema.rb",
47
- "test/test_deep_cloneable.rb",
48
- "test/test_helper.rb"
49
- ]
50
- s.homepage = "http://github.com/moiristo/deep_cloneable"
51
- s.rubygems_version = "2.4.2"
52
- s.summary = "This gem gives every ActiveRecord::Base object the possibility to do a deep clone."
53
-
54
- if s.respond_to? :specification_version then
55
- s.specification_version = 4
3
+ $:.unshift File.expand_path('../lib', __FILE__)
4
+ require 'deep_cloneable/version'
56
5
 
57
- if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
58
- s.add_runtime_dependency(%q<activerecord>, ["< 5.0.0", ">= 3.1.0"])
59
- else
60
- s.add_dependency(%q<activerecord>, ["< 5.0.0", ">= 3.1.0"])
61
- end
62
- else
63
- s.add_dependency(%q<activerecord>, ["< 5.0.0", ">= 3.1.0"])
64
- 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', '< 7'])
65
21
  end
66
-
data/init.rb CHANGED
@@ -1 +1,3 @@
1
- require 'deep_cloneable'
1
+ # frozen_string_literal: true
2
+
3
+ require 'deep_cloneable'
@@ -1,136 +1,18 @@
1
- class ActiveRecord::Base
2
- module DeepCloneable
1
+ # frozen_string_literal: true
3
2
 
4
- # Deep dups an ActiveRecord model. See README.rdoc
5
- def deep_clone *args, &block
6
- options = args[0] || {}
3
+ require 'active_record'
4
+ require 'active_support/lazy_load_hooks'
5
+ require 'active_support/core_ext/array/wrap'
7
6
 
8
- dict = options[:dictionary]
9
- dict ||= {} if options.delete(:use_dictionary)
7
+ require 'deep_cloneable/association_not_found_exception'
8
+ require 'deep_cloneable/skip_validations'
9
+ require 'deep_cloneable/deep_clone'
10
10
 
11
- kopy = unless dict
12
- dup()
13
- else
14
- tableized_class = self.class.name.tableize.to_sym
15
- dict[tableized_class] ||= {}
16
- dict[tableized_class][self] ||= dup()
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
- kopy.send(:write_attribute, attribute, self.class.column_defaults.dup[attribute.to_s]) 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
- kopy.send(:write_attribute, attribute, self.class.column_defaults.dup[attribute.to_s]) 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
- Array(options[:include]).each do |association, deep_associations|
43
- if (association.kind_of? Hash)
44
- deep_associations = association[association.keys.first]
45
- association = association.keys.first
46
- end
47
-
48
- dup_options = deep_associations.blank? ? {} : {:include => deep_associations}
49
- dup_options.merge!(:except => deep_exceptions[association]) if deep_exceptions[association]
50
- dup_options.merge!(:only => deep_onlinesses[association]) if deep_onlinesses[association]
51
- dup_options.merge!(:dictionary => dict) if dict
52
-
53
- association_reflection = self.class.reflect_on_association(association)
54
- raise AssociationNotFoundException.new("#{self.class}##{association}") if association_reflection.nil?
55
-
56
- if options[:validate] == false
57
- kopy.instance_eval do
58
- # Force :validate => false on all saves.
59
- def perform_validations(options={})
60
- options[:validate] = false
61
- super(options)
62
- end
63
- end
64
- end
65
-
66
- association_type = association_reflection.macro
67
- association_type = "#{association_type}_through" if association_reflection.is_a?(ActiveRecord::Reflection::ThroughReflection)
68
-
69
- cloned_object = send(
70
- "dup_#{association_type}_association",
71
- { :reflection => association_reflection, :association => association, :copy => kopy, :dup_options => dup_options },
72
- &block
73
- )
74
-
75
- kopy.send("#{association}=", cloned_object)
76
- end
77
- end
78
-
79
- return kopy
80
- end
81
-
82
- private
83
-
84
- def dup_belongs_to_association options, &block
85
- self.send(options[:association]) && self.send(options[:association]).deep_clone(options[:dup_options], &block)
86
- end
87
-
88
- def dup_has_one_association options, &block
89
- dup_belongs_to_association options, &block
90
- end
91
-
92
- def dup_has_many_association options, &block
93
- primary_key_name = options[:reflection].foreign_key.to_s
94
-
95
- reverse_association_name = options[:reflection].klass.reflect_on_all_associations.detect do |reflection|
96
- reflection.foreign_key.to_s == primary_key_name && reflection != options[:reflection]
97
- end.try(:name)
98
-
99
- self.send(options[:association]).collect do |obj|
100
- tmp = obj.deep_clone(options[:dup_options], &block)
101
- tmp.send("#{primary_key_name}=", nil)
102
- tmp.send("#{reverse_association_name.to_s}=", options[:copy]) if reverse_association_name
103
- tmp
104
- end
105
- end
106
-
107
- def dup_has_many_through_association options, &block
108
- dup_join_association(
109
- options.merge(:macro => :has_many, :primary_key_name => options[:reflection].through_reflection.foreign_key.to_s),
110
- &block)
111
- end
112
-
113
- def dup_has_and_belongs_to_many_association options, &block
114
- dup_join_association(
115
- options.merge(:macro => :has_and_belongs_to_many, :primary_key_name => options[:reflection].foreign_key.to_s),
116
- &block)
117
- end
118
-
119
- def dup_join_association options, &block
120
- reverse_association_name = options[:reflection].klass.reflect_on_all_associations.detect do |reflection|
121
- (reflection.macro == options[:macro]) && (reflection.association_foreign_key.to_s == options[:primary_key_name])
122
- end.try(:name)
123
-
124
- self.send(options[:association]).collect do |obj|
125
- obj.send(reverse_association_name).target << options[:copy] if reverse_association_name
126
- obj
127
- end
128
- end
129
-
130
- class AssociationNotFoundException < StandardError; end
11
+ module DeepCloneable
12
+ end
131
13
 
132
- ActiveRecord::Base.class_eval { protected :initialize_dup } if ActiveRecord::VERSION::MAJOR == 3 && ActiveRecord::VERSION::MINOR == 1
133
- end
14
+ ActiveSupport.on_load :active_record do
15
+ protected :initialize_dup if ActiveRecord::VERSION::MAJOR == 3 && ActiveRecord::VERSION::MINOR == 1
134
16
 
135
- include DeepCloneable
17
+ include DeepCloneable::DeepClone
136
18
  end
@@ -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