amoeba 2.1.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,14 @@
1
+ module Amoeba
2
+ module Macros
3
+ extend self
4
+ def list
5
+ @list ||= {}
6
+ end
7
+
8
+ def add(klass)
9
+ @list ||= {}
10
+ key = klass.name.demodulize.underscore.to_sym
11
+ @list[key] = klass
12
+ end
13
+ end
14
+ 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