amoeba 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use --create 1.9.3-p0@amoeba
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in power_dup.gemspec
4
+ gemspec
@@ -0,0 +1,448 @@
1
+ # Amoeba
2
+
3
+ ## Overview
4
+
5
+ Easy copying of rails associations such as `has_many`.
6
+
7
+ I named this gem Amoeba because amoebas are (small life forms that are) good at reproducing. Their children and grandchildren also reproduce themselves quickly and easily.
8
+
9
+ ## Details
10
+
11
+ An ActiveRecord extension gem to allow the duplication of associated child record objects when duplicating an active record model. This gem overrides and adds to the built in `ActiveRecord::Base#dup` method.
12
+
13
+ Rails 3 compatible.
14
+
15
+ ## Features
16
+
17
+ - Supports automatic recursive duplication of associated `has_one`, `has_many` and `has_and_belongs_to_many` child records.
18
+ - Allows configuration of which fields to copy through a simple DSL applied to your rails models.
19
+ - Supports multiple configuration styles such as inclusive, exclusive and indiscriminate (aka copy everything).
20
+ - Supports preprocessing of fields to help indicate uniqueness and ensure the integrity of your data depending on your business logic needs.
21
+ - Supports per instance configuration to override configuration and behavior on the fly.
22
+
23
+ ## Installation
24
+
25
+ is hopefully as you would expect:
26
+
27
+ gem install amoeba
28
+
29
+ or just add it to your Gemfile:
30
+
31
+ gem 'amoeba'
32
+
33
+ ## Usage
34
+
35
+ Configure your models with one of the styles below and then just run the `dup` method on your model as you normally would:
36
+
37
+ p = Post.create(:title => "Hello World!", :content => "Lorum ipsum dolor")
38
+ p.comments.create(:content => "I love it!")
39
+ p.comments.create(:content => "This sucks!")
40
+
41
+ puts Comment.all.count # should be 2
42
+
43
+ my_copy = p.dup
44
+ my_copy.save
45
+
46
+ puts Comment.all.count # should be 4
47
+
48
+ By default, when enabled, amoeba will copy any and all associated child records automatically and associated them with the new parent record.
49
+
50
+ You can configure the behavior to only include fields that you list or to only include fields that you don't exclude. Of the three, the most performant will be the indiscriminate style, followed by the inclusive style, and the exclusive style will be the slowest because of the need for an extra explicit check on each field. This performance difference is likely negligible enough that you can choose the style to use based on which is easiest to read and write, however, if your data tree is large enough and you need control over what fields get copied, inclusive style is probably a better choice than exclusive style.
51
+
52
+ ## Configuration
53
+
54
+ Please note that these examples are only loose approximations of real world scenarios and may not be particularly realistic, they are only for the purpose of demonstrating feature usage.
55
+
56
+ ### Indiscriminate Style
57
+
58
+ This is the most basic usage case and will simply enable the copying of any known associations.
59
+
60
+ If you have some models for a blog about like this:
61
+
62
+ class Post < ActiveRecord::Base
63
+ has_many :comments
64
+ end
65
+
66
+ class Comment < ActiveRecord::Base
67
+ belongs_to :post
68
+ end
69
+
70
+ Add the amoeba configuration block to your model and call the enable method to enable the copying of child records, like this:
71
+
72
+ class Post < ActiveRecord::Base
73
+ has_many :comments
74
+
75
+ amoeba do
76
+ enable
77
+ end
78
+ end
79
+
80
+ class Comment < ActiveRecord::Base
81
+ belongs_to :post
82
+ end
83
+
84
+ Child records will be automatically copied when you run the dup method.
85
+
86
+ ### Inclusive Style
87
+
88
+ If you only want some of the associations copied but not others, you may use the inclusive style:
89
+
90
+ class Post < ActiveRecord::Base
91
+ has_many :comments
92
+ has_many :tags
93
+ has_many :authors
94
+
95
+ amoeba do
96
+ enable
97
+ include_field :tags
98
+ include_field :authors
99
+ end
100
+ end
101
+
102
+ class Comment < ActiveRecord::Base
103
+ belongs_to :post
104
+ end
105
+
106
+ Using the inclusive style within the amoeba block actually implies that you wish to enable amoeba, so there is no need to run the enable method, though it won't hurt either:
107
+
108
+ class Post < ActiveRecord::Base
109
+ has_many :comments
110
+ has_many :tags
111
+ has_many :authors
112
+
113
+ amoeba do
114
+ include_field :tags
115
+ include_field :authors
116
+ end
117
+ end
118
+
119
+ class Comment < ActiveRecord::Base
120
+ belongs_to :post
121
+ end
122
+
123
+ You may also specify fields to be copied by passing an array.
124
+
125
+ class Post < ActiveRecord::Base
126
+ has_many :comments
127
+ has_many :tags
128
+ has_many :authors
129
+
130
+ amoeba do
131
+ include_field [:tags, :authors]
132
+ end
133
+ end
134
+
135
+ class Comment < ActiveRecord::Base
136
+ belongs_to :post
137
+ end
138
+
139
+ These examples will copy the post's tags and authors but not its comments.
140
+
141
+ ### Exclusive Style
142
+
143
+ If you have more fields to include than to exclude, you may wish to shorten the amount of typing and reading you need to do by using the exclusive style. All fields that are not explicitly excluded will be copied:
144
+
145
+ class Post < ActiveRecord::Base
146
+ has_many :comments
147
+ has_many :tags
148
+ has_many :authors
149
+
150
+ amoeba do
151
+ exclude_field :comments
152
+ end
153
+ end
154
+
155
+ class Comment < ActiveRecord::Base
156
+ belongs_to :post
157
+ end
158
+
159
+ This example does the same thing as the inclusive style example, it will copy the post's tags and authors but not its comments. As with inclusive style, there is no need to explicitly enable amoeba when specifying fields to exclude.
160
+
161
+ ### Limiting Association Types
162
+
163
+ By default, amoeba recognizes and attemps to copy any children of the following association types:
164
+
165
+ - has one
166
+ - has many
167
+ - has and belongs to many
168
+
169
+ You may control which association types amoeba applies itself to by using the `recognize` method within the amoeba configuration block.
170
+
171
+ class Post < ActiveRecord::Base
172
+ has_one :config
173
+ has_many :comments
174
+ has_and_belongs_to_many :tags
175
+
176
+ amoeba do
177
+ recognize [:has_one, :has_and_belongs_to_many]
178
+ end
179
+ end
180
+
181
+ class Comment < ActiveRecord::Base
182
+ belongs_to :post
183
+ end
184
+
185
+ class Tag < ActiveRecord::Base
186
+ has_and_belongs_to_many :posts
187
+ end
188
+
189
+ This example will copy the post's configuration data and keep tags associated with the new post, but will not copy the post's comments because amoeba will only recognize and copy children of `has_one` and `has_and_belongs_to_many` associations and in this example, comments are not an `has_and_belongs_to_many` association.
190
+
191
+ ### Field Preprocessors
192
+
193
+ #### Nullify
194
+
195
+ If you wish to prevent a regular (non `has_*` association based) field from retaining it's value when copied, you may "zero out" or "nullify" the field, like this:
196
+
197
+ class Topic < ActiveRecord::Base
198
+ has_many :posts
199
+ end
200
+
201
+ class Post < ActiveRecord::Base
202
+ belongs_to :topic
203
+ has_many :comments
204
+
205
+ amoeba do
206
+ enable
207
+ nullify :date_published
208
+ nullify :topic_id
209
+ end
210
+ end
211
+
212
+ class Comment < ActiveRecord::Base
213
+ belongs_to :post
214
+ end
215
+
216
+ This example will copy all of a post's comments. It will also nullify the publishing date and dissociate the post from its original topic.
217
+
218
+ Unlike inclusive and exclusive styles, specifying null fields will not automatically enable amoeba to copy all child records. As with any active record object, the default field value will be used instead of `nil` if a default value exists on the migration.
219
+
220
+ #### Prepend
221
+
222
+ You may add a string to the beginning of a copied object's field during the copy phase:
223
+
224
+ class Post < ActiveRecord::Base
225
+ amoeba do
226
+ enable
227
+ prepend :title => "Copy of "
228
+ end
229
+ end
230
+
231
+ #### Append
232
+
233
+ You may add a string to the end of a copied object's field during the copy phase:
234
+
235
+ class Post < ActiveRecord::Base
236
+ amoeba do
237
+ enable
238
+ append :title => "Copy of "
239
+ end
240
+ end
241
+
242
+ #### Regex
243
+
244
+ You may run a search and replace query on a copied object's field during the copy phase:
245
+
246
+ class Post < ActiveRecord::Base
247
+ amoeba do
248
+ enable
249
+ regex :contents => {:replace => /dog/, :with => 'cat'}
250
+ end
251
+ end
252
+
253
+ #### Chaining
254
+
255
+ You may apply a preprocessor to multiple fields at once.
256
+
257
+ class Post < ActiveRecord::Base
258
+ amoeba do
259
+ enable
260
+ prepend :title => "Copy of ", :contents => "Copied contents: "
261
+ end
262
+ end
263
+
264
+ #### Stacking
265
+
266
+ You may apply multiple preproccessing directives to a single model at once.
267
+
268
+ class Post < ActiveRecord::Base
269
+ amoeba do
270
+ prepend :title => "Copy of ", :contents => "Original contents: "
271
+ append :contents => " (copied version)"
272
+ regex :contents => {:replace => /dog/, :with => 'cat'}
273
+ end
274
+ end
275
+
276
+ This example should result in something like this:
277
+
278
+ post = Post.create(
279
+ :title => "Hello world",
280
+ :contents => "I like dogs, dogs are awesome."
281
+ )
282
+
283
+ new_post = post.dup
284
+
285
+ new_post.title # "Copy of Hello world"
286
+ new_post.contents # "Original contents: I like cats, cats are awesome. (copied version)"
287
+
288
+ Like `nullify`, the preprocessing directives do not automatically enable the copying of associated child records. If only preprocessing directives are used and you do want to copy child records and no `include_field` or `exclude_field` list is provided, you must still explicitly enable the copying of child records by calling the enable method from within the amoeba block on your model.
289
+
290
+ ### Precedence
291
+
292
+ You may use a combination of configuration methods within each model's amoeba block. Recognized association types take precedence over inclusion or exclusion lists. Inclusive style takes precedence over exclusive style, and these two explicit styles take precedence over the indiscriminate style. In other words, if you list fields to copy, amoeba will only copy the fields you list, or only copy the fields you don't exclude as the case may be. Additionally, if a field type is not recognized it will not be copied, regardless of whether it appears in an inclusion list. If you want amoeba to automatically copy all of your child records, do not list any fields using either `include_field` or `exclude_field`.
293
+
294
+ The following example syntax is perfectly valid, and will result in the usage of inclusive style. The order in which you call the configuration methods within the amoeba block does not matter:
295
+
296
+ class Topic < ActiveRecord::Base
297
+ has_many :posts
298
+ end
299
+
300
+ class Post < ActiveRecord::Base
301
+ belongs_to :topic
302
+ has_many :comments
303
+ has_many :tags
304
+ has_many :authors
305
+
306
+ amoeba do
307
+ exclude_field :authors
308
+ include_field :tags
309
+ nullify :date_published
310
+ prepend :title => "Copy of "
311
+ append :contents => " (copied version)"
312
+ regex :contents => {:replace => /dog/, :with => 'cat'}
313
+ include_field :authors
314
+ enable
315
+ nullify :topic_id
316
+ end
317
+ end
318
+
319
+ class Comment < ActiveRecord::Base
320
+ belongs_to :post
321
+ end
322
+
323
+ This example will copy all of a post's tags and authors, but not its comments. It will also nullify the publishing date and dissociate the post from its original topic. It will also preprocess the post's fields as in the previous preprocessing example.
324
+
325
+ Note that, because of precedence, inclusive style is used and the list of exclude fields is never consulted. Additionally, the `enable` method is redundant because amoeba is automatically enabled when using `include_field`.
326
+
327
+ The preprocessing directives are run after child records are copied andare run in this order.
328
+
329
+ 1. Null fields
330
+ 2. Prepends
331
+ 3. Appends
332
+ 4. Search and Replace
333
+
334
+ Preprocessing directives do not affect inclusion and exclusion lists.
335
+
336
+ ### Recursing
337
+
338
+ You may cause amoeba to keep copying down the chain as far as you like, simply add amoeba blocks to each model you wish to have copy its children. Amoeba will automatically recurse into any enabled grandchildren and copy them as well.
339
+
340
+ class Post < ActiveRecord::Base
341
+ has_many :comments
342
+
343
+ amoeba do
344
+ enable
345
+ end
346
+ end
347
+
348
+ class Comment < ActiveRecord::Base
349
+ belongs_to :post
350
+ has_many :ratings
351
+
352
+ amoeba do
353
+ enable
354
+ end
355
+ end
356
+
357
+ class Rating < ActiveRecord::Base
358
+ belongs_to :comment
359
+ end
360
+
361
+ In this example, when a post is copied, amoeba will copy each all of a post's comments and will also copy each comment's ratings.
362
+
363
+
364
+ ### On The Fly Configuration
365
+
366
+ You may control how amoeba copies your object, on the fly, by passing a configuration block to the model's amoeba method. The configuration method is static but the configuration is applied on a per instance basis.
367
+
368
+ class Post < ActiveRecord::Base
369
+ has_many :comments
370
+
371
+ amoeba do
372
+ enable
373
+ prepend :title => "Copy of "
374
+ end
375
+ end
376
+
377
+ class Comment < ActiveRecord::Base
378
+ belongs_to :post
379
+ end
380
+
381
+ class PostsController < ActionController
382
+ def duplicate_a_post
383
+ old_post = Post.create(
384
+ :title => "Hello world",
385
+ :contents => "Lorum ipsum"
386
+ )
387
+
388
+ old_post.class.amoeba do
389
+ prepend :contents => "Here's a copy: "
390
+ end
391
+
392
+ new_post = old_post.dup
393
+
394
+ new_post.title # should be "Copy of Hello world"
395
+ new_post.contents # should be "Here's a copy: Lorum ipsum"
396
+ new_post.save
397
+ end
398
+ end
399
+
400
+ ### Config-Block Reference
401
+
402
+ Here is a static reference to the available configuration methods, usable within the amoeba block on your rails models.
403
+
404
+ `enable`
405
+
406
+ Enables amoeba in the default style of copying all known associated child records.
407
+
408
+ `include_field`
409
+
410
+ Adds a field to the list of fields which should be copied. All associations not in this list will not be copied. This method may be called multiple times, once per desired field, or you may pass an array of field names.
411
+
412
+ `exclude_field`
413
+
414
+ Adds a field to the list of fields which should not be copied. Only the associations that are not in this list will be copied. This method may be called multiple times, once per desired field, or you may pass an array of field names.
415
+
416
+ `nullify`
417
+
418
+ Adds a field to the list of non-association based fields which should be set to nil during copy. All fields in this list will be set to `nil` - note that any nullified field will be given its default value if a default value exists on this model's migration. This method may be called multiple times, once per desired field, or you may pass an array of field names.
419
+
420
+ `prepend`
421
+
422
+ Prefix a field with some text. This only works for string fields. Accepts a hash of fields to prepend. The keys are the field names and the values are the prefix strings. An example scenario would be to add a string such as "Copy of " to your title field. Don't forget to include extra space to the right if you want it.
423
+
424
+ `append`
425
+
426
+ Append some text to a field. This only works for string fields. Accepts a hash of fields to prepend. The keys are the field names and the values are the prefix strings. An example would be to add " (copied version)" to your description field. Don't forget to add a leading space if you want it.
427
+
428
+ `regex`
429
+
430
+ Globally search and replace the field for a given pattern. Accepts a hash of fields to run search and replace upon. The keys are the field names and the values are each a hash with information about what to find and what to replace it with. in the form of . An example would be to replace all occurrences of the word "dog" with the word "cat", the parameter hash would look like this `:contents => {:replace => /dog/, :with => "cat"}`
431
+
432
+ ## Known Limitations and Issues
433
+
434
+ Amoeba does not yet recognize advanced versions of `has_and_belongs_to_many` such as `has_and_belongs_to_many :foo, :through => :bar`. Amoeba does not copy the actual HABMT child records but rather simply adds records to the M:M breakout table to associate the new parent copy with the same records that the original parent were associated with. In other words, it doesn't duplicate your tags or categories, but merely reassociates your parent copy with the same tags or categories that the old parent had.
435
+
436
+ The regular expression preprocessor uses case-sensitive `String#gsub`. Given the performance decreases inherrent in using regular expressions already, the fact that character classes can essentially account for case-insensitive searches, the desire to keep the DSL simple and the general use cases for this gem, I don't see a good reason to add yet more decision based conditional syntax to accommodate using case-insensitive searches or singular replacements with `String#sub`. If you find yourself wanting either of these features, by all means fork the code base and if you like your changes, submit a pull request.
437
+
438
+ The behavior when copying nested hierarchical models is undefined. Copying a category model which has a `parent_id` field pointing to the parent category, for example, is currently undefined. The behavior when copying polymorphic `has_many` associations is also undefined. Support for these types of associations is planned for a future release.
439
+
440
+ ### For Developers
441
+
442
+ You may run the rspec tests like this:
443
+
444
+ bundle exec rspec spec
445
+
446
+ ## TODO
447
+
448
+ Write more tests.... anyone?
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,29 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "amoeba/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "amoeba"
7
+ s.version = Amoeba::VERSION
8
+ s.authors = ["Vaughn Draughon"]
9
+ s.email = "vaughn@rocksolidwebdesign.com"
10
+ s.homepage = "http://github.com/rocksolidwebdesign/amoeba"
11
+ s.license = "BSD"
12
+ s.summary = %q{Easy copying of rails models and their child associations.}
13
+ s.description = %q{An extension to ActiveRecord to allow the duplication method to also copy associated children, with recursive support for nested of grandchildren. The behavior is controllable with a simple DSL both on your rails models and on the fly, i.e. per instance. Numerous configuration options and styles and preprocessing directives are included for power and flexibility.}
14
+
15
+ s.rubyforge_project = "amoeba"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+
22
+ # specify any dependencies here; for example:
23
+ s.add_development_dependency "bundler", ">= 1.0.0"
24
+ s.add_development_dependency "rspec", "~> 2.3"
25
+
26
+ s.add_development_dependency "sqlite3-ruby"
27
+
28
+ s.add_dependency "activerecord", ">= 3.0"
29
+ end
@@ -0,0 +1,260 @@
1
+ require "amoeba/version"
2
+
3
+ module Amoeba
4
+ module ClassMethods
5
+ def amoeba(&block)
6
+ @config ||= Amoeba::ClassMethods::Config.new
7
+ @config.instance_eval(&block) if block_given?
8
+ @config
9
+ end
10
+
11
+ class Config
12
+ # Getters {{{
13
+ def enabled
14
+ @enabled ||= false
15
+ @enabled
16
+ end
17
+
18
+ def do_preproc
19
+ @do_preproc ||= false
20
+ @do_preproc
21
+ end
22
+
23
+ def includes
24
+ @includes ||= []
25
+ @includes
26
+ end
27
+
28
+ def excludes
29
+ @excludes ||= []
30
+ @excludes
31
+ end
32
+
33
+ def known_macros
34
+ @known_macros ||= [:has_one, :has_many, :has_and_belongs_to_many]
35
+ @known_macros
36
+ end
37
+
38
+ def null_fields
39
+ @null_fields ||= []
40
+ @null_fields
41
+ end
42
+
43
+ def prefixes
44
+ @prefixes ||= {}
45
+ @prefixes
46
+ end
47
+
48
+ def suffixes
49
+ @suffixes ||= {}
50
+ @suffixes
51
+ end
52
+
53
+ def regexes
54
+ @regexes ||= {}
55
+ @regexes
56
+ end
57
+ # }}}
58
+
59
+ # Setters (Config DSL) {{{
60
+ def enable
61
+ @enabled = true
62
+ end
63
+
64
+ def disable
65
+ @enabled = false
66
+ end
67
+
68
+ def include_field(value=nil)
69
+ @enabled ||= true
70
+ @includes ||= []
71
+ if value.is_a?(Array)
72
+ @includes = value
73
+ else
74
+ @includes << value if value
75
+ end
76
+ @includes
77
+ end
78
+
79
+ def exclude_field(value=nil)
80
+ @enabled ||= true
81
+ @excludes ||= []
82
+ if value.is_a?(Array)
83
+ @excludes = value
84
+ else
85
+ @excludes << value if value
86
+ end
87
+ @excludes
88
+ end
89
+
90
+ def recognize(value=nil)
91
+ @enabled ||= true
92
+ @known_macros ||= []
93
+ if value.is_a?(Array)
94
+ @known_macros = value
95
+ else
96
+ @known_macros << value if value
97
+ end
98
+ @known_macros
99
+ end
100
+
101
+ def nullify(value=nil)
102
+ @do_preproc ||= true
103
+ @null_fields ||= []
104
+ if value.is_a?(Array)
105
+ @null_fields = value
106
+ else
107
+ @null_fields << value if value
108
+ end
109
+ @null_fields
110
+ end
111
+
112
+ def prepend(defs=nil)
113
+ @do_preproc ||= true
114
+ @prefixes ||= {}
115
+ if defs.is_a?(Array)
116
+ @prefixes = {}
117
+
118
+ defs.each do |d|
119
+ d.each do |k,v|
120
+ @prefixes[k] = v if v
121
+ end
122
+ end
123
+ else
124
+ defs.each do |k,v|
125
+ @prefixes[k] = v if v
126
+ end
127
+ end
128
+ @prefixes
129
+ end
130
+
131
+ def append(defs=nil)
132
+ @do_preproc ||= true
133
+ @suffixes ||= {}
134
+ if defs.is_a?(Array)
135
+ @suffixes = {}
136
+
137
+ defs.each do |d|
138
+ d.each do |k,v|
139
+ @suffixes[k] = v if v
140
+ end
141
+ end
142
+ else
143
+ defs.each do |k,v|
144
+ @suffixes[k] = v if v
145
+ end
146
+ end
147
+ @suffixes
148
+ end
149
+
150
+ def regex(defs=nil)
151
+ @do_preproc ||= true
152
+ @regexes ||= {}
153
+ if defs.is_a?(Array)
154
+ @regexes = {}
155
+
156
+ defs.each do |d|
157
+ d.each do |k,v|
158
+ @regexes[k] = v if v
159
+ end
160
+ end
161
+ else
162
+ defs.each do |k,v|
163
+ @regexes[k] = v if v
164
+ end
165
+ end
166
+ @regexes
167
+ end
168
+ # }}}
169
+ end
170
+ end
171
+
172
+ module InstanceMethods
173
+ def amoeba_conf
174
+ self.class.amoeba
175
+ end
176
+
177
+ def amo_process_association(relation_name, settings)
178
+ if not amoeba_conf.known_macros.include?(settings.macro)
179
+ return
180
+ end
181
+
182
+ case settings.macro
183
+ when :has_one
184
+ old_obj = self.send(relation_name)
185
+
186
+ copy_of_obj = old_obj.dup
187
+ copy_of_obj[:"#{settings.foreign_key}"] = nil
188
+
189
+ @result.send(:"#{relation_name}=", copy_of_obj)
190
+ when :has_many
191
+ self.send(relation_name).each do |old_obj|
192
+ copy_of_obj = old_obj.dup
193
+ copy_of_obj[:"#{settings.foreign_key}"] = nil
194
+
195
+ # associate this new child to the new parent object
196
+ @result.send(relation_name) << copy_of_obj
197
+ end
198
+ when :has_and_belongs_to_many
199
+ self.send(relation_name).each do |old_obj|
200
+ # associate this new child to the new parent object
201
+ @result.send(relation_name) << old_obj
202
+ end
203
+ end
204
+ end
205
+
206
+ def dup(options={})
207
+ @result = super()
208
+
209
+ if amoeba_conf.enabled
210
+ if amoeba_conf.includes.count > 0
211
+ amoeba_conf.includes.each do |i|
212
+ r = self.class.reflect_on_association i
213
+ amo_process_association(i, r)
214
+ end
215
+ elsif amoeba_conf.excludes.count > 0
216
+ reflections.each do |r|
217
+ if not amoeba_conf.excludes.include?(r[0])
218
+ amo_process_association(r[0], r[1])
219
+ end
220
+ end
221
+ else
222
+ reflections.each do |r|
223
+ amo_process_association(r[0], r[1])
224
+ end
225
+ end
226
+ end
227
+
228
+ if amoeba_conf.do_preproc
229
+ preprocess_parent_copy
230
+ end
231
+
232
+ @result
233
+ end
234
+
235
+ def preprocess_parent_copy
236
+ # nullify any fields the user has configured
237
+ amoeba_conf.null_fields.each do |n|
238
+ @result[n] = nil
239
+ end
240
+
241
+ # prepend any extra strings to indicate uniqueness of the new record(s)
242
+ amoeba_conf.prefixes.each do |field,prefix|
243
+ @result[field] = "#{prefix}#{@result[field]}"
244
+ end
245
+
246
+ # postpend any extra strings to indicate uniqueness of the new record(s)
247
+ amoeba_conf.suffixes.each do |field,suffix|
248
+ @result[field] = "#{@result[field]}#{suffix}"
249
+ end
250
+
251
+ # regex andy fields that need changing
252
+ amoeba_conf.regexes.each do |field,action|
253
+ @result[field].gsub!(action[:replace], action[:with])
254
+ end
255
+ end
256
+ end
257
+ end
258
+
259
+ ActiveRecord::Base.send :extend, Amoeba::ClassMethods
260
+ ActiveRecord::Base.send :include, Amoeba::InstanceMethods
@@ -0,0 +1,3 @@
1
+ module Amoeba
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,45 @@
1
+ require 'active_record'
2
+ require 'spec_helper'
3
+
4
+ describe "amoeba" do
5
+ context "dup" do
6
+ it "duplicates associated child records" do
7
+ old_post = Post.find(1)
8
+ old_post.comments.map(&:contents).include?("I love it!").should be true
9
+
10
+ old_post.class.amoeba do
11
+ prepend :contents => "Here's a copy: "
12
+ end
13
+
14
+ new_post = old_post.dup
15
+
16
+ start_tag_count = Tag.all.count
17
+ start_post_count = Post.all.count
18
+ start_comment_count = Comment.all.count
19
+ start_rating_count = Rating.all.count
20
+ start_postconfig_count = PostConfig.all.count
21
+ rs = ActiveRecord::Base.connection.select_one('SELECT COUNT(*) AS tag_count FROM posts_tags')
22
+ start_posttag_count = rs["tag_count"]
23
+
24
+ new_post.save
25
+
26
+ end_tag_count = Tag.all.count
27
+ end_post_count = Post.all.count
28
+ end_comment_count = Comment.all.count
29
+ end_rating_count = Rating.all.count
30
+ end_postconfig_count = PostConfig.all.count
31
+ rs = ActiveRecord::Base.connection.select_one('SELECT COUNT(*) AS tag_count FROM posts_tags')
32
+ end_posttag_count = rs["tag_count"]
33
+
34
+ end_tag_count.should == start_tag_count
35
+ end_post_count.should == start_post_count * 2
36
+ end_comment_count.should == start_comment_count * 2
37
+ end_rating_count.should == start_rating_count * 2
38
+ end_postconfig_count.should == start_postconfig_count * 2
39
+ end_posttag_count.should == start_posttag_count * 2
40
+
41
+ new_post.title.should == "Copy of #{old_post.title}"
42
+ new_post.contents.should == "Here's a copy: #{old_post.contents.gsub(/dog/, 'cat')} (copied version)"
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,8 @@
1
+ require 'amoeba'
2
+
3
+ ActiveRecord::Base.establish_connection(:adapter => "sqlite3",
4
+ :database => File.dirname(__FILE__) + "/test.sqlite3")
5
+
6
+ load File.dirname(__FILE__) + '/support/schema.rb'
7
+ load File.dirname(__FILE__) + '/support/models.rb'
8
+ load File.dirname(__FILE__) + '/support/data.rb'
@@ -0,0 +1,41 @@
1
+ u1 = User.create(:name => "Robert Johnson", :email => "bob@crossroads.com")
2
+ u2 = User.create(:name => "Miles Davis", :email => "miles@kindofblue.com")
3
+
4
+ t = Topic.create(:title => "Ponies", :description => "Lets talk about my ponies.")
5
+
6
+ # First Post {{{
7
+ p1 = t.posts.create(:author => u1, :title => "My little pony", :contents => "Lorum ipsum dolor rainbow bright. I like dogs, dogs are awesome.")
8
+ f1 = p1.create_post_config(:is_visible => true, :is_open => false, :password => 'abcdefg123')
9
+ c1 = p1.comments.create(:contents => "I love it!")
10
+ c1.ratings.create(:num_stars => 5)
11
+ c1.ratings.create(:num_stars => 5)
12
+ c1.ratings.create(:num_stars => 4)
13
+ c1.ratings.create(:num_stars => 3)
14
+ c1.ratings.create(:num_stars => 5)
15
+ c1.ratings.create(:num_stars => 5)
16
+
17
+ c2 = p1.comments.create(:contents => "I hate it!")
18
+ c2.ratings.create(:num_stars => 3)
19
+ c2.ratings.create(:num_stars => 1)
20
+ c2.ratings.create(:num_stars => 4)
21
+ c2.ratings.create(:num_stars => 1)
22
+ c2.ratings.create(:num_stars => 1)
23
+ c2.ratings.create(:num_stars => 2)
24
+
25
+ c3 = p1.comments.create(:contents => "zomg kthxbbq!!11!!!1!eleven!!")
26
+ c3.ratings.create(:num_stars => 0)
27
+ c3.ratings.create(:num_stars => 0)
28
+ c3.ratings.create(:num_stars => 1)
29
+ c3.ratings.create(:num_stars => 2)
30
+ c3.ratings.create(:num_stars => 1)
31
+ c3.ratings.create(:num_stars => 0)
32
+
33
+ t1 = Tag.create(:value => "funny")
34
+ t2 = Tag.create(:value => "wtf")
35
+ t3 = Tag.create(:value => "cats")
36
+
37
+ p1.tags << t1
38
+ p1.tags << t2
39
+ p1.tags << t3
40
+ p1.save
41
+ # }}}
@@ -0,0 +1,43 @@
1
+ class Topic < ActiveRecord::Base
2
+ has_many :posts
3
+ end
4
+
5
+ class Post < ActiveRecord::Base
6
+ belongs_to :topic
7
+ belongs_to :author, :class_name => 'User'
8
+ has_one :post_config
9
+ has_many :comments
10
+ has_and_belongs_to_many :tags
11
+
12
+ amoeba do
13
+ enable
14
+ prepend :title => "Copy of "
15
+ append :contents => " (copied version)"
16
+ regex :contents => {:replace => /dog/, :with => 'cat'}
17
+ end
18
+ end
19
+
20
+ class PostConfig < ActiveRecord::Base
21
+ belongs_to :post
22
+ end
23
+
24
+ class Comment < ActiveRecord::Base
25
+ belongs_to :post
26
+ has_many :ratings
27
+
28
+ amoeba do
29
+ enable
30
+ end
31
+ end
32
+
33
+ class Rating < ActiveRecord::Base
34
+ belongs_to :comment
35
+ end
36
+
37
+ class Tag < ActiveRecord::Base
38
+ has_and_belongs_to_many :posts
39
+ end
40
+
41
+ class User < ActiveRecord::Base
42
+ has_many :posts
43
+ end
@@ -0,0 +1,61 @@
1
+ ActiveRecord::Schema.define do
2
+ self.verbose = false
3
+
4
+ create_table :topics, :force => true do |t|
5
+ t.string :title
6
+ t.string :description
7
+ t.timestamps
8
+ end
9
+
10
+ create_table :posts, :force => true do |t|
11
+ t.integer :topic_id
12
+ t.integer :author_id
13
+ t.string :title
14
+ t.string :contents
15
+ t.timestamps
16
+ end
17
+
18
+ create_table :post_configs, :force => true do |t|
19
+ t.integer :post_id
20
+ t.integer :is_visible
21
+ t.integer :is_open
22
+ t.string :password
23
+ t.timestamps
24
+ end
25
+
26
+ create_table :comments, :force => true do |t|
27
+ t.integer :post_id
28
+ t.string :contents
29
+ t.timestamps
30
+ end
31
+
32
+ create_table :comments, :force => true do |t|
33
+ t.integer :post_id
34
+ t.string :contents
35
+ t.timestamps
36
+ end
37
+
38
+ create_table :ratings, :force => true do |t|
39
+ t.integer :comment_id
40
+ t.string :num_stars
41
+ t.timestamps
42
+ end
43
+
44
+ create_table :tags, :force => true do |t|
45
+ t.integer :post_id
46
+ t.string :value
47
+ t.timestamps
48
+ end
49
+
50
+ create_table :users, :force => true do |t|
51
+ t.integer :post_id
52
+ t.string :name
53
+ t.string :email
54
+ t.timestamps
55
+ end
56
+
57
+ create_table :posts_tags, :force => true do |t|
58
+ t.integer :post_id
59
+ t.integer :tag_id
60
+ end
61
+ end
Binary file
metadata ADDED
@@ -0,0 +1,107 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: amoeba
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Vaughn Draughon
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-02-28 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: &20942620 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 1.0.0
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *20942620
25
+ - !ruby/object:Gem::Dependency
26
+ name: rspec
27
+ requirement: &20941880 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ~>
31
+ - !ruby/object:Gem::Version
32
+ version: '2.3'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *20941880
36
+ - !ruby/object:Gem::Dependency
37
+ name: sqlite3-ruby
38
+ requirement: &20940660 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *20940660
47
+ - !ruby/object:Gem::Dependency
48
+ name: activerecord
49
+ requirement: &20956240 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: *20956240
58
+ description: An extension to ActiveRecord to allow the duplication method to also
59
+ copy associated children, with recursive support for nested of grandchildren. The
60
+ behavior is controllable with a simple DSL both on your rails models and on the
61
+ fly, i.e. per instance. Numerous configuration options and styles and preprocessing
62
+ directives are included for power and flexibility.
63
+ email: vaughn@rocksolidwebdesign.com
64
+ executables: []
65
+ extensions: []
66
+ extra_rdoc_files: []
67
+ files:
68
+ - .gitignore
69
+ - .rvmrc
70
+ - Gemfile
71
+ - README.md
72
+ - Rakefile
73
+ - amoeba.gemspec
74
+ - lib/amoeba.rb
75
+ - lib/amoeba/version.rb
76
+ - spec/lib/amoeba_spec.rb
77
+ - spec/spec_helper.rb
78
+ - spec/support/data.rb
79
+ - spec/support/models.rb
80
+ - spec/support/schema.rb
81
+ - spec/test.sqlite3
82
+ homepage: http://github.com/rocksolidwebdesign/amoeba
83
+ licenses:
84
+ - BSD
85
+ post_install_message:
86
+ rdoc_options: []
87
+ require_paths:
88
+ - lib
89
+ required_ruby_version: !ruby/object:Gem::Requirement
90
+ none: false
91
+ requirements:
92
+ - - ! '>='
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ required_rubygems_version: !ruby/object:Gem::Requirement
96
+ none: false
97
+ requirements:
98
+ - - ! '>='
99
+ - !ruby/object:Gem::Version
100
+ version: '0'
101
+ requirements: []
102
+ rubyforge_project: amoeba
103
+ rubygems_version: 1.8.10
104
+ signing_key:
105
+ specification_version: 3
106
+ summary: Easy copying of rails models and their child associations.
107
+ test_files: []