amoeba 1.2.1 → 3.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.cane +4 -0
- data/.gitignore +4 -1
- data/.rspec +2 -0
- data/.rubocop.yml +17 -0
- data/.travis.yml +110 -0
- data/Appraisals +76 -0
- data/Gemfile +11 -3
- data/README.md +763 -529
- data/Rakefile +6 -1
- data/amoeba.gemspec +20 -15
- data/defaults.reek +11 -0
- data/gemfiles/activerecord_4.2.gemfile +18 -0
- data/gemfiles/activerecord_5.0.gemfile +18 -0
- data/gemfiles/activerecord_5.1.gemfile +18 -0
- data/gemfiles/activerecord_5.2.gemfile +18 -0
- data/gemfiles/activerecord_6.0.gemfile +18 -0
- data/gemfiles/activerecord_6.1.gemfile +18 -0
- data/gemfiles/activerecord_head.gemfile +24 -0
- data/gemfiles/jruby_activerecord_6.1.gemfile +19 -0
- data/gemfiles/jruby_activerecord_head.gemfile +28 -0
- data/lib/amoeba.rb +13 -517
- data/lib/amoeba/class_methods.rb +28 -0
- data/lib/amoeba/cloner.rb +172 -0
- data/lib/amoeba/config.rb +182 -0
- data/lib/amoeba/instance_methods.rb +37 -0
- data/lib/amoeba/macros.rb +14 -0
- data/lib/amoeba/macros/base.rb +26 -0
- data/lib/amoeba/macros/has_and_belongs_to_many.rb +19 -0
- data/lib/amoeba/macros/has_many.rb +42 -0
- data/lib/amoeba/macros/has_one.rb +15 -0
- data/lib/amoeba/version.rb +1 -1
- data/spec/lib/amoeba_spec.rb +336 -111
- data/spec/spec_helper.rb +24 -3
- data/spec/support/data.rb +65 -84
- data/spec/support/models.rb +241 -25
- data/spec/support/schema.rb +102 -41
- metadata +63 -70
- data/.rvmrc +0 -1
@@ -0,0 +1,28 @@
|
|
1
|
+
module Amoeba
|
2
|
+
module ClassMethods
|
3
|
+
def amoeba(&block)
|
4
|
+
@config_block ||= block if block_given?
|
5
|
+
|
6
|
+
@config ||= Amoeba::Config.new(self)
|
7
|
+
@config.instance_eval(&block) if block_given?
|
8
|
+
@config
|
9
|
+
end
|
10
|
+
|
11
|
+
def fresh_amoeba(&block)
|
12
|
+
@config_block = block if block_given?
|
13
|
+
|
14
|
+
@config = Amoeba::Config.new(self)
|
15
|
+
@config.instance_eval(&block) if block_given?
|
16
|
+
@config
|
17
|
+
end
|
18
|
+
|
19
|
+
def reset_amoeba(&block)
|
20
|
+
@config_block = block if block_given?
|
21
|
+
@config = Amoeba::Config.new(self)
|
22
|
+
end
|
23
|
+
|
24
|
+
def amoeba_block
|
25
|
+
@config_block
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Amoeba
|
4
|
+
class Cloner
|
5
|
+
extend Forwardable
|
6
|
+
|
7
|
+
attr_reader :new_object, :old_object, :object_klass
|
8
|
+
|
9
|
+
def_delegators :old_object, :_parent_amoeba, :_amoeba_settings,
|
10
|
+
:_parent_amoeba_settings
|
11
|
+
|
12
|
+
def_delegators :object_klass, :amoeba, :fresh_amoeba, :reset_amoeba
|
13
|
+
|
14
|
+
def initialize(object, options = {})
|
15
|
+
@old_object = object
|
16
|
+
@options = options
|
17
|
+
@object_klass = @old_object.class
|
18
|
+
inherit_parent_settings
|
19
|
+
@new_object = object.__send__(amoeba.dup_method)
|
20
|
+
end
|
21
|
+
|
22
|
+
def run
|
23
|
+
process_overrides
|
24
|
+
apply if amoeba.enabled
|
25
|
+
after_apply if amoeba.do_preproc
|
26
|
+
@new_object
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def parenting_style
|
32
|
+
amoeba.upbringing ? amoeba.upbringing : _parent_amoeba.parenting
|
33
|
+
end
|
34
|
+
|
35
|
+
def inherit_strict_parent_settings
|
36
|
+
fresh_amoeba(&_parent_amoeba_settings)
|
37
|
+
end
|
38
|
+
|
39
|
+
def inherit_relaxed_parent_settings
|
40
|
+
amoeba(&_parent_amoeba_settings)
|
41
|
+
end
|
42
|
+
|
43
|
+
def inherit_submissive_parent_settings
|
44
|
+
reset_amoeba(&_amoeba_settings)
|
45
|
+
amoeba(&_parent_amoeba_settings)
|
46
|
+
amoeba(&_amoeba_settings)
|
47
|
+
end
|
48
|
+
|
49
|
+
def inherit_parent_settings
|
50
|
+
return if !_parent_amoeba.inherit
|
51
|
+
return unless %w(strict relaxed submissive).include?(parenting_style.to_s)
|
52
|
+
__send__("inherit_#{parenting_style}_parent_settings".to_sym)
|
53
|
+
end
|
54
|
+
|
55
|
+
def apply_clones
|
56
|
+
amoeba.clones.each do |clone_field|
|
57
|
+
exclude_clone_if_has_many_through(clone_field)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def exclude_clone_if_has_many_through(clone_field)
|
62
|
+
association = @object_klass.reflect_on_association(clone_field)
|
63
|
+
|
64
|
+
# if this is a has many through and we're gonna deep
|
65
|
+
# copy the child records, exclude the regular join
|
66
|
+
# table from copying so we don't end up with the new
|
67
|
+
# and old children on the copy
|
68
|
+
return unless association.macro == :has_many ||
|
69
|
+
association.is_a?(::ActiveRecord::Reflection::ThroughReflection)
|
70
|
+
amoeba.exclude_association(association.options[:through])
|
71
|
+
end
|
72
|
+
|
73
|
+
def follow_only_includes
|
74
|
+
amoeba.includes.each do |include, options|
|
75
|
+
next if options[:if] && !@old_object.send(options[:if])
|
76
|
+
follow_association(include, @object_klass.reflect_on_association(include))
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def follow_all_except_excludes
|
81
|
+
@object_klass.reflections.each do |name, association|
|
82
|
+
exclude = amoeba.excludes[name.to_sym]
|
83
|
+
next if exclude && (exclude.blank? || @old_object.send(exclude[:if]))
|
84
|
+
follow_association(name, association)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def follow_all
|
89
|
+
@object_klass.reflections.each do |name, association|
|
90
|
+
follow_association(name, association)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def apply_associations
|
95
|
+
if amoeba.includes.present?
|
96
|
+
follow_only_includes
|
97
|
+
elsif amoeba.excludes.present?
|
98
|
+
follow_all_except_excludes
|
99
|
+
else
|
100
|
+
follow_all
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def apply
|
105
|
+
apply_clones
|
106
|
+
apply_associations
|
107
|
+
end
|
108
|
+
|
109
|
+
def follow_association(relation_name, association)
|
110
|
+
return unless amoeba.known_macros.include?(association.macro.to_sym)
|
111
|
+
follow_klass = ::Amoeba::Macros.list[association.macro.to_sym]
|
112
|
+
follow_klass.new(self).follow(relation_name, association) if follow_klass
|
113
|
+
end
|
114
|
+
|
115
|
+
def process_overrides
|
116
|
+
amoeba.overrides.each do |block|
|
117
|
+
block.call(@old_object, @new_object)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def process_null_fields
|
122
|
+
# nullify any fields the user has configured
|
123
|
+
amoeba.null_fields.each do |field_key|
|
124
|
+
@new_object[field_key] = nil
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def process_coercions
|
129
|
+
# prepend any extra strings to indicate uniqueness of the new record(s)
|
130
|
+
amoeba.coercions.each do |field, coercion|
|
131
|
+
@new_object[field] = coercion.to_s
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def process_prefixes
|
136
|
+
# prepend any extra strings to indicate uniqueness of the new record(s)
|
137
|
+
amoeba.prefixes.each do |field, prefix|
|
138
|
+
@new_object[field] = "#{prefix}#{@new_object[field]}"
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def process_suffixes
|
143
|
+
# postpend any extra strings to indicate uniqueness of the new record(s)
|
144
|
+
amoeba.suffixes.each do |field, suffix|
|
145
|
+
@new_object[field] = "#{@new_object[field]}#{suffix}"
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def process_regexes
|
150
|
+
# regex any fields that need changing
|
151
|
+
amoeba.regexes.each do |field, action|
|
152
|
+
@new_object[field].gsub!(action[:replace], action[:with])
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def process_customizations
|
157
|
+
# prepend any extra strings to indicate uniqueness of the new record(s)
|
158
|
+
amoeba.customizations.each do |block|
|
159
|
+
block.call(@old_object, @new_object)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def after_apply
|
164
|
+
process_null_fields
|
165
|
+
process_coercions
|
166
|
+
process_prefixes
|
167
|
+
process_suffixes
|
168
|
+
process_regexes
|
169
|
+
process_customizations
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
@@ -0,0 +1,182 @@
|
|
1
|
+
module Amoeba
|
2
|
+
class Config
|
3
|
+
DEFAULTS = {
|
4
|
+
enabled: false,
|
5
|
+
inherit: false,
|
6
|
+
do_preproc: false,
|
7
|
+
parenting: false,
|
8
|
+
raised: false,
|
9
|
+
dup_method: :dup,
|
10
|
+
remap_method: nil,
|
11
|
+
includes: {},
|
12
|
+
excludes: {},
|
13
|
+
clones: [],
|
14
|
+
customizations: [],
|
15
|
+
overrides: [],
|
16
|
+
null_fields: [],
|
17
|
+
coercions: {},
|
18
|
+
prefixes: {},
|
19
|
+
suffixes: {},
|
20
|
+
regexes: {},
|
21
|
+
known_macros: [:has_one, :has_many, :has_and_belongs_to_many]
|
22
|
+
}
|
23
|
+
|
24
|
+
# ActiveRecord 3.x have different implementation of deep_dup
|
25
|
+
if ::ActiveRecord::VERSION::MAJOR == 3
|
26
|
+
DEFAULTS.instance_eval do
|
27
|
+
def deep_dup
|
28
|
+
each_with_object(dup) do |(key, value), hash|
|
29
|
+
hash[key.deep_dup] = value.deep_dup
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
Object.class_eval do
|
34
|
+
def deep_dup
|
35
|
+
duplicable? ? dup : self
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
DEFAULTS.freeze
|
41
|
+
|
42
|
+
DEFAULTS.each do |key, value|
|
43
|
+
value.freeze if value.is_a?(Array) || value.is_a?(Hash)
|
44
|
+
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
45
|
+
def #{key} # def enabled
|
46
|
+
@config[:#{key}] # @config[:enabled]
|
47
|
+
end # end
|
48
|
+
EOS
|
49
|
+
end
|
50
|
+
|
51
|
+
def initialize(klass)
|
52
|
+
@klass = klass
|
53
|
+
@config = self.class::DEFAULTS.deep_dup
|
54
|
+
end
|
55
|
+
|
56
|
+
alias_method :upbringing, :raised
|
57
|
+
|
58
|
+
def enable
|
59
|
+
@config[:enabled] = true
|
60
|
+
end
|
61
|
+
|
62
|
+
def disable
|
63
|
+
@config[:enabled] = false
|
64
|
+
end
|
65
|
+
|
66
|
+
def raised(style = :submissive)
|
67
|
+
@config[:raised] = style
|
68
|
+
end
|
69
|
+
|
70
|
+
def propagate(style = :submissive)
|
71
|
+
@config[:parenting] ||= style
|
72
|
+
@config[:inherit] = true
|
73
|
+
end
|
74
|
+
|
75
|
+
def push_value_to_array(value, key)
|
76
|
+
res = @config[key]
|
77
|
+
if value.is_a?(::Array)
|
78
|
+
res = value
|
79
|
+
elsif value
|
80
|
+
res << value
|
81
|
+
end
|
82
|
+
@config[key] = res.uniq
|
83
|
+
end
|
84
|
+
|
85
|
+
def push_array_value_to_hash(value, config_key)
|
86
|
+
@config[config_key] = {}
|
87
|
+
|
88
|
+
value.each do |definition|
|
89
|
+
definition.each do |key, val|
|
90
|
+
fill_hash_value_for(config_key, key, val)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def push_value_to_hash(value, config_key)
|
96
|
+
if value.is_a?(Array)
|
97
|
+
push_array_value_to_hash(value, config_key)
|
98
|
+
else
|
99
|
+
value.each do |key, val|
|
100
|
+
fill_hash_value_for(config_key, key, val)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
@config[config_key]
|
104
|
+
end
|
105
|
+
|
106
|
+
def fill_hash_value_for(config_key, key, val)
|
107
|
+
@config[config_key][key] = val if val || (!val.nil? && config_key == :coercions)
|
108
|
+
end
|
109
|
+
|
110
|
+
def include_association(value = nil, options = {})
|
111
|
+
enable
|
112
|
+
@config[:excludes] = {}
|
113
|
+
value = value.is_a?(Array) ? Hash[value.map! { |v| [v, options] }] : { value => options }
|
114
|
+
push_value_to_hash(value, :includes)
|
115
|
+
end
|
116
|
+
|
117
|
+
def include_associations(*values)
|
118
|
+
values.flatten.each { |v| include_association(v) }
|
119
|
+
end
|
120
|
+
|
121
|
+
# TODO: remove this method in v3.0.0
|
122
|
+
def include_field(value = nil)
|
123
|
+
warn 'include_field is deprecated and will be removed in version 3.0.0; please use include_association instead'
|
124
|
+
include_association(value)
|
125
|
+
end
|
126
|
+
|
127
|
+
def exclude_association(value = nil, options = {})
|
128
|
+
enable
|
129
|
+
@config[:includes] = {}
|
130
|
+
value = value.is_a?(Array) ? Hash[value.map! { |v| [v, options] }] : { value => options }
|
131
|
+
push_value_to_hash(value, :excludes)
|
132
|
+
end
|
133
|
+
|
134
|
+
def exclude_associations(*values)
|
135
|
+
values.flatten.each { |v| exclude_association(v) }
|
136
|
+
end
|
137
|
+
|
138
|
+
# TODO: remove this method in v3.0.0
|
139
|
+
def exclude_field(value = nil)
|
140
|
+
warn 'exclude_field is deprecated and will be removed in version 3.0.0; please use exclude_association instead'
|
141
|
+
exclude_association(value)
|
142
|
+
end
|
143
|
+
|
144
|
+
def clone(value = nil)
|
145
|
+
enable
|
146
|
+
push_value_to_array(value, :clones)
|
147
|
+
end
|
148
|
+
|
149
|
+
def recognize(value = nil)
|
150
|
+
enable
|
151
|
+
push_value_to_array(value, :known_macros)
|
152
|
+
end
|
153
|
+
|
154
|
+
{ override: 'overrides', customize: 'customizations',
|
155
|
+
nullify: 'null_fields' }.each do |method, key|
|
156
|
+
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
157
|
+
def #{method}(value = nil) # def override(value = nil)
|
158
|
+
@config[:do_preproc] = true # @config[:do_preproc] = true
|
159
|
+
push_value_to_array(value, :#{key}) # push_value_to_array(value, :overrides)
|
160
|
+
end # end
|
161
|
+
EOS
|
162
|
+
end
|
163
|
+
|
164
|
+
{ set: 'coercions', prepend: 'prefixes',
|
165
|
+
append: 'suffixes', regex: 'regexes' }.each do |method, key|
|
166
|
+
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
167
|
+
def #{method}(value = nil) # def set(value = nil)
|
168
|
+
@config[:do_preproc] = true # @config[:do_preproc] = true
|
169
|
+
push_value_to_hash(value, :#{key}) # push_value_to_hash(value, :coercions)
|
170
|
+
end # end
|
171
|
+
EOS
|
172
|
+
end
|
173
|
+
|
174
|
+
def through(value)
|
175
|
+
@config[:dup_method] = value.to_sym
|
176
|
+
end
|
177
|
+
|
178
|
+
def remapper(value)
|
179
|
+
@config[:remap_method] = value.to_sym
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Amoeba
|
2
|
+
module InstanceMethods
|
3
|
+
def _parent_amoeba
|
4
|
+
if _first_superclass_with_amoeba.respond_to?(:amoeba)
|
5
|
+
_first_superclass_with_amoeba.amoeba
|
6
|
+
else
|
7
|
+
false
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def _first_superclass_with_amoeba
|
12
|
+
return @_first_superclass_with_amoeba unless @_first_superclass_with_amoeba.nil?
|
13
|
+
klass = self.class
|
14
|
+
while klass.superclass < ::ActiveRecord::Base
|
15
|
+
klass = klass.superclass
|
16
|
+
break if klass.respond_to?(:amoeba) && klass.amoeba.enabled
|
17
|
+
end
|
18
|
+
@_first_superclass_with_amoeba = klass
|
19
|
+
end
|
20
|
+
|
21
|
+
def _amoeba_settings
|
22
|
+
self.class.amoeba_block
|
23
|
+
end
|
24
|
+
|
25
|
+
def _parent_amoeba_settings
|
26
|
+
if _first_superclass_with_amoeba.respond_to?(:amoeba_block)
|
27
|
+
_first_superclass_with_amoeba.amoeba_block
|
28
|
+
else
|
29
|
+
false
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def amoeba_dup(options = {})
|
34
|
+
::Amoeba::Cloner.new(self, options).run
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Amoeba
|
2
|
+
module Macros
|
3
|
+
class Base
|
4
|
+
def initialize(cloner)
|
5
|
+
@cloner = cloner
|
6
|
+
@old_object = cloner.old_object
|
7
|
+
@new_object = cloner.new_object
|
8
|
+
end
|
9
|
+
|
10
|
+
def follow(_relation_name, _association)
|
11
|
+
fail "#{self.class.name} doesn't implement `follow`!"
|
12
|
+
end
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def inherited(klass)
|
16
|
+
::Amoeba::Macros.add(klass)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def remapped_relation_name(name)
|
21
|
+
return name unless @cloner.amoeba.remap_method
|
22
|
+
@old_object.__send__(@cloner.amoeba.remap_method, name.to_sym) || name
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|