deep_cloneable 2.0.2 → 3.1.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.
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