deep_cloneable 2.3.0 → 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) 2017 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,25 +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#dup 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.license = "MIT"
15
- # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
16
- end
17
- Jeweler::GemcutterTasks.new
18
- rescue LoadError
19
- puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
20
- end
21
-
22
6
  require 'rake/testtask'
7
+
23
8
  Rake::TestTask.new(:test) do |test|
24
9
  test.libs << 'lib' << 'test'
25
10
  test.pattern = 'test/**/test_*.rb'
@@ -30,7 +15,7 @@ task :default => :test
30
15
 
31
16
  require 'rdoc/task'
32
17
  Rake::RDocTask.new do |rdoc|
33
- version = File.exist?('VERSION') ? File.read('VERSION') : ""
18
+ version = File.exist?('VERSION') ? File.read('VERSION') : ''
34
19
 
35
20
  rdoc.rdoc_dir = 'rdoc'
36
21
  rdoc.title = "deep_cloneable #{version}"
@@ -1,69 +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.3.0 ruby lib
1
+ # frozen_string_literal: true
6
2
 
7
- Gem::Specification.new do |s|
8
- s.name = "deep_cloneable"
9
- s.version = "2.3.0"
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 = "2017-06-14"
15
- s.description = "Extends the functionality of ActiveRecord::Base#dup 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.md"
20
- ]
21
- s.files = [
22
- ".document",
23
- ".travis.yml",
24
- "Appraisals",
25
- "Gemfile",
26
- "Gemfile.lock",
27
- "LICENSE",
28
- "Rakefile",
29
- "VERSION",
30
- "deep_cloneable.gemspec",
31
- "gemfiles/3.1.gemfile",
32
- "gemfiles/3.1.gemfile.lock",
33
- "gemfiles/3.2.gemfile",
34
- "gemfiles/3.2.gemfile.lock",
35
- "gemfiles/4.0.gemfile",
36
- "gemfiles/4.0.gemfile.lock",
37
- "gemfiles/4.1.gemfile",
38
- "gemfiles/4.1.gemfile.lock",
39
- "gemfiles/4.2.gemfile",
40
- "gemfiles/4.2.gemfile.lock",
41
- "gemfiles/5.0.gemfile",
42
- "gemfiles/5.0.gemfile.lock",
43
- "init.rb",
44
- "lib/deep_cloneable.rb",
45
- "readme.md",
46
- "test/database.yml",
47
- "test/models.rb",
48
- "test/schema.rb",
49
- "test/test_deep_cloneable.rb",
50
- "test/test_helper.rb"
51
- ]
52
- s.homepage = "http://github.com/moiristo/deep_cloneable"
53
- s.licenses = ["MIT"]
54
- s.rubygems_version = "2.4.8"
55
- s.summary = "This gem gives every ActiveRecord::Base object the possibility to do a deep clone."
56
-
57
- if s.respond_to? :specification_version then
58
- s.specification_version = 4
3
+ $:.unshift File.expand_path('../lib', __FILE__)
4
+ require 'deep_cloneable/version'
59
5
 
60
- if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
61
- s.add_runtime_dependency(%q<activerecord>, ["< 5.2.0", ">= 3.1.0"])
62
- else
63
- s.add_dependency(%q<activerecord>, ["< 5.2.0", ">= 3.1.0"])
64
- end
65
- else
66
- s.add_dependency(%q<activerecord>, ["< 5.2.0", ">= 3.1.0"])
67
- 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'])
68
21
  end
69
-
data/init.rb CHANGED
@@ -1 +1,3 @@
1
- require 'deep_cloneable'
1
+ # frozen_string_literal: true
2
+
3
+ require 'deep_cloneable'
@@ -1,199 +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[:if] = conditions_or_deep_associations.delete(:if) if conditions_or_deep_associations[:if]
52
- conditions[:unless] = conditions_or_deep_associations.delete(:unless) if conditions_or_deep_associations[:unless]
53
- elsif conditions_or_deep_associations.kind_of?(Array)
54
- conditions_or_deep_associations.delete_if {|entry| conditions.merge!(entry) if entry.is_a?(Hash) && (entry.key?(:if) || entry.key?(:unless)) }
55
- end
56
-
57
- dup_options = {}
58
- dup_options.merge!(:include => conditions_or_deep_associations) if conditions_or_deep_associations.present?
59
- dup_options.merge!(:except => deep_exceptions[association]) if deep_exceptions[association]
60
- dup_options.merge!(:only => deep_onlinesses[association]) if deep_onlinesses[association]
61
- dup_options.merge!(:dictionary => dict) if dict
62
- dup_options.merge!(:skip_missing_associations => options[:skip_missing_associations]) if options[:skip_missing_associations]
63
-
64
- if association_reflection = self.class.reflect_on_association(association)
65
- if options[:validate] == false
66
- kopy.instance_eval do
67
- # Force :validate => false on all saves.
68
- def perform_validations(options={})
69
- options[:validate] = false
70
- super(options)
71
- end
72
- end
73
- end
74
-
75
- association_type = association_reflection.macro
76
- association_type = "#{association_type}_through" if association_reflection.is_a?(ActiveRecord::Reflection::ThroughReflection)
77
-
78
- duped_object = send(
79
- "dup_#{association_type}_association",
80
- { :reflection => association_reflection, :association => association, :copy => kopy, :conditions => conditions, :dup_options => dup_options },
81
- &block
82
- )
83
-
84
- kopy.send("#{association}=", duped_object)
85
- elsif !options[:skip_missing_associations]
86
- raise AssociationNotFoundException.new("#{self.class}##{association}")
87
- end
88
- end
89
- end
90
-
91
- return kopy
92
- end
93
-
94
- protected
95
-
96
- def find_in_dict_or_dup(dict, dup_on_miss = true)
97
- tableized_class = self.class.name.tableize.to_sym
98
- dict[tableized_class] ||= {}
99
- dict_val = dict[tableized_class][self]
100
- dict_val.nil? && dup_on_miss ? dict[tableized_class][self] = dup() : dict_val
101
- end
102
-
103
- private
104
-
105
- def dup_default_attribute_value_to(kopy, attribute, origin)
106
- kopy[attribute] = origin.class.column_defaults.dup[attribute.to_s]
107
- end
108
-
109
- def dup_belongs_to_association options, &block
110
- object = self.send(options[:association])
111
- object = nil if options[:conditions].any? && evaluate_conditions(object, options[:conditions])
112
- object && object.deep_clone(options[:dup_options], &block)
113
- end
114
-
115
- def dup_has_one_association options, &block
116
- dup_belongs_to_association options, &block
117
- end
118
-
119
- def dup_has_many_association options, &block
120
- primary_key_name = options[:reflection].foreign_key.to_s
121
-
122
- if options[:reflection].inverse_of.present?
123
- reverse_association_name = options[:reflection].inverse_of.name
124
- else
125
- reverse_association_name = options[:reflection].klass.reflect_on_all_associations.detect do |reflection|
126
- reflection.foreign_key.to_s == primary_key_name && reflection != options[:reflection]
127
- end.try(:name)
128
- end
129
-
130
- objects = self.send(options[:association])
131
- objects = objects.select{|object| evaluate_conditions(object, options[:conditions]) } if options[:conditions].any?
132
-
133
- objects.collect do |object|
134
- tmp = object.deep_clone(options[:dup_options], &block)
135
- tmp.send("#{primary_key_name}=", nil)
136
- tmp.send("#{reverse_association_name.to_s}=", options[:copy]) if reverse_association_name
137
- tmp
138
- end
139
- end
140
-
141
- def dup_has_many_through_association options, &block
142
- dup_join_association(
143
- options.merge(:macro => :has_many, :primary_key_name => options[:reflection].through_reflection.foreign_key.to_s),
144
- &block)
145
- end
146
-
147
- def dup_has_and_belongs_to_many_association options, &block
148
- dup_join_association(
149
- options.merge(:macro => :has_and_belongs_to_many, :primary_key_name => options[:reflection].foreign_key.to_s),
150
- &block)
151
- end
152
-
153
- def dup_join_association options, &block
154
- if options[:reflection].inverse_of.present?
155
- reverse_association_name = options[:reflection].inverse_of.name
156
- else
157
- reverse_association_name = options[:reflection].klass.reflect_on_all_associations.detect do |reflection|
158
- (reflection.macro == options[:macro]) && (reflection.association_foreign_key.to_s == options[:primary_key_name])
159
- end.try(:name)
160
- end
161
-
162
- objects = self.send(options[:association])
163
- objects = objects.select{|object| evaluate_conditions(object, options[:conditions]) } if options[:conditions].any?
164
-
165
- objects.collect do |object|
166
- dict = options[:dup_options][:dictionary]
167
- if(dict && object.find_in_dict_or_dup(dict, false))
168
- object = object.deep_clone(options[:dup_options], &block)
169
- else
170
- object.send(reverse_association_name).target << options[:copy] if reverse_association_name
171
- end
172
- object
173
- end
174
- end
175
-
176
- def evaluate_conditions object, conditions
177
- (conditions[:if] && conditions[:if].call(object)) || (conditions[:unless] && !conditions[:unless].call(object))
178
- end
179
-
180
- def normalized_includes_list includes
181
- list = []
182
- Array(includes).each do |item|
183
- if item.is_a?(Hash) && item.size > 1
184
- item.each{|key, value| list << { key => value } }
185
- else
186
- list << item
187
- end
188
- end
189
-
190
- list
191
- end
192
-
193
- class AssociationNotFoundException < StandardError; end
11
+ module DeepCloneable
12
+ end
194
13
 
195
- ActiveRecord::Base.class_eval { protected :initialize_dup } if ActiveRecord::VERSION::MAJOR == 3 && ActiveRecord::VERSION::MINOR == 1
196
- end
14
+ ActiveSupport.on_load :active_record do
15
+ protected :initialize_dup if ActiveRecord::VERSION::MAJOR == 3 && ActiveRecord::VERSION::MINOR == 1
197
16
 
198
- include DeepCloneable
17
+ include DeepCloneable::DeepClone
199
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