deep_cloneable 2.3.0 → 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) 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