amoeba 2.1.0 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|