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.
@@ -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