amoeba 1.2.0 → 1.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.rvmrc +1 -1
- data/README.md +299 -190
- data/amoeba.gemspec +1 -1
- data/lib/amoeba.rb +30 -5
- data/lib/amoeba/version.rb +1 -1
- data/spec/lib/amoeba_spec.rb +10 -0
- data/spec/support/data.rb +4 -1
- data/spec/support/models.rb +16 -6
- data/spec/support/schema.rb +7 -0
- metadata +32 -12
data/.rvmrc
CHANGED
@@ -1 +1 @@
|
|
1
|
-
rvm use --create 1.9.3-
|
1
|
+
rvm use --create 1.9.3-p125@amoeba
|
data/README.md
CHANGED
@@ -4,29 +4,19 @@ Easy copying of rails associations such as `has_many`.
|
|
4
4
|
|
5
5
|
![amoebalogo](http://rocksolidwebdesign.com/wp_cms/wp-content/uploads/2012/02/amoeba_logo.jpg)
|
6
6
|
|
7
|
-
## Installation
|
8
|
-
|
9
|
-
is hopefully as you would expect:
|
10
|
-
|
11
|
-
gem install amoeba
|
12
|
-
|
13
|
-
or just add it to your Gemfile:
|
14
|
-
|
15
|
-
gem 'amoeba'
|
16
|
-
|
17
7
|
## What?
|
18
8
|
|
19
9
|
The goal was to be able to easily and quickly reproduce ActiveRecord objects including their children, for example copying a blog post maintaining its associated tags or categories.
|
20
10
|
|
21
11
|
This gem is named "Amoeba" because amoebas are (small life forms that are) good at reproducing. Their children and grandchildren also reproduce themselves quickly and easily.
|
22
12
|
|
23
|
-
|
13
|
+
### Technical Details
|
24
14
|
|
25
15
|
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.
|
26
16
|
|
27
17
|
Rails 3.2 compatible.
|
28
18
|
|
29
|
-
|
19
|
+
### Features
|
30
20
|
|
31
21
|
- Supports the following association types
|
32
22
|
- `has_many`
|
@@ -50,6 +40,16 @@ Rails 3.2 compatible.
|
|
50
40
|
|
51
41
|
## Usage
|
52
42
|
|
43
|
+
### Installation
|
44
|
+
|
45
|
+
is hopefully as you would expect:
|
46
|
+
|
47
|
+
gem install amoeba
|
48
|
+
|
49
|
+
or just add it to your Gemfile:
|
50
|
+
|
51
|
+
gem 'amoeba'
|
52
|
+
|
53
53
|
Configure your models with one of the styles below and then just run the `dup` method on your model as you normally would:
|
54
54
|
|
55
55
|
p = Post.create(:title => "Hello World!", :content => "Lorum ipsum dolor")
|
@@ -67,11 +67,11 @@ By default, when enabled, amoeba will copy any and all associated child records
|
|
67
67
|
|
68
68
|
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.
|
69
69
|
|
70
|
-
|
70
|
+
### Configuration
|
71
71
|
|
72
72
|
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.
|
73
73
|
|
74
|
-
|
74
|
+
#### Indiscriminate Style
|
75
75
|
|
76
76
|
This is the most basic usage case and will simply enable the copying of any known associations.
|
77
77
|
|
@@ -101,7 +101,7 @@ simply add the amoeba configuration block to your model and call the enable meth
|
|
101
101
|
|
102
102
|
Child records will be automatically copied when you run the dup method.
|
103
103
|
|
104
|
-
|
104
|
+
#### Inclusive Style
|
105
105
|
|
106
106
|
If you only want some of the associations copied but not others, you may use the inclusive style:
|
107
107
|
|
@@ -158,7 +158,7 @@ These examples will copy the post's tags and authors but not its comments.
|
|
158
158
|
|
159
159
|
The inclusive style, when used, will automatically disable any ther style that was previously selected.
|
160
160
|
|
161
|
-
|
161
|
+
#### Exclusive Style
|
162
162
|
|
163
163
|
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:
|
164
164
|
|
@@ -180,7 +180,7 @@ This example does the same thing as the inclusive style example, it will copy th
|
|
180
180
|
|
181
181
|
The exclusive style, when used, will automatically disable any other style that was previously selected, so if you selected include fields, and then you choose some exclude fields, the `exclude_field` method will disable the previously slected inclusive style and wipe out any corresponding include fields.
|
182
182
|
|
183
|
-
|
183
|
+
#### Cloning
|
184
184
|
|
185
185
|
If you are using a Many-to-Many relationship, you may tell amoeba to actually make duplicates of the original related records rather than merely maintaining association with the original records. Cloning is easy, merely tell amoeba which fields to clone in the same way you tell it which fields to include or exclude.
|
186
186
|
|
@@ -212,217 +212,119 @@ If you are using a Many-to-Many relationship, you may tell amoeba to actually ma
|
|
212
212
|
|
213
213
|
This example will actually duplicate the warnings and widgets in the database. If there were originally 3 warnings in the database then, upon duplicating a post, you will end up with 6 warnings in the database. This is in contrast to the default behavior where your new post would merely be re-associated with any previously existing warnings and those warnings themselves would not be duplicated.
|
214
214
|
|
215
|
-
|
216
|
-
|
217
|
-
If you are using the Single Table Inheritance provided by ActiveRecord, you may cause amoeba to automatically process child classes in the same way as their parents. All you need to do is call the `propagate` method within the amoeba block of the parent class and all child classes should copy in a similar manner.
|
218
|
-
|
219
|
-
create_table :products, :force => true do |t|
|
220
|
-
t.string :type # this is the STI column
|
215
|
+
### Limiting Association Types
|
221
216
|
|
222
|
-
|
223
|
-
t.string :title
|
224
|
-
t.decimal :price
|
217
|
+
By default, amoeba recognizes and attemps to copy any children of the following association types:
|
225
218
|
|
226
|
-
|
227
|
-
|
228
|
-
|
219
|
+
- has one
|
220
|
+
- has many
|
221
|
+
- has and belongs to many
|
229
222
|
|
230
|
-
|
231
|
-
t.integer :ram_size
|
232
|
-
t.integer :hard_drive_size
|
233
|
-
end
|
223
|
+
You may control which association types amoeba applies itself to by using the `recognize` method within the amoeba configuration block.
|
234
224
|
|
235
|
-
class
|
236
|
-
|
237
|
-
|
225
|
+
class Post < ActiveRecord::Base
|
226
|
+
has_one :config
|
227
|
+
has_many :comments
|
228
|
+
has_and_belongs_to_many :tags
|
238
229
|
|
239
230
|
amoeba do
|
240
|
-
|
241
|
-
propagate
|
231
|
+
recognize [:has_one, :has_and_belongs_to_many]
|
242
232
|
end
|
243
233
|
end
|
244
234
|
|
245
|
-
class
|
246
|
-
|
247
|
-
|
248
|
-
class Computer < Product
|
235
|
+
class Comment < ActiveRecord::Base
|
236
|
+
belongs_to :post
|
249
237
|
end
|
250
238
|
|
251
|
-
class
|
252
|
-
|
253
|
-
my_shirt = Shirt.find(1)
|
254
|
-
my_shirt.dup
|
255
|
-
my_shirt.save
|
256
|
-
|
257
|
-
# this shirt should now:
|
258
|
-
# - have its own copy of all parent images
|
259
|
-
# - be in the same categories as the parent
|
260
|
-
end
|
239
|
+
class Tag < ActiveRecord::Base
|
240
|
+
has_and_belongs_to_many :posts
|
261
241
|
end
|
262
242
|
|
263
|
-
This example
|
264
|
-
|
265
|
-
### Parenting Style
|
266
|
-
|
267
|
-
By default, propagation uses submissive parenting, meaning the config settings on the parent will be applied, but any child settings, if present, will either add to or overwrite the parent settings depending on how you call the DSL methods.
|
243
|
+
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.
|
268
244
|
|
269
|
-
|
245
|
+
### Nested Association Validation
|
270
246
|
|
271
|
-
|
247
|
+
If you end up with some validation issues when trying to validate the presence of a child's `belongs_to` association, just be sure to include the `:inverse_of` declaration on your relationships and all should be well.
|
272
248
|
|
273
|
-
|
249
|
+
For example this will throw a validation error saying that your posts are invalid:
|
274
250
|
|
275
|
-
class
|
276
|
-
has_many :
|
277
|
-
has_and_belongs_to_many :sections
|
251
|
+
class Author < ActiveRecord::Base
|
252
|
+
has_many :posts
|
278
253
|
|
279
254
|
amoeba do
|
280
|
-
|
281
|
-
propagate :relaxed
|
255
|
+
enable
|
282
256
|
end
|
283
257
|
end
|
284
258
|
|
285
|
-
class
|
286
|
-
|
287
|
-
|
288
|
-
prepend :title => "Copy of "
|
289
|
-
end
|
290
|
-
|
291
|
-
In this example, the conflicting `include_field` settings on the child will be ignored and the parent `exclude_field` setting will be used, while the `prepend` setting on the child will be honored because it doesn't conflict with the parent.
|
292
|
-
|
293
|
-
#### Strict Parenting
|
294
|
-
|
295
|
-
The `:strict` style will ignore child settings altogether and inherit any parent settings.
|
296
|
-
|
297
|
-
class Product < ActiveRecord::Base
|
298
|
-
has_many :images
|
299
|
-
has_and_belongs_to_many :sections
|
259
|
+
class Post < ActiveRecord::Base
|
260
|
+
belongs_to :author
|
261
|
+
validates_presence_of :author
|
300
262
|
|
301
263
|
amoeba do
|
302
|
-
|
303
|
-
propagate :strict
|
264
|
+
enable
|
304
265
|
end
|
305
266
|
end
|
306
267
|
|
307
|
-
|
308
|
-
|
309
|
-
include_field :sections
|
310
|
-
prepend :title => "Copy of "
|
311
|
-
end
|
312
|
-
|
313
|
-
In this example, the only processing that will happen when a Shirt is duplicated is whatever processing is allowed by the parent. So in this case the parent's `exclude_field` directive takes precedence over the child's `include_field` settings, and not only that, but none of the other settings for the child are used either. The `prepend` setting of the child is completely ignored.
|
314
|
-
|
315
|
-
#### Parenting and Precedence
|
316
|
-
|
317
|
-
Because of the two general forms of DSL config parameter usage, you may wish to make yourself mindful of how your coding style will affect the outcome of duplicating an object.
|
318
|
-
|
319
|
-
Just remember that:
|
320
|
-
|
321
|
-
* If you pass an array you will wipe all previous settings
|
322
|
-
* If you pass single values, you will add to currently existing settings
|
323
|
-
|
324
|
-
This means that, for example:
|
325
|
-
|
326
|
-
* When using the submissive parenting style, you can child take full precedence on a per field basis by passing an array of config values. This will cause the setting from the parent to be overridden instead of added to.
|
327
|
-
* When using the relaxed parenting style, you can still let the parent take precedence on a per field basis by passing an array of config values. This will cause the setting for that child to be overridden instead of added to.
|
268
|
+
author = Author.find(1)
|
269
|
+
author.dup
|
328
270
|
|
329
|
-
|
271
|
+
author.save # this will fail validation
|
330
272
|
|
331
|
-
|
273
|
+
Wheras this will work fine:
|
332
274
|
|
333
|
-
class
|
334
|
-
has_many :
|
335
|
-
has_and_belongs_to_many :sections
|
275
|
+
class Author < ActiveRecord::Base
|
276
|
+
has_many :posts, :inverse_of => :author
|
336
277
|
|
337
278
|
amoeba do
|
338
|
-
|
339
|
-
propagate
|
279
|
+
enable
|
340
280
|
end
|
341
281
|
end
|
342
282
|
|
343
|
-
class
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
The next version will use only the child settings because passing an array will override any previous settings rather than adding to them and the child config takes precedence in the `submissive` parenting style. So in this case only the sections will be copied.
|
348
|
-
|
349
|
-
class Product < ActiveRecord::Base
|
350
|
-
has_many :images
|
351
|
-
has_and_belongs_to_many :sections
|
283
|
+
class Post < ActiveRecord::Base
|
284
|
+
belongs_to :author, :inverse_of => :posts
|
285
|
+
validates_presence_of :author
|
352
286
|
|
353
287
|
amoeba do
|
354
|
-
|
355
|
-
propagate
|
288
|
+
enable
|
356
289
|
end
|
357
290
|
end
|
358
291
|
|
359
|
-
|
360
|
-
|
361
|
-
end
|
292
|
+
author = Author.find(1)
|
293
|
+
author.dup
|
362
294
|
|
363
|
-
|
295
|
+
author.save # this will pass validation
|
364
296
|
|
365
|
-
This
|
366
|
-
|
367
|
-
class Product < ActiveRecord::Base
|
368
|
-
has_many :images
|
369
|
-
has_and_belongs_to_many :sections
|
297
|
+
This issue is not amoeba specific and also occurs when creating new objects using `accepts_nested_attributes_for`, like this:
|
370
298
|
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
end
|
299
|
+
class Author < ActiveRecord::Base
|
300
|
+
has_many :posts
|
301
|
+
accepts_nested_attributes_for :posts
|
375
302
|
end
|
376
303
|
|
377
|
-
class
|
378
|
-
|
304
|
+
class Post < ActiveRecord::Base
|
305
|
+
belongs_to :author
|
306
|
+
validates_presence_of :author
|
379
307
|
end
|
380
308
|
|
381
|
-
|
382
|
-
|
383
|
-
class Product < ActiveRecord::Base
|
384
|
-
has_many :images
|
385
|
-
has_and_belongs_to_many :sections
|
309
|
+
# this will fail validation
|
310
|
+
author = Author.create({:name => "Jim Smith", :posts => [{:title => "Hello World", :contents => "Lorum ipsum dolor}]})
|
386
311
|
|
387
|
-
|
388
|
-
include_field [:images]
|
389
|
-
propagate
|
390
|
-
end
|
391
|
-
end
|
312
|
+
This issue with `accepts_nested_attributes_for` can also be solved by using `:inverse_of`, like this:
|
392
313
|
|
393
|
-
class
|
394
|
-
|
314
|
+
class Author < ActiveRecord::Base
|
315
|
+
has_many :posts, :inverse_of => :author
|
316
|
+
accepts_nested_attributes_for :posts
|
395
317
|
end
|
396
318
|
|
397
|
-
### Limiting Association Types
|
398
|
-
|
399
|
-
By default, amoeba recognizes and attemps to copy any children of the following association types:
|
400
|
-
|
401
|
-
- has one
|
402
|
-
- has many
|
403
|
-
- has and belongs to many
|
404
|
-
|
405
|
-
You may control which association types amoeba applies itself to by using the `recognize` method within the amoeba configuration block.
|
406
|
-
|
407
319
|
class Post < ActiveRecord::Base
|
408
|
-
|
409
|
-
|
410
|
-
has_and_belongs_to_many :tags
|
411
|
-
|
412
|
-
amoeba do
|
413
|
-
recognize [:has_one, :has_and_belongs_to_many]
|
414
|
-
end
|
415
|
-
end
|
416
|
-
|
417
|
-
class Comment < ActiveRecord::Base
|
418
|
-
belongs_to :post
|
320
|
+
belongs_to :author, :inverse_of => :posts
|
321
|
+
validates_presence_of :author
|
419
322
|
end
|
420
323
|
|
421
|
-
|
422
|
-
|
423
|
-
end
|
324
|
+
# this will pass validation
|
325
|
+
author = Author.create({:name => "Jim Smith", :posts => [{:title => "Hello World", :contents => "Lorum ipsum dolor}]})
|
424
326
|
|
425
|
-
|
327
|
+
The crux of the issue is that upon duplication, the new `Author` instance does not yet have an ID because it has not yet been persisted, so the `:posts` do not yet have an `:author_id` either, and thus no `:author` and thus they will fail validation. This issue may likely affect amoeba usage so if you get some validation failures, be sure to add `:inverse_of` to your models.
|
426
328
|
|
427
329
|
### Field Preprocessors
|
428
330
|
|
@@ -500,6 +402,8 @@ You may run a search and replace query on a copied object's field during the cop
|
|
500
402
|
|
501
403
|
#### Custom Methods
|
502
404
|
|
405
|
+
##### Customize
|
406
|
+
|
503
407
|
You may run a custom method or methods to do basically anything you like, simply pass a lambda block, or an array of lambda blocks to the `customize` directive. Each block must have the same form, meaning that each block must accept two parameters, the original object and the newly copied object. You may then do whatever you wish, like this:
|
504
408
|
|
505
409
|
class Post < ActiveRecord::Base
|
@@ -536,9 +440,27 @@ or this, using an array:
|
|
536
440
|
end
|
537
441
|
end
|
538
442
|
|
443
|
+
##### Override
|
444
|
+
|
445
|
+
Lambda blocks passed to customize run, by default, after all copying and field pre-processing. If you wish to run a method before any customization or field pre-processing, you may use `override` the cousin of `customize`. Usage is the same as above.
|
446
|
+
|
447
|
+
class Post < ActiveRecord::Base
|
448
|
+
amoeba do
|
449
|
+
prepend :title => "Hello world! "
|
450
|
+
|
451
|
+
override(lambda { |original_post,new_post|
|
452
|
+
if original_post.foo == "bar"
|
453
|
+
new_post.baz = "qux"
|
454
|
+
end
|
455
|
+
})
|
456
|
+
|
457
|
+
append :comments => "... know what I'm sayin?"
|
458
|
+
end
|
459
|
+
end
|
460
|
+
|
539
461
|
#### Chaining
|
540
462
|
|
541
|
-
You may apply a preprocessor to multiple fields at once.
|
463
|
+
You may apply a single preprocessor to multiple fields at once.
|
542
464
|
|
543
465
|
class Post < ActiveRecord::Base
|
544
466
|
amoeba do
|
@@ -739,29 +661,211 @@ You may control how amoeba copies your object, on the fly, by passing a configur
|
|
739
661
|
end
|
740
662
|
end
|
741
663
|
|
742
|
-
###
|
664
|
+
### Inheritance
|
665
|
+
|
666
|
+
If you are using the Single Table Inheritance provided by ActiveRecord, you may cause amoeba to automatically process child classes in the same way as their parents. All you need to do is call the `propagate` method within the amoeba block of the parent class and all child classes should copy in a similar manner.
|
667
|
+
|
668
|
+
create_table :products, :force => true do |t|
|
669
|
+
t.string :type # this is the STI column
|
670
|
+
|
671
|
+
# these belong to all products
|
672
|
+
t.string :title
|
673
|
+
t.decimal :price
|
674
|
+
|
675
|
+
# these are for shirts only
|
676
|
+
t.decimal :sleeve_length
|
677
|
+
t.decimal :collar_size
|
678
|
+
|
679
|
+
# these are for computers only
|
680
|
+
t.integer :ram_size
|
681
|
+
t.integer :hard_drive_size
|
682
|
+
end
|
683
|
+
|
684
|
+
class Product < ActiveRecord::Base
|
685
|
+
has_many :images
|
686
|
+
has_and_belongs_to_many :categories
|
687
|
+
|
688
|
+
amoeba do
|
689
|
+
enable
|
690
|
+
propagate
|
691
|
+
end
|
692
|
+
end
|
693
|
+
|
694
|
+
class Shirt < Product
|
695
|
+
end
|
696
|
+
|
697
|
+
class Computer < Product
|
698
|
+
end
|
699
|
+
|
700
|
+
class ProductsController
|
701
|
+
def some_method
|
702
|
+
my_shirt = Shirt.find(1)
|
703
|
+
my_shirt.dup
|
704
|
+
my_shirt.save
|
705
|
+
|
706
|
+
# this shirt should now:
|
707
|
+
# - have its own copy of all parent images
|
708
|
+
# - be in the same categories as the parent
|
709
|
+
end
|
710
|
+
end
|
711
|
+
|
712
|
+
This example should duplicate all the images and sections associated with this Shirt, which is a child of Product
|
713
|
+
|
714
|
+
#### Parenting Style
|
715
|
+
|
716
|
+
By default, propagation uses submissive parenting, meaning the config settings on the parent will be applied, but any child settings, if present, will either add to or overwrite the parent settings depending on how you call the DSL methods.
|
717
|
+
|
718
|
+
You may change this behavior, the so called "parenting style", to give preference to the parent settings or to ignore any and all child settings.
|
719
|
+
|
720
|
+
##### Relaxed Parenting
|
721
|
+
|
722
|
+
The `:relaxed` parenting style will prefer parent settings.
|
723
|
+
|
724
|
+
class Product < ActiveRecord::Base
|
725
|
+
has_many :images
|
726
|
+
has_and_belongs_to_many :sections
|
727
|
+
|
728
|
+
amoeba do
|
729
|
+
exclude_field :images
|
730
|
+
propagate :relaxed
|
731
|
+
end
|
732
|
+
end
|
733
|
+
|
734
|
+
class Shirt < Product
|
735
|
+
include_field :images
|
736
|
+
include_field :sections
|
737
|
+
prepend :title => "Copy of "
|
738
|
+
end
|
739
|
+
|
740
|
+
In this example, the conflicting `include_field` settings on the child will be ignored and the parent `exclude_field` setting will be used, while the `prepend` setting on the child will be honored because it doesn't conflict with the parent.
|
741
|
+
|
742
|
+
##### Strict Parenting
|
743
|
+
|
744
|
+
The `:strict` style will ignore child settings altogether and inherit any parent settings.
|
745
|
+
|
746
|
+
class Product < ActiveRecord::Base
|
747
|
+
has_many :images
|
748
|
+
has_and_belongs_to_many :sections
|
749
|
+
|
750
|
+
amoeba do
|
751
|
+
exclude_field :images
|
752
|
+
propagate :strict
|
753
|
+
end
|
754
|
+
end
|
755
|
+
|
756
|
+
class Shirt < Product
|
757
|
+
include_field :images
|
758
|
+
include_field :sections
|
759
|
+
prepend :title => "Copy of "
|
760
|
+
end
|
761
|
+
|
762
|
+
In this example, the only processing that will happen when a Shirt is duplicated is whatever processing is allowed by the parent. So in this case the parent's `exclude_field` directive takes precedence over the child's `include_field` settings, and not only that, but none of the other settings for the child are used either. The `prepend` setting of the child is completely ignored.
|
763
|
+
|
764
|
+
##### Parenting and Precedence
|
765
|
+
|
766
|
+
Because of the two general forms of DSL config parameter usage, you may wish to make yourself mindful of how your coding style will affect the outcome of duplicating an object.
|
767
|
+
|
768
|
+
Just remember that:
|
769
|
+
|
770
|
+
* If you pass an array you will wipe all previous settings
|
771
|
+
* If you pass single values, you will add to currently existing settings
|
772
|
+
|
773
|
+
This means that, for example:
|
774
|
+
|
775
|
+
* When using the submissive parenting style, you can child take full precedence on a per field basis by passing an array of config values. This will cause the setting from the parent to be overridden instead of added to.
|
776
|
+
* When using the relaxed parenting style, you can still let the parent take precedence on a per field basis by passing an array of config values. This will cause the setting for that child to be overridden instead of added to.
|
777
|
+
|
778
|
+
##### A Submissive Override Example
|
779
|
+
|
780
|
+
This version will use both the parent and child settings, so both the images and sections will be copied.
|
781
|
+
|
782
|
+
class Product < ActiveRecord::Base
|
783
|
+
has_many :images
|
784
|
+
has_and_belongs_to_many :sections
|
785
|
+
|
786
|
+
amoeba do
|
787
|
+
include_field :images
|
788
|
+
propagate
|
789
|
+
end
|
790
|
+
end
|
791
|
+
|
792
|
+
class Shirt < Product
|
793
|
+
include_field :sections
|
794
|
+
end
|
795
|
+
|
796
|
+
The next version will use only the child settings because passing an array will override any previous settings rather than adding to them and the child config takes precedence in the `submissive` parenting style. So in this case only the sections will be copied.
|
797
|
+
|
798
|
+
class Product < ActiveRecord::Base
|
799
|
+
has_many :images
|
800
|
+
has_and_belongs_to_many :sections
|
801
|
+
|
802
|
+
amoeba do
|
803
|
+
include_field :images
|
804
|
+
propagate
|
805
|
+
end
|
806
|
+
end
|
807
|
+
|
808
|
+
class Shirt < Product
|
809
|
+
include_field [:sections]
|
810
|
+
end
|
811
|
+
|
812
|
+
##### A Relaxed Override Example
|
813
|
+
|
814
|
+
This version will use both the parent and child settings, so both the images and sections will be copied.
|
815
|
+
|
816
|
+
class Product < ActiveRecord::Base
|
817
|
+
has_many :images
|
818
|
+
has_and_belongs_to_many :sections
|
819
|
+
|
820
|
+
amoeba do
|
821
|
+
include_field :images
|
822
|
+
propagate :relaxed
|
823
|
+
end
|
824
|
+
end
|
825
|
+
|
826
|
+
class Shirt < Product
|
827
|
+
include_field :sections
|
828
|
+
end
|
829
|
+
|
830
|
+
The next version will use only the parent settings because passing an array will override any previous settings rather than adding to them and the parent config takes precedence in the `relaxed` parenting style. So in this case only the images will be copied.
|
831
|
+
|
832
|
+
class Product < ActiveRecord::Base
|
833
|
+
has_many :images
|
834
|
+
has_and_belongs_to_many :sections
|
835
|
+
|
836
|
+
amoeba do
|
837
|
+
include_field [:images]
|
838
|
+
propagate
|
839
|
+
end
|
840
|
+
end
|
841
|
+
|
842
|
+
class Shirt < Product
|
843
|
+
include_field :sections
|
844
|
+
end
|
845
|
+
|
846
|
+
## Configuration Reference
|
743
847
|
|
744
848
|
Here is a static reference to the available configuration methods, usable within the amoeba block on your rails models.
|
745
849
|
|
746
|
-
|
850
|
+
### Controlling Associations
|
747
851
|
|
748
|
-
|
852
|
+
#### enable
|
749
853
|
|
750
854
|
Enables amoeba in the default style of copying all known associated child records. Using the enable method is only required if you wish to enable amoeba but you are not using either the `include_field` or `exclude_field` directives. If you use either inclusive or exclusive style, amoeba is automatically enabled for you, so calling `enable` would be redundant, though it won't hurt.
|
751
855
|
|
752
|
-
|
856
|
+
#### include_field
|
753
857
|
|
754
858
|
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. Passing a single symbol will add to the list of included fields. Passing an array will empty the list and replace it with the array you pass.
|
755
859
|
|
756
|
-
|
860
|
+
#### exclude_field
|
757
861
|
|
758
862
|
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. Passing a single symbol will add to the list of excluded fields. Passing an array will empty the list and replace it with the array you pass.
|
759
863
|
|
760
|
-
|
864
|
+
#### clone
|
761
865
|
|
762
866
|
Adds a field to the list of associations which should have their associated children actually cloned. This means for example, that instead of just maintaining original associations with previously existing tags, a copy will be made of each tag, and the new record will be associated with these new tag copies rather than the old tag copies. This method may be called multiple times, once per desired field, or you may pass an array of field names. Passing a single symbol will add to the list of excluded fields. Passing an array will empty the list and replace it with the array you pass.
|
763
867
|
|
764
|
-
|
868
|
+
#### propagate
|
765
869
|
|
766
870
|
This causes any inherited child models to take the same config settings when copied. This method may take up to one argument to control the so called "parenting style". The argument should be one of `strict`, `relaxed` or `submissive`.
|
767
871
|
|
@@ -775,7 +879,7 @@ Here is a static reference to the available configuration methods, usable within
|
|
775
879
|
|
776
880
|
will choose the strict parenting style of inherited settings.
|
777
881
|
|
778
|
-
|
882
|
+
#### raised
|
779
883
|
|
780
884
|
This causes any child to behave with a (potentially) different "parenting style" than its actual parent. This method takes up to a single parameter for which there are three options, `strict`, `relaxed` and `submissive`.
|
781
885
|
|
@@ -789,29 +893,29 @@ Here is a static reference to the available configuration methods, usable within
|
|
789
893
|
|
790
894
|
will choose the relaxed parenting style of inherited settings for this child. A parenting style set via the `raised` method takes precedence over the parenting style set using the `propagate` method.
|
791
895
|
|
792
|
-
|
896
|
+
### Pre-Processing Fields
|
793
897
|
|
794
|
-
|
898
|
+
#### nullify
|
795
899
|
|
796
900
|
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. Passing a single symbol will add to the list of null fields. Passing an array will empty the list and replace it with the array you pass.
|
797
901
|
|
798
|
-
|
902
|
+
#### prepend
|
799
903
|
|
800
904
|
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. Passing a hash will add each key value pair to the list of prepend directives. If you wish to empty the list of directives, you may pass the hash inside of an array like this `[{:title => "Copy of "}]`.
|
801
905
|
|
802
|
-
|
906
|
+
#### append
|
803
907
|
|
804
908
|
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. Passing a hash will add each key value pair to the list of append directives. If you wish to empty the list of directives, you may pass the hash inside of an array like this `[{:contents => " (copied version)"}]`.
|
805
909
|
|
806
|
-
|
910
|
+
#### set
|
807
911
|
|
808
912
|
Set a field to a given value. This sould work for almost any type of field. Accepts a hash of fields and the values you want them set to.. 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. Passing a hash will add each key value pair to the list of append directives. If you wish to empty the list of directives, you may pass the hash inside of an array like this `[{:approval_state => "open_for_editing"}]`.
|
809
913
|
|
810
|
-
|
914
|
+
#### regex
|
811
915
|
|
812
916
|
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"}`. Passing a hash will add each key value pair to the list of regex directives. If you wish to empty the list of directives, you may pass the hash inside of an array like this `[{:contents => {:replace => /dog/, :with => "cat"}]`.
|
813
917
|
|
814
|
-
|
918
|
+
#### customize
|
815
919
|
|
816
920
|
Runs a custom method so you can do basically whatever you want. All you need to do is pass a lambda block or an array of lambda blocks that take two parameters, the original object and the new object copy
|
817
921
|
|
@@ -825,12 +929,17 @@ The behavior when copying nested hierarchical models is undefined. Copying a cat
|
|
825
929
|
|
826
930
|
The behavior when copying polymorphic `has_many` associations is also undefined. Support for these types of associations is planned for a future release.
|
827
931
|
|
828
|
-
|
932
|
+
## For Developers
|
829
933
|
|
830
934
|
You may run the rspec tests like this:
|
831
935
|
|
832
936
|
bundle exec rspec spec
|
833
937
|
|
938
|
+
### TODO
|
939
|
+
|
940
|
+
* add ability to cancel further processing from within an override block
|
941
|
+
* write some spec for the override method
|
942
|
+
|
834
943
|
## License
|
835
944
|
|
836
945
|
[The BSD License](http://www.opensource.org/licenses/bsd-license.php)
|
data/amoeba.gemspec
CHANGED
data/lib/amoeba.rb
CHANGED
@@ -59,6 +59,11 @@ module Amoeba
|
|
59
59
|
@customizations
|
60
60
|
end
|
61
61
|
|
62
|
+
def overrides
|
63
|
+
@overrides ||= []
|
64
|
+
@overrides
|
65
|
+
end
|
66
|
+
|
62
67
|
def null_fields
|
63
68
|
@null_fields ||= []
|
64
69
|
@null_fields
|
@@ -138,6 +143,17 @@ module Amoeba
|
|
138
143
|
@clones
|
139
144
|
end
|
140
145
|
|
146
|
+
def override(value=nil)
|
147
|
+
@do_preproc ||= true
|
148
|
+
@overrides ||= []
|
149
|
+
if value.is_a?(Array)
|
150
|
+
@overrides = value
|
151
|
+
else
|
152
|
+
@overrides << value if value
|
153
|
+
end
|
154
|
+
@overrides
|
155
|
+
end
|
156
|
+
|
141
157
|
def customize(value=nil)
|
142
158
|
@do_preproc ||= true
|
143
159
|
@customizations ||= []
|
@@ -310,6 +326,7 @@ module Amoeba
|
|
310
326
|
def dup(options={})
|
311
327
|
@result = super()
|
312
328
|
|
329
|
+
# Inherit Parent Settings {{{
|
313
330
|
if !amoeba_conf.enabled && parent_amoeba_conf.inherit
|
314
331
|
if amoeba_conf.upbringing
|
315
332
|
parenting_style = amoeba_conf.upbringing
|
@@ -332,8 +349,16 @@ module Amoeba
|
|
332
349
|
self.class.amoeba(&child_settings)
|
333
350
|
end
|
334
351
|
end
|
352
|
+
# }}}
|
335
353
|
|
336
354
|
# Run Amoeba {{{
|
355
|
+
# pramoeba_conf.overridesepend any extra strings to indicate uniqueness of the new record(s)
|
356
|
+
if amoeba_conf.overrides.length > 0
|
357
|
+
amoeba_conf.overrides.each do |block|
|
358
|
+
block.call(self, @result)
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
337
362
|
if amoeba_conf.enabled
|
338
363
|
# Deep Clone Settings {{{
|
339
364
|
amoeba_conf.clones.each do |clone_field|
|
@@ -464,11 +489,6 @@ module Amoeba
|
|
464
489
|
@result[n] = nil
|
465
490
|
end
|
466
491
|
|
467
|
-
# prepend any extra strings to indicate uniqueness of the new record(s)
|
468
|
-
amoeba_conf.customizations.each do |block|
|
469
|
-
block.call(self, @result)
|
470
|
-
end
|
471
|
-
|
472
492
|
# prepend any extra strings to indicate uniqueness of the new record(s)
|
473
493
|
amoeba_conf.coercions.each do |field,coercion|
|
474
494
|
@result[field] = "#{coercion}"
|
@@ -488,6 +508,11 @@ module Amoeba
|
|
488
508
|
amoeba_conf.regexes.each do |field,action|
|
489
509
|
@result[field].gsub!(action[:replace], action[:with])
|
490
510
|
end
|
511
|
+
|
512
|
+
# prepend any extra strings to indicate uniqueness of the new record(s)
|
513
|
+
amoeba_conf.customizations.each do |block|
|
514
|
+
block.call(self, @result)
|
515
|
+
end
|
491
516
|
end
|
492
517
|
# }}}
|
493
518
|
end
|
data/lib/amoeba/version.rb
CHANGED
data/spec/lib/amoeba_spec.rb
CHANGED
@@ -33,6 +33,7 @@ describe "amoeba" do
|
|
33
33
|
start_postnote_count = rs["note_count"]
|
34
34
|
|
35
35
|
new_post.save
|
36
|
+
new_post.errors.messages.length.should == 0
|
36
37
|
|
37
38
|
end_account_count = Account.all.count
|
38
39
|
end_history_count = History.all.count
|
@@ -83,6 +84,12 @@ describe "amoeba" do
|
|
83
84
|
old_post.widgets.map(&:id).include?(id).should_not be true
|
84
85
|
end
|
85
86
|
# }}}
|
87
|
+
# Author {{{
|
88
|
+
old_author = Author.find(1)
|
89
|
+
new_author = old_author.dup
|
90
|
+
new_author.save
|
91
|
+
new_author.errors.messages.length.should == 0
|
92
|
+
# }}}
|
86
93
|
# Products {{{
|
87
94
|
# Base Class {{{
|
88
95
|
old_product = Product.find(1)
|
@@ -94,6 +101,7 @@ describe "amoeba" do
|
|
94
101
|
|
95
102
|
new_product = old_product.dup
|
96
103
|
new_product.save
|
104
|
+
new_product.errors.messages.length.should == 0
|
97
105
|
|
98
106
|
end_image_count = Image.where(:product_id => old_product.id).count
|
99
107
|
end_newimage_count = Image.where(:product_id => new_product.id).count
|
@@ -121,6 +129,7 @@ describe "amoeba" do
|
|
121
129
|
|
122
130
|
new_product = old_product.dup
|
123
131
|
new_product.save
|
132
|
+
new_product.errors.messages.length.should == 0
|
124
133
|
|
125
134
|
end_image_count = Image.where(:product_id => old_product.id).count
|
126
135
|
end_newimage_count = Image.where(:product_id => new_product.id).count
|
@@ -147,6 +156,7 @@ describe "amoeba" do
|
|
147
156
|
|
148
157
|
new_product = old_product.dup
|
149
158
|
new_product.save
|
159
|
+
new_product.errors.messages.length.should == 0
|
150
160
|
|
151
161
|
end_image_count = Image.where(:product_id => old_product.id).count
|
152
162
|
end_newimage_count = Image.where(:product_id => new_product.id).count
|
data/spec/support/data.rb
CHANGED
@@ -1,10 +1,13 @@
|
|
1
1
|
u1 = User.create(:name => "Robert Johnson", :email => "bob@crossroads.com")
|
2
2
|
u2 = User.create(:name => "Miles Davis", :email => "miles@kindofblue.com")
|
3
3
|
|
4
|
+
a1 = Author.create(:full_name => "Kermit The Vonnegut", :nickname => "kvsoitgoes")
|
5
|
+
a2 = Author.create(:full_name => "Arthur Sees Clarck", :nickname => "strangewater")
|
6
|
+
|
4
7
|
t = Topic.create(:title => "Ponies", :description => "Lets talk about my ponies.")
|
5
8
|
|
6
9
|
# First Post {{{
|
7
|
-
p1 = t.posts.create(:
|
10
|
+
p1 = t.posts.create(:owner => u1, :author => a1, :title => "My little pony", :contents => "Lorum ipsum dolor rainbow bright. I like dogs, dogs are awesome.")
|
8
11
|
f1 = p1.create_post_config(:is_visible => true, :is_open => false, :password => 'abcdefg123')
|
9
12
|
a1 = p1.create_account(:title => "Foo")
|
10
13
|
h1 = p1.account.create_history(:some_stuff => "Bar")
|
data/spec/support/models.rb
CHANGED
@@ -2,9 +2,21 @@ class Topic < ActiveRecord::Base
|
|
2
2
|
has_many :posts
|
3
3
|
end
|
4
4
|
|
5
|
+
class User < ActiveRecord::Base
|
6
|
+
has_many :posts
|
7
|
+
end
|
8
|
+
|
9
|
+
class Author < ActiveRecord::Base
|
10
|
+
has_many :posts, :inverse_of => :author
|
11
|
+
amoeba do
|
12
|
+
enable
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
5
16
|
class Post < ActiveRecord::Base
|
6
17
|
belongs_to :topic
|
7
|
-
belongs_to :
|
18
|
+
belongs_to :owner, :class_name => 'User'
|
19
|
+
belongs_to :author, :inverse_of => :posts
|
8
20
|
has_one :post_config
|
9
21
|
has_one :account
|
10
22
|
has_one :history, :through => :account
|
@@ -15,7 +27,9 @@ class Post < ActiveRecord::Base
|
|
15
27
|
has_many :widgets, :through => :post_widgets
|
16
28
|
has_and_belongs_to_many :tags
|
17
29
|
has_and_belongs_to_many :notes
|
18
|
-
|
30
|
+
|
31
|
+
validates_presence_of :topic
|
32
|
+
validates_presence_of :author
|
19
33
|
|
20
34
|
amoeba do
|
21
35
|
enable
|
@@ -141,10 +155,6 @@ class Note < ActiveRecord::Base
|
|
141
155
|
has_and_belongs_to_many :posts
|
142
156
|
end
|
143
157
|
|
144
|
-
class User < ActiveRecord::Base
|
145
|
-
has_many :posts
|
146
|
-
end
|
147
|
-
|
148
158
|
# Inheritance
|
149
159
|
class Product < ActiveRecord::Base
|
150
160
|
has_many :images
|
data/spec/support/schema.rb
CHANGED
@@ -9,6 +9,7 @@ ActiveRecord::Schema.define do
|
|
9
9
|
|
10
10
|
create_table :posts, :force => true do |t|
|
11
11
|
t.integer :topic_id
|
12
|
+
t.integer :owner_id
|
12
13
|
t.integer :author_id
|
13
14
|
t.string :title
|
14
15
|
t.string :contents
|
@@ -110,6 +111,12 @@ ActiveRecord::Schema.define do
|
|
110
111
|
t.timestamps
|
111
112
|
end
|
112
113
|
|
114
|
+
create_table :authors, :force => true do |t|
|
115
|
+
t.string :full_name
|
116
|
+
t.string :nickname
|
117
|
+
t.timestamps
|
118
|
+
end
|
119
|
+
|
113
120
|
create_table :posts_tags, :force => true do |t|
|
114
121
|
t.integer :post_id
|
115
122
|
t.integer :tag_id
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: amoeba
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.2.
|
4
|
+
version: 1.2.1
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
12
|
+
date: 2012-04-08 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|
16
|
-
requirement:
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,10 +21,15 @@ dependencies:
|
|
21
21
|
version: 1.0.0
|
22
22
|
type: :development
|
23
23
|
prerelease: false
|
24
|
-
version_requirements:
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 1.0.0
|
25
30
|
- !ruby/object:Gem::Dependency
|
26
31
|
name: rspec
|
27
|
-
requirement:
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
28
33
|
none: false
|
29
34
|
requirements:
|
30
35
|
- - ~>
|
@@ -32,10 +37,15 @@ dependencies:
|
|
32
37
|
version: '2.3'
|
33
38
|
type: :development
|
34
39
|
prerelease: false
|
35
|
-
version_requirements:
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '2.3'
|
36
46
|
- !ruby/object:Gem::Dependency
|
37
|
-
name: sqlite3
|
38
|
-
requirement:
|
47
|
+
name: sqlite3
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
39
49
|
none: false
|
40
50
|
requirements:
|
41
51
|
- - ! '>='
|
@@ -43,10 +53,15 @@ dependencies:
|
|
43
53
|
version: '0'
|
44
54
|
type: :development
|
45
55
|
prerelease: false
|
46
|
-
version_requirements:
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
47
62
|
- !ruby/object:Gem::Dependency
|
48
63
|
name: activerecord
|
49
|
-
requirement:
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
50
65
|
none: false
|
51
66
|
requirements:
|
52
67
|
- - ! '>='
|
@@ -54,7 +69,12 @@ dependencies:
|
|
54
69
|
version: '3.0'
|
55
70
|
type: :runtime
|
56
71
|
prerelease: false
|
57
|
-
version_requirements:
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '3.0'
|
58
78
|
description: ! 'An extension to ActiveRecord to allow the duplication method to also
|
59
79
|
copy associated children, with recursive support for nested of grandchildren. The
|
60
80
|
behavior is controllable with a simple DSL both on your rails models and on the
|
@@ -123,7 +143,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
123
143
|
version: '0'
|
124
144
|
requirements: []
|
125
145
|
rubyforge_project: amoeba
|
126
|
-
rubygems_version: 1.8.
|
146
|
+
rubygems_version: 1.8.21
|
127
147
|
signing_key:
|
128
148
|
specification_version: 3
|
129
149
|
summary: Easy copying of rails models and their child associations.
|