amoeba 1.2.1 → 3.2.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,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,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