amoeba 1.2.1 → 3.2.0

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