amoeba 2.1.0 → 3.0.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.
- checksums.yaml +4 -4
- data/.cane +4 -0
- data/.gitignore +2 -0
- data/.rubocop.yml +17 -0
- data/.travis.yml +13 -5
- data/Appraisals +24 -0
- data/Gemfile +6 -4
- data/README.md +134 -44
- data/Rakefile +2 -2
- data/amoeba.gemspec +20 -15
- data/defaults.reek +11 -0
- data/gemfiles/activerecord_3.2.gemfile +17 -0
- data/gemfiles/activerecord_4.0.gemfile +17 -0
- data/gemfiles/activerecord_4.1.gemfile +17 -0
- data/gemfiles/activerecord_4.2.gemfile +17 -0
- data/gemfiles/activerecord_head.gemfile +23 -0
- data/lib/amoeba.rb +13 -522
- data/lib/amoeba/class_methods.rb +23 -0
- data/lib/amoeba/cloner.rb +168 -0
- data/lib/amoeba/config.rb +172 -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 +180 -87
- data/spec/spec_helper.rb +23 -4
- data/spec/support/data.rb +73 -73
- data/spec/support/models.rb +132 -28
- data/spec/support/schema.rb +69 -42
- metadata +38 -29
- data/.ruby-version +0 -1
- data/gemfiles/Gemfile.activerecord-3.2.x +0 -11
- data/gemfiles/Gemfile.activerecord-4.0.x +0 -11
@@ -0,0 +1,23 @@
|
|
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 amoeba_block
|
20
|
+
@config_block
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,168 @@
|
|
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
|
13
|
+
|
14
|
+
def initialize(object, options = {})
|
15
|
+
@old_object, @options = object, options
|
16
|
+
@object_klass = @old_object.class
|
17
|
+
inherit_parent_settings
|
18
|
+
@new_object = object.__send__(amoeba.dup_method)
|
19
|
+
end
|
20
|
+
|
21
|
+
def run
|
22
|
+
process_overrides
|
23
|
+
apply if amoeba.enabled
|
24
|
+
after_apply if amoeba.do_preproc
|
25
|
+
@new_object
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def parenting_style
|
31
|
+
amoeba.upbringing ? amoeba.upbringing : _parent_amoeba.parenting
|
32
|
+
end
|
33
|
+
|
34
|
+
def inherit_strict_parent_settings
|
35
|
+
fresh_amoeba(&_parent_amoeba_settings)
|
36
|
+
end
|
37
|
+
|
38
|
+
def inherit_relaxed_parent_settings
|
39
|
+
amoeba(&_parent_amoeba_settings)
|
40
|
+
end
|
41
|
+
|
42
|
+
def inherit_submissive_parent_settings
|
43
|
+
fresh_amoeba(&_parent_amoeba_settings)
|
44
|
+
amoeba(&_amoeba_settings)
|
45
|
+
end
|
46
|
+
|
47
|
+
def inherit_parent_settings
|
48
|
+
return if amoeba.enabled || !_parent_amoeba.inherit
|
49
|
+
return unless %w(strict relaxed submissive).include?(parenting_style.to_s)
|
50
|
+
__send__("inherit_#{parenting_style}_parent_settings".to_sym)
|
51
|
+
end
|
52
|
+
|
53
|
+
def apply_clones
|
54
|
+
amoeba.clones.each do |clone_field|
|
55
|
+
exclude_clone_if_has_many_through(clone_field)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def exclude_clone_if_has_many_through(clone_field)
|
60
|
+
association = @object_klass.reflect_on_association(clone_field)
|
61
|
+
|
62
|
+
# if this is a has many through and we're gonna deep
|
63
|
+
# copy the child records, exclude the regular join
|
64
|
+
# table from copying so we don't end up with the new
|
65
|
+
# and old children on the copy
|
66
|
+
return unless association.macro == :has_many ||
|
67
|
+
association.is_a?(::ActiveRecord::Reflection::ThroughReflection)
|
68
|
+
amoeba.exclude_association(association.options[:through])
|
69
|
+
end
|
70
|
+
|
71
|
+
def follow_only_includes
|
72
|
+
amoeba.includes.each do |include|
|
73
|
+
follow_association(include, @object_klass.reflect_on_association(include))
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def follow_all_except_excludes
|
78
|
+
@object_klass.reflections.each do |name, association|
|
79
|
+
next if amoeba.excludes.include?(name.to_sym)
|
80
|
+
follow_association(name, association)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def follow_all
|
85
|
+
@object_klass.reflections.each do |name, association|
|
86
|
+
follow_association(name, association)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def apply_associations
|
91
|
+
if amoeba.includes.size > 0
|
92
|
+
follow_only_includes
|
93
|
+
elsif amoeba.excludes.size > 0
|
94
|
+
follow_all_except_excludes
|
95
|
+
else
|
96
|
+
follow_all
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def apply
|
101
|
+
apply_clones
|
102
|
+
apply_associations
|
103
|
+
end
|
104
|
+
|
105
|
+
def follow_association(relation_name, association)
|
106
|
+
return unless amoeba.known_macros.include?(association.macro.to_sym)
|
107
|
+
follow_klass = ::Amoeba::Macros.list[association.macro.to_sym]
|
108
|
+
follow_klass.new(self).follow(relation_name, association) if follow_klass
|
109
|
+
end
|
110
|
+
|
111
|
+
def process_overrides
|
112
|
+
amoeba.overrides.each do |block|
|
113
|
+
block.call(@new_object, @new_object)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def process_null_fields
|
118
|
+
# nullify any fields the user has configured
|
119
|
+
amoeba.null_fields.each do |field_key|
|
120
|
+
@new_object[field_key] = nil
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def process_coercions
|
125
|
+
# prepend any extra strings to indicate uniqueness of the new record(s)
|
126
|
+
amoeba.coercions.each do |field, coercion|
|
127
|
+
@new_object[field] = "#{coercion}"
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def process_prefixes
|
132
|
+
# prepend any extra strings to indicate uniqueness of the new record(s)
|
133
|
+
amoeba.prefixes.each do |field, prefix|
|
134
|
+
@new_object[field] = "#{prefix}#{@new_object[field]}"
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def process_suffixes
|
139
|
+
# postpend any extra strings to indicate uniqueness of the new record(s)
|
140
|
+
amoeba.suffixes.each do |field, suffix|
|
141
|
+
@new_object[field] = "#{@new_object[field]}#{suffix}"
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def process_regexes
|
146
|
+
# regex any fields that need changing
|
147
|
+
amoeba.regexes.each do |field, action|
|
148
|
+
@new_object[field].gsub!(action[:replace], action[:with])
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def process_customizations
|
153
|
+
# prepend any extra strings to indicate uniqueness of the new record(s)
|
154
|
+
amoeba.customizations.each do |block|
|
155
|
+
block.call(@old_object, @new_object)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def after_apply
|
160
|
+
process_null_fields
|
161
|
+
process_coercions
|
162
|
+
process_prefixes
|
163
|
+
process_suffixes
|
164
|
+
process_regexes
|
165
|
+
process_customizations
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
@@ -0,0 +1,172 @@
|
|
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
|
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
|
+
else
|
80
|
+
res << value if 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)
|
111
|
+
@config[:enabled] = true
|
112
|
+
@config[:excludes] = []
|
113
|
+
push_value_to_array(value, :includes)
|
114
|
+
end
|
115
|
+
|
116
|
+
# TODO remove this method in v3.0.0
|
117
|
+
def include_field(value = nil)
|
118
|
+
warn "include_field is deprecated and will be removed in version 3.0.0; please use include_association instead"
|
119
|
+
include_association(value)
|
120
|
+
end
|
121
|
+
|
122
|
+
def exclude_association(value = nil)
|
123
|
+
@config[:enabled] = true
|
124
|
+
@config[:includes] = []
|
125
|
+
push_value_to_array(value, :excludes)
|
126
|
+
end
|
127
|
+
|
128
|
+
# TODO remove this method in v3.0.0
|
129
|
+
def exclude_field(value = nil)
|
130
|
+
warn "exclude_field is deprecated and will be removed in version 3.0.0; please use exclude_association instead"
|
131
|
+
exclude_association(value)
|
132
|
+
end
|
133
|
+
|
134
|
+
def clone(value = nil)
|
135
|
+
@config[:enabled] = true
|
136
|
+
push_value_to_array(value, :clones)
|
137
|
+
end
|
138
|
+
|
139
|
+
def recognize(value = nil)
|
140
|
+
@config[:enabled] = true
|
141
|
+
push_value_to_array(value, :known_macros)
|
142
|
+
end
|
143
|
+
|
144
|
+
{ override: 'overrides', customize: 'customizations',
|
145
|
+
nullify: 'null_fields' }.each do |method, key|
|
146
|
+
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
147
|
+
def #{method}(value = nil) # def override(value = nil)
|
148
|
+
@config[:do_preproc] = true # @config[:do_preproc] = true
|
149
|
+
push_value_to_array(value, :#{key}) # push_value_to_array(value, :overrides)
|
150
|
+
end # end
|
151
|
+
EOS
|
152
|
+
end
|
153
|
+
|
154
|
+
{ set: 'coercions', prepend: 'prefixes',
|
155
|
+
append: 'suffixes', regex: 'regexes' }.each do |method, key|
|
156
|
+
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
157
|
+
def #{method}(value = nil) # def set(value = nil)
|
158
|
+
@config[:do_preproc] = true # @config[:do_preproc] = true
|
159
|
+
push_value_to_hash(value, :#{key}) # push_value_to_hash(value, :coercions)
|
160
|
+
end # end
|
161
|
+
EOS
|
162
|
+
end
|
163
|
+
|
164
|
+
def through(value)
|
165
|
+
@config[:dup_method] = value.to_sym
|
166
|
+
end
|
167
|
+
|
168
|
+
def remapper(value)
|
169
|
+
@config[:remap_method] = value.to_sym
|
170
|
+
end
|
171
|
+
end
|
172
|
+
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
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Amoeba
|
2
|
+
module Macros
|
3
|
+
class HasAndBelongsToMany < ::Amoeba::Macros::Base
|
4
|
+
def follow(relation_name, _association)
|
5
|
+
clone = @cloner.amoeba.clones.include?(relation_name.to_sym)
|
6
|
+
@old_object.__send__(relation_name).each do |old_obj|
|
7
|
+
fill_relation(relation_name, old_obj, clone)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def fill_relation(relation_name, old_obj, clone)
|
12
|
+
# associate this new child to the new parent object
|
13
|
+
old_obj = old_obj.amoeba_dup if clone
|
14
|
+
relation_name = remapped_relation_name(relation_name)
|
15
|
+
@new_object.__send__(relation_name) << old_obj
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|