amoeba 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +251 -36
- data/lib/amoeba.rb +163 -49
- data/lib/amoeba/version.rb +1 -1
- data/spec/lib/amoeba_spec.rb +83 -0
- data/spec/support/data.rb +34 -0
- data/spec/support/models.rb +43 -0
- data/spec/support/schema.rb +51 -0
- metadata +10 -10
data/README.md
CHANGED
@@ -4,11 +4,21 @@ 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
|
-
##
|
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
|
+
## What?
|
8
18
|
|
9
19
|
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.
|
10
20
|
|
11
|
-
|
21
|
+
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.
|
12
22
|
|
13
23
|
## Details
|
14
24
|
|
@@ -24,11 +34,12 @@ Rails 3.2 compatible.
|
|
24
34
|
- `has_many :through`
|
25
35
|
- `has_and_belongs_to_many`
|
26
36
|
- A simple DSL for configuration of which fields to copy. The DSL can be applied to your rails models or used on the fly.
|
37
|
+
- Supports STI (Single Table Inheritance) children inheriting their parent amoeba settings.
|
27
38
|
- Multiple configuration styles such as inclusive, exclusive and indiscriminate (aka copy everything).
|
28
39
|
- Supports cloning of the children of Many-to-Many records or merely maintaining original associations
|
29
|
-
- Supports recursive copying of child and grandchild records.
|
40
|
+
- Supports automatic drill-down i.e. recursive copying of child and grandchild records.
|
30
41
|
- Supports preprocessing of fields to help indicate uniqueness and ensure the integrity of your data depending on your business logic needs, e.g. prepending "Copy of " or similar text.
|
31
|
-
- Supports preprocessing of fields with custom lambda blocks so you can basically whatever you want, for example
|
42
|
+
- Supports preprocessing of fields with custom lambda blocks so you can do basically whatever you want if, for example, you need some custom logic while making copies.
|
32
43
|
- Amoeba can perform the following preprocessing operations on fields of copied records
|
33
44
|
- set
|
34
45
|
- prepend
|
@@ -37,16 +48,6 @@ Rails 3.2 compatible.
|
|
37
48
|
- customize
|
38
49
|
- regex
|
39
50
|
|
40
|
-
## Installation
|
41
|
-
|
42
|
-
is hopefully as you would expect:
|
43
|
-
|
44
|
-
gem install amoeba
|
45
|
-
|
46
|
-
or just add it to your Gemfile:
|
47
|
-
|
48
|
-
gem 'amoeba'
|
49
|
-
|
50
51
|
## Usage
|
51
52
|
|
52
53
|
Configure your models with one of the styles below and then just run the `dup` method on your model as you normally would:
|
@@ -155,6 +156,8 @@ You may also specify fields to be copied by passing an array. If you call the `i
|
|
155
156
|
|
156
157
|
These examples will copy the post's tags and authors but not its comments.
|
157
158
|
|
159
|
+
The inclusive style, when used, will automatically disable any ther style that was previously selected.
|
160
|
+
|
158
161
|
### Exclusive Style
|
159
162
|
|
160
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:
|
@@ -175,6 +178,8 @@ If you have more fields to include than to exclude, you may wish to shorten the
|
|
175
178
|
|
176
179
|
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.
|
177
180
|
|
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
|
+
|
178
183
|
### Cloning
|
179
184
|
|
180
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.
|
@@ -207,6 +212,188 @@ If you are using a Many-to-Many relationship, you may tell amoeba to actually ma
|
|
207
212
|
|
208
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.
|
209
214
|
|
215
|
+
## Inheritance
|
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
|
221
|
+
|
222
|
+
# these belong to all products
|
223
|
+
t.string :title
|
224
|
+
t.decimal :price
|
225
|
+
|
226
|
+
# these are for shirts only
|
227
|
+
t.decimal :sleeve_length
|
228
|
+
t.decimal :collar_size
|
229
|
+
|
230
|
+
# these are for computers only
|
231
|
+
t.integer :ram_size
|
232
|
+
t.integer :hard_drive_size
|
233
|
+
end
|
234
|
+
|
235
|
+
class Product < ActiveRecord::Base
|
236
|
+
has_many :images
|
237
|
+
has_and_belongs_to_many :categories
|
238
|
+
|
239
|
+
amoeba do
|
240
|
+
enable
|
241
|
+
propagate
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
class Shirt < Product
|
246
|
+
end
|
247
|
+
|
248
|
+
class Computer < Product
|
249
|
+
end
|
250
|
+
|
251
|
+
class ProductsController
|
252
|
+
def some_method
|
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
|
261
|
+
end
|
262
|
+
|
263
|
+
This example should duplicate all the images and sections associated with this Shirt, which is a child of Product
|
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.
|
268
|
+
|
269
|
+
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.
|
270
|
+
|
271
|
+
#### Relaxed Parenting
|
272
|
+
|
273
|
+
The `:relaxed` parenting style will prefer parent settings.
|
274
|
+
|
275
|
+
class Product < ActiveRecord::Base
|
276
|
+
has_many :images
|
277
|
+
has_and_belongs_to_many :sections
|
278
|
+
|
279
|
+
amoeba do
|
280
|
+
exclude_field :images
|
281
|
+
propagate :relaxed
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
class Shirt < Product
|
286
|
+
include_field :images
|
287
|
+
include_field :sections
|
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
|
300
|
+
|
301
|
+
amoeba do
|
302
|
+
exclude_field :images
|
303
|
+
propagate :strict
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
class Shirt < Product
|
308
|
+
include_field :images
|
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.
|
328
|
+
|
329
|
+
#### A Submissive Override Example
|
330
|
+
|
331
|
+
This version will use both the parent and child settings, so both the images and sections will be copied.
|
332
|
+
|
333
|
+
class Product < ActiveRecord::Base
|
334
|
+
has_many :images
|
335
|
+
has_and_belongs_to_many :sections
|
336
|
+
|
337
|
+
amoeba do
|
338
|
+
include_field :images
|
339
|
+
propagate
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
class Shirt < Product
|
344
|
+
include_field :sections
|
345
|
+
end
|
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
|
352
|
+
|
353
|
+
amoeba do
|
354
|
+
include_field :images
|
355
|
+
propagate
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
class Shirt < Product
|
360
|
+
include_field [:sections]
|
361
|
+
end
|
362
|
+
|
363
|
+
#### A Relaxed Override Example
|
364
|
+
|
365
|
+
This version will use both the parent and child settings, so both the images and sections will be copied.
|
366
|
+
|
367
|
+
class Product < ActiveRecord::Base
|
368
|
+
has_many :images
|
369
|
+
has_and_belongs_to_many :sections
|
370
|
+
|
371
|
+
amoeba do
|
372
|
+
include_field :images
|
373
|
+
propagate :relaxed
|
374
|
+
end
|
375
|
+
end
|
376
|
+
|
377
|
+
class Shirt < Product
|
378
|
+
include_field :sections
|
379
|
+
end
|
380
|
+
|
381
|
+
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.
|
382
|
+
|
383
|
+
class Product < ActiveRecord::Base
|
384
|
+
has_many :images
|
385
|
+
has_and_belongs_to_many :sections
|
386
|
+
|
387
|
+
amoeba do
|
388
|
+
include_field [:images]
|
389
|
+
propagate
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
class Shirt < Product
|
394
|
+
include_field :sections
|
395
|
+
end
|
396
|
+
|
210
397
|
### Limiting Association Types
|
211
398
|
|
212
399
|
By default, amoeba recognizes and attemps to copy any children of the following association types:
|
@@ -552,55 +739,83 @@ You may control how amoeba copies your object, on the fly, by passing a configur
|
|
552
739
|
end
|
553
740
|
end
|
554
741
|
|
555
|
-
###
|
742
|
+
### Configuration Reference
|
556
743
|
|
557
744
|
Here is a static reference to the available configuration methods, usable within the amoeba block on your rails models.
|
558
745
|
|
559
746
|
#### Controlling Associations
|
560
747
|
|
561
|
-
`enable`
|
748
|
+
* `enable`
|
749
|
+
|
750
|
+
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
|
+
|
752
|
+
* `include_field`
|
753
|
+
|
754
|
+
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.
|
562
755
|
|
563
|
-
|
756
|
+
* `exclude_field`
|
564
757
|
|
565
|
-
|
758
|
+
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.
|
566
759
|
|
567
|
-
|
760
|
+
* `clone`
|
568
761
|
|
569
|
-
|
762
|
+
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.
|
570
763
|
|
571
|
-
|
764
|
+
* `propagate`
|
572
765
|
|
573
|
-
`
|
766
|
+
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
|
+
|
768
|
+
The default "parenting style" is `submissive`
|
769
|
+
|
770
|
+
for example
|
771
|
+
|
772
|
+
amoeba do
|
773
|
+
propagate :strict
|
774
|
+
end
|
775
|
+
|
776
|
+
will choose the strict parenting style of inherited settings.
|
777
|
+
|
778
|
+
* `raised`
|
779
|
+
|
780
|
+
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
|
+
|
782
|
+
The default "parenting style" is `submissive`
|
783
|
+
|
784
|
+
for example:
|
785
|
+
|
786
|
+
amoeba do
|
787
|
+
raised :relaxed
|
788
|
+
end
|
574
789
|
|
575
|
-
|
790
|
+
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.
|
576
791
|
|
577
792
|
#### Pre-Processing Fields
|
578
793
|
|
579
|
-
`nullify`
|
794
|
+
* `nullify`
|
580
795
|
|
581
|
-
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.
|
796
|
+
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.
|
582
797
|
|
583
|
-
`prepend`
|
798
|
+
* `prepend`
|
584
799
|
|
585
|
-
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 "}]`.
|
800
|
+
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 "}]`.
|
586
801
|
|
587
|
-
`append`
|
802
|
+
* `append`
|
588
803
|
|
589
|
-
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)"}]`.
|
804
|
+
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)"}]`.
|
590
805
|
|
591
|
-
`set`
|
806
|
+
* `set`
|
592
807
|
|
593
|
-
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"}]`.
|
808
|
+
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"}]`.
|
594
809
|
|
595
|
-
`regex`
|
810
|
+
* `regex`
|
596
811
|
|
597
|
-
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"}]`.
|
812
|
+
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"}]`.
|
598
813
|
|
599
|
-
`customize`
|
814
|
+
* `customize`
|
600
815
|
|
601
|
-
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
|
816
|
+
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
|
602
817
|
|
603
|
-
This method may be called multiple times, once per desired customizer block, or you may pass an array of lambdas. Passing a single lambda will add to the list of processing directives. Passing an array will empty the list and replace it with the array you pass.
|
818
|
+
This method may be called multiple times, once per desired customizer block, or you may pass an array of lambdas. Passing a single lambda will add to the list of processing directives. Passing an array will empty the list and replace it with the array you pass.
|
604
819
|
|
605
820
|
## Known Limitations and Issues
|
606
821
|
|
data/lib/amoeba.rb
CHANGED
@@ -2,25 +2,38 @@ require "active_record"
|
|
2
2
|
require "amoeba/version"
|
3
3
|
|
4
4
|
module Amoeba
|
5
|
-
module
|
6
|
-
def amoeba(&block)
|
7
|
-
@config ||= Amoeba::ClassMethods::Config.new
|
8
|
-
@config.instance_eval(&block) if block_given?
|
9
|
-
@config
|
10
|
-
end
|
11
|
-
|
5
|
+
module Dsl # {{{
|
12
6
|
class Config
|
7
|
+
def initialize(parent)
|
8
|
+
@parent = parent
|
9
|
+
end
|
10
|
+
|
13
11
|
# Getters {{{
|
12
|
+
def upbringing
|
13
|
+
@raised ||= false
|
14
|
+
@raised
|
15
|
+
end
|
16
|
+
|
14
17
|
def enabled
|
15
18
|
@enabled ||= false
|
16
19
|
@enabled
|
17
20
|
end
|
18
21
|
|
22
|
+
def inherit
|
23
|
+
@inherit ||= false
|
24
|
+
@inherit
|
25
|
+
end
|
26
|
+
|
19
27
|
def do_preproc
|
20
28
|
@do_preproc ||= false
|
21
29
|
@do_preproc
|
22
30
|
end
|
23
31
|
|
32
|
+
def parenting
|
33
|
+
@parenting ||= false
|
34
|
+
@parenting
|
35
|
+
end
|
36
|
+
|
24
37
|
def known_macros
|
25
38
|
@known_macros ||= [:has_one, :has_many, :has_and_belongs_to_many]
|
26
39
|
@known_macros
|
@@ -81,8 +94,18 @@ module Amoeba
|
|
81
94
|
@enabled = false
|
82
95
|
end
|
83
96
|
|
97
|
+
def raised(style=:submissive)
|
98
|
+
@raised = style
|
99
|
+
end
|
100
|
+
|
101
|
+
def propagate(style=:submissive)
|
102
|
+
@parenting ||= style
|
103
|
+
@inherit = true
|
104
|
+
end
|
105
|
+
|
84
106
|
def include_field(value=nil)
|
85
107
|
@enabled ||= true
|
108
|
+
@excludes = []
|
86
109
|
@includes ||= []
|
87
110
|
if value.is_a?(Array)
|
88
111
|
@includes = value
|
@@ -94,6 +117,7 @@ module Amoeba
|
|
94
117
|
|
95
118
|
def exclude_field(value=nil)
|
96
119
|
@enabled ||= true
|
120
|
+
@includes = []
|
97
121
|
@excludes ||= []
|
98
122
|
if value.is_a?(Array)
|
99
123
|
@excludes = value
|
@@ -224,13 +248,141 @@ module Amoeba
|
|
224
248
|
end
|
225
249
|
# }}}
|
226
250
|
end
|
251
|
+
end # }}}
|
252
|
+
|
253
|
+
module ClassMethods
|
254
|
+
def amoeba(&block)
|
255
|
+
@config_block ||= block if block_given?
|
256
|
+
|
257
|
+
@config ||= Amoeba::Dsl::Config.new(self)
|
258
|
+
@config.instance_eval(&block) if block_given?
|
259
|
+
@config
|
260
|
+
end
|
261
|
+
|
262
|
+
def fresh_amoeba(&block)
|
263
|
+
@config_block = block if block_given?
|
264
|
+
|
265
|
+
@config = Amoeba::Dsl::Config.new(self)
|
266
|
+
@config.instance_eval(&block) if block_given?
|
267
|
+
@config
|
268
|
+
end
|
269
|
+
|
270
|
+
def amoeba_block
|
271
|
+
@config_block
|
272
|
+
end
|
227
273
|
end
|
228
274
|
|
229
275
|
module InstanceMethods
|
276
|
+
# Config Getters {{{
|
230
277
|
def amoeba_conf
|
231
278
|
self.class.amoeba
|
232
279
|
end
|
233
280
|
|
281
|
+
def has_parent_amoeba_conf?
|
282
|
+
self.class.superclass.respond_to?(:amoeba)
|
283
|
+
end
|
284
|
+
|
285
|
+
def parent_amoeba_conf
|
286
|
+
if has_parent_amoeba_conf?
|
287
|
+
self.class.superclass.amoeba
|
288
|
+
else
|
289
|
+
false
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
def amoeba_settings
|
294
|
+
self.class.amoeba_block
|
295
|
+
end
|
296
|
+
|
297
|
+
def has_parent_amoeba_settings?
|
298
|
+
self.class.superclass.respond_to?(:amoeba_block)
|
299
|
+
end
|
300
|
+
|
301
|
+
def parent_amoeba_settings
|
302
|
+
if has_parent_amoeba_conf?
|
303
|
+
self.class.superclass.amoeba_block
|
304
|
+
else
|
305
|
+
false
|
306
|
+
end
|
307
|
+
end
|
308
|
+
# }}}
|
309
|
+
|
310
|
+
def dup(options={})
|
311
|
+
@result = super()
|
312
|
+
|
313
|
+
if !amoeba_conf.enabled && parent_amoeba_conf.inherit
|
314
|
+
if amoeba_conf.upbringing
|
315
|
+
parenting_style = amoeba_conf.upbringing
|
316
|
+
else
|
317
|
+
parenting_style = parent_amoeba_conf.parenting
|
318
|
+
end
|
319
|
+
|
320
|
+
case parenting_style
|
321
|
+
when :strict
|
322
|
+
# parent settings only
|
323
|
+
self.class.fresh_amoeba(&parent_amoeba_settings)
|
324
|
+
when :relaxed
|
325
|
+
# parent takes precedence
|
326
|
+
self.class.amoeba(&parent_amoeba_settings)
|
327
|
+
when :submissive
|
328
|
+
# parent suggests things
|
329
|
+
# child does what it wants to anyway
|
330
|
+
child_settings = amoeba_settings
|
331
|
+
self.class.fresh_amoeba(&parent_amoeba_settings)
|
332
|
+
self.class.amoeba(&child_settings)
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
# Run Amoeba {{{
|
337
|
+
if amoeba_conf.enabled
|
338
|
+
# Deep Clone Settings {{{
|
339
|
+
amoeba_conf.clones.each do |clone_field|
|
340
|
+
r = self.class.reflect_on_association clone_field
|
341
|
+
|
342
|
+
# if this is a has many through and we're gonna deep
|
343
|
+
# copy the child records, exclude the regular join
|
344
|
+
# table from copying so we don't end up with the new
|
345
|
+
# and old children on the copy
|
346
|
+
if r.macro == :has_many && r.is_a?(ActiveRecord::Reflection::ThroughReflection)
|
347
|
+
amoeba_conf.exclude_field r.options[:through]
|
348
|
+
end
|
349
|
+
end
|
350
|
+
# }}}
|
351
|
+
|
352
|
+
# Inclusive Style {{{
|
353
|
+
if amoeba_conf.includes.count > 0
|
354
|
+
amoeba_conf.includes.each do |i|
|
355
|
+
r = self.class.reflect_on_association i
|
356
|
+
amo_process_association(i, r)
|
357
|
+
end
|
358
|
+
# }}}
|
359
|
+
# Exclusive Style {{{
|
360
|
+
elsif amoeba_conf.excludes.count > 0
|
361
|
+
reflections.each do |r|
|
362
|
+
if not amoeba_conf.excludes.include?(r[0])
|
363
|
+
amo_process_association(r[0], r[1])
|
364
|
+
end
|
365
|
+
end
|
366
|
+
# }}}
|
367
|
+
# Indiscriminate Style {{{
|
368
|
+
else
|
369
|
+
reflections.each do |r|
|
370
|
+
amo_process_association(r[0], r[1])
|
371
|
+
end
|
372
|
+
end
|
373
|
+
# }}}
|
374
|
+
end
|
375
|
+
|
376
|
+
if amoeba_conf.do_preproc
|
377
|
+
amo_preprocess_parent_copy
|
378
|
+
end
|
379
|
+
# }}}
|
380
|
+
|
381
|
+
@result
|
382
|
+
end
|
383
|
+
|
384
|
+
private
|
385
|
+
# Copy Children {{{
|
234
386
|
def amo_process_association(relation_name, settings)
|
235
387
|
if not amoeba_conf.known_macros.include?(settings.macro)
|
236
388
|
return
|
@@ -303,49 +455,10 @@ module Amoeba
|
|
303
455
|
end
|
304
456
|
end
|
305
457
|
end
|
458
|
+
# }}}
|
306
459
|
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
amoeba_conf.clones.each do |clone_field|
|
311
|
-
r = self.class.reflect_on_association clone_field
|
312
|
-
|
313
|
-
# if this is a has many through and we're gonna deep
|
314
|
-
# copy the child records, exclude the regular join
|
315
|
-
# table from copying so we don't end up with the new
|
316
|
-
# and old children on the copy
|
317
|
-
if r.macro == :has_many && r.is_a?(ActiveRecord::Reflection::ThroughReflection)
|
318
|
-
amoeba_conf.exclude_field r.options[:through]
|
319
|
-
end
|
320
|
-
end
|
321
|
-
|
322
|
-
if amoeba_conf.enabled
|
323
|
-
if amoeba_conf.includes.count > 0
|
324
|
-
amoeba_conf.includes.each do |i|
|
325
|
-
r = self.class.reflect_on_association i
|
326
|
-
amo_process_association(i, r)
|
327
|
-
end
|
328
|
-
elsif amoeba_conf.excludes.count > 0
|
329
|
-
reflections.each do |r|
|
330
|
-
if not amoeba_conf.excludes.include?(r[0])
|
331
|
-
amo_process_association(r[0], r[1])
|
332
|
-
end
|
333
|
-
end
|
334
|
-
else
|
335
|
-
reflections.each do |r|
|
336
|
-
amo_process_association(r[0], r[1])
|
337
|
-
end
|
338
|
-
end
|
339
|
-
end
|
340
|
-
|
341
|
-
if amoeba_conf.do_preproc
|
342
|
-
preprocess_parent_copy
|
343
|
-
end
|
344
|
-
|
345
|
-
@result
|
346
|
-
end
|
347
|
-
|
348
|
-
def preprocess_parent_copy
|
460
|
+
# Field Preprocessor {{{
|
461
|
+
def amo_preprocess_parent_copy
|
349
462
|
# nullify any fields the user has configured
|
350
463
|
amoeba_conf.null_fields.each do |n|
|
351
464
|
@result[n] = nil
|
@@ -376,6 +489,7 @@ module Amoeba
|
|
376
489
|
@result[field].gsub!(action[:replace], action[:with])
|
377
490
|
end
|
378
491
|
end
|
492
|
+
# }}}
|
379
493
|
end
|
380
494
|
end
|
381
495
|
|
data/lib/amoeba/version.rb
CHANGED
data/spec/lib/amoeba_spec.rb
CHANGED
@@ -4,6 +4,7 @@ require 'spec_helper'
|
|
4
4
|
describe "amoeba" do
|
5
5
|
context "dup" do
|
6
6
|
it "duplicates associated child records" do
|
7
|
+
# Posts {{{
|
7
8
|
old_post = Post.find(1)
|
8
9
|
old_post.comments.map(&:contents).include?("I love it!").should be true
|
9
10
|
|
@@ -81,6 +82,88 @@ describe "amoeba" do
|
|
81
82
|
new_post.widgets.map(&:id).each do |id|
|
82
83
|
old_post.widgets.map(&:id).include?(id).should_not be true
|
83
84
|
end
|
85
|
+
# }}}
|
86
|
+
# Products {{{
|
87
|
+
# Base Class {{{
|
88
|
+
old_product = Product.find(1)
|
89
|
+
|
90
|
+
start_image_count = Image.where(:product_id => old_product.id).count
|
91
|
+
start_section_count = Section.all.length
|
92
|
+
rs = ActiveRecord::Base.connection.select_one('SELECT COUNT(*) AS section_count FROM products_sections WHERE product_id = ?', old_product.id)
|
93
|
+
start_prodsection_count = rs["section_count"]
|
94
|
+
|
95
|
+
new_product = old_product.dup
|
96
|
+
new_product.save
|
97
|
+
|
98
|
+
end_image_count = Image.where(:product_id => old_product.id).count
|
99
|
+
end_newimage_count = Image.where(:product_id => new_product.id).count
|
100
|
+
end_section_count = Section.all.length
|
101
|
+
rs = ActiveRecord::Base.connection.select_one('SELECT COUNT(*) AS section_count FROM products_sections WHERE product_id = ?', 1)
|
102
|
+
end_prodsection_count = rs["section_count"]
|
103
|
+
rs = ActiveRecord::Base.connection.select_one('SELECT COUNT(*) AS section_count FROM products_sections WHERE product_id = ?', new_product.id)
|
104
|
+
end_newprodsection_count = rs["section_count"]
|
105
|
+
|
106
|
+
end_image_count.should == start_image_count
|
107
|
+
end_newimage_count.should == start_image_count
|
108
|
+
end_section_count.should == start_section_count
|
109
|
+
end_prodsection_count.should == start_prodsection_count
|
110
|
+
end_newprodsection_count.should == start_prodsection_count
|
111
|
+
# }}}
|
112
|
+
|
113
|
+
# Inherited Class {{{
|
114
|
+
# Shirt {{{
|
115
|
+
old_product = Shirt.find(2)
|
116
|
+
|
117
|
+
start_image_count = Image.where(:product_id => old_product.id).count
|
118
|
+
start_section_count = Section.all.length
|
119
|
+
rs = ActiveRecord::Base.connection.select_one('SELECT COUNT(*) AS section_count FROM products_sections WHERE product_id = ?', old_product.id)
|
120
|
+
start_prodsection_count = rs["section_count"]
|
121
|
+
|
122
|
+
new_product = old_product.dup
|
123
|
+
new_product.save
|
124
|
+
|
125
|
+
end_image_count = Image.where(:product_id => old_product.id).count
|
126
|
+
end_newimage_count = Image.where(:product_id => new_product.id).count
|
127
|
+
end_section_count = Section.all.length
|
128
|
+
rs = ActiveRecord::Base.connection.select_one('SELECT COUNT(*) AS section_count FROM products_sections WHERE product_id = ?', 1)
|
129
|
+
end_prodsection_count = rs["section_count"]
|
130
|
+
rs = ActiveRecord::Base.connection.select_one('SELECT COUNT(*) AS section_count FROM products_sections WHERE product_id = ?', new_product.id)
|
131
|
+
end_newprodsection_count = rs["section_count"]
|
132
|
+
|
133
|
+
end_image_count.should == start_image_count
|
134
|
+
end_newimage_count.should == start_image_count
|
135
|
+
end_section_count.should == start_section_count
|
136
|
+
end_prodsection_count.should == start_prodsection_count
|
137
|
+
end_newprodsection_count.should == start_prodsection_count
|
138
|
+
# }}}
|
139
|
+
|
140
|
+
# Necklace {{{
|
141
|
+
old_product = Necklace.find(3)
|
142
|
+
|
143
|
+
start_image_count = Image.where(:product_id => old_product.id).count
|
144
|
+
start_section_count = Section.all.length
|
145
|
+
rs = ActiveRecord::Base.connection.select_one('SELECT COUNT(*) AS section_count FROM products_sections WHERE product_id = ?', old_product.id)
|
146
|
+
start_prodsection_count = rs["section_count"]
|
147
|
+
|
148
|
+
new_product = old_product.dup
|
149
|
+
new_product.save
|
150
|
+
|
151
|
+
end_image_count = Image.where(:product_id => old_product.id).count
|
152
|
+
end_newimage_count = Image.where(:product_id => new_product.id).count
|
153
|
+
end_section_count = Section.all.length
|
154
|
+
rs = ActiveRecord::Base.connection.select_one('SELECT COUNT(*) AS section_count FROM products_sections WHERE product_id = ?', 1)
|
155
|
+
end_prodsection_count = rs["section_count"]
|
156
|
+
rs = ActiveRecord::Base.connection.select_one('SELECT COUNT(*) AS section_count FROM products_sections WHERE product_id = ?', new_product.id)
|
157
|
+
end_newprodsection_count = rs["section_count"]
|
158
|
+
|
159
|
+
end_image_count.should == start_image_count
|
160
|
+
end_newimage_count.should == start_image_count
|
161
|
+
end_section_count.should == start_section_count
|
162
|
+
end_prodsection_count.should == start_prodsection_count
|
163
|
+
end_newprodsection_count.should == start_prodsection_count
|
164
|
+
# }}}
|
165
|
+
# }}}
|
166
|
+
# }}}
|
84
167
|
end
|
85
168
|
end
|
86
169
|
end
|
data/spec/support/data.rb
CHANGED
@@ -79,3 +79,37 @@ s3.superkittens.create(:value => "Dopey")
|
|
79
79
|
s3.superkittens.create(:value => "Sneezy")
|
80
80
|
s3.superkittens.create(:value => "Sleepy")
|
81
81
|
# }}}
|
82
|
+
|
83
|
+
# Product {{{
|
84
|
+
product1 = Product.create(:title => "Sticky Notes 5-Pak", :price => 5.99, :weight => 0.56)
|
85
|
+
shirt1 = Shirt.create(:title => "Fancy Shirt", :price => 48.95, :sleeve => 32, :collar => 15.5)
|
86
|
+
necklace1 = Necklace.create(:title => "Pearl Necklace", :price => 2995.99, :length => 18, :metal => "14k")
|
87
|
+
|
88
|
+
img1 = product1.images.create(:filename => "sticky.jpg")
|
89
|
+
img2 = product1.images.create(:filename => "notes.jpg")
|
90
|
+
|
91
|
+
img1 = shirt1.images.create(:filename => "02948u31.jpg")
|
92
|
+
img2 = shirt1.images.create(:filename => "zsif8327.jpg")
|
93
|
+
|
94
|
+
img1 = necklace1.images.create(:filename => "ae02x9f1.jpg")
|
95
|
+
img2 = necklace1.images.create(:filename => "cba9f912.jpg")
|
96
|
+
|
97
|
+
office = Section.create(:name => "Office", :num_employees => 2, :total_sales => "1234.56")
|
98
|
+
supplies = Section.create(:name => "Supplies", :num_employees => 1, :total_sales => "543.21")
|
99
|
+
mens = Section.create(:name => "Mens", :num_employees => 3, :total_sales => "11982.63")
|
100
|
+
apparel = Section.create(:name => "Apparel", :num_employees => 5, :total_sales => "1315.20")
|
101
|
+
accessories = Section.create(:name => "Accessories", :num_employees => 1, :total_sales => "8992.34")
|
102
|
+
jewelry = Section.create(:name => "Jewelry", :num_employees => 3, :total_sales => "25481.77")
|
103
|
+
|
104
|
+
product1.sections << office
|
105
|
+
product1.sections << supplies
|
106
|
+
product1.save
|
107
|
+
|
108
|
+
shirt1.sections << mens
|
109
|
+
shirt1.sections << apparel
|
110
|
+
shirt1.save
|
111
|
+
|
112
|
+
necklace1.sections << jewelry
|
113
|
+
necklace1.sections << accessories
|
114
|
+
necklace1.save
|
115
|
+
# }}}
|
data/spec/support/models.rb
CHANGED
@@ -144,3 +144,46 @@ end
|
|
144
144
|
class User < ActiveRecord::Base
|
145
145
|
has_many :posts
|
146
146
|
end
|
147
|
+
|
148
|
+
# Inheritance
|
149
|
+
class Product < ActiveRecord::Base
|
150
|
+
has_many :images
|
151
|
+
has_and_belongs_to_many :sections
|
152
|
+
|
153
|
+
amoeba do
|
154
|
+
enable
|
155
|
+
propagate
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
class Section < ActiveRecord::Base
|
160
|
+
end
|
161
|
+
|
162
|
+
class Image < ActiveRecord::Base
|
163
|
+
end
|
164
|
+
|
165
|
+
class Shirt < Product
|
166
|
+
amoeba do
|
167
|
+
raised :submissive
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
class Necklace < Product
|
172
|
+
amoeba do
|
173
|
+
raised :relaxed
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# Polymorphism
|
178
|
+
class Address < ActiveRecord::Base
|
179
|
+
belongs_to :addressable, :polymorphic => true
|
180
|
+
end
|
181
|
+
|
182
|
+
class Employee < ActiveRecord::Base
|
183
|
+
has_many :addresses, :as => :addressable
|
184
|
+
end
|
185
|
+
|
186
|
+
class Customer < ActiveRecord::Base
|
187
|
+
has_many :addresses, :as => :addressable
|
188
|
+
end
|
189
|
+
|
data/spec/support/schema.rb
CHANGED
@@ -15,6 +15,57 @@ ActiveRecord::Schema.define do
|
|
15
15
|
t.timestamps
|
16
16
|
end
|
17
17
|
|
18
|
+
create_table :products, :force => true do |t|
|
19
|
+
t.string :type
|
20
|
+
t.string :title
|
21
|
+
t.decimal :price
|
22
|
+
t.decimal :weight
|
23
|
+
t.decimal :cost
|
24
|
+
t.decimal :sleeve
|
25
|
+
t.decimal :collar
|
26
|
+
t.decimal :length
|
27
|
+
t.string :metal
|
28
|
+
end
|
29
|
+
|
30
|
+
create_table :products_sections, :force => true do |t|
|
31
|
+
t.integer :section_id
|
32
|
+
t.integer :product_id
|
33
|
+
end
|
34
|
+
|
35
|
+
create_table :sections, :force => true do |t|
|
36
|
+
t.string :name
|
37
|
+
t.integer :num_employees
|
38
|
+
t.decimal :total_sales
|
39
|
+
end
|
40
|
+
|
41
|
+
create_table :images, :force => true do |t|
|
42
|
+
t.string :filename
|
43
|
+
t.integer :product_id
|
44
|
+
end
|
45
|
+
|
46
|
+
create_table :employees, :force => true do |t|
|
47
|
+
t.string :name
|
48
|
+
t.string :ssn
|
49
|
+
t.decimal :salary
|
50
|
+
end
|
51
|
+
|
52
|
+
create_table :customers, :force => true do |t|
|
53
|
+
t.string :email
|
54
|
+
t.string :password
|
55
|
+
t.decimal :balance
|
56
|
+
end
|
57
|
+
|
58
|
+
create_table :addresses, :force => true do |t|
|
59
|
+
t.integer :addressable_id
|
60
|
+
t.string :addressable_type
|
61
|
+
|
62
|
+
t.string :street
|
63
|
+
t.string :unit
|
64
|
+
t.string :city
|
65
|
+
t.string :state
|
66
|
+
t.string :zip
|
67
|
+
end
|
68
|
+
|
18
69
|
create_table :post_configs, :force => true do |t|
|
19
70
|
t.integer :post_id
|
20
71
|
t.integer :is_visible
|
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.
|
4
|
+
version: 1.2.0
|
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-03-
|
12
|
+
date: 2012-03-25 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|
16
|
-
requirement: &
|
16
|
+
requirement: &11153980 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: 1.0.0
|
22
22
|
type: :development
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *11153980
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: rspec
|
27
|
-
requirement: &
|
27
|
+
requirement: &11153480 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ~>
|
@@ -32,10 +32,10 @@ dependencies:
|
|
32
32
|
version: '2.3'
|
33
33
|
type: :development
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *11153480
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: sqlite3-ruby
|
38
|
-
requirement: &
|
38
|
+
requirement: &11153080 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
40
40
|
requirements:
|
41
41
|
- - ! '>='
|
@@ -43,10 +43,10 @@ dependencies:
|
|
43
43
|
version: '0'
|
44
44
|
type: :development
|
45
45
|
prerelease: false
|
46
|
-
version_requirements: *
|
46
|
+
version_requirements: *11153080
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: activerecord
|
49
|
-
requirement: &
|
49
|
+
requirement: &11152540 !ruby/object:Gem::Requirement
|
50
50
|
none: false
|
51
51
|
requirements:
|
52
52
|
- - ! '>='
|
@@ -54,7 +54,7 @@ dependencies:
|
|
54
54
|
version: '3.0'
|
55
55
|
type: :runtime
|
56
56
|
prerelease: false
|
57
|
-
version_requirements: *
|
57
|
+
version_requirements: *11152540
|
58
58
|
description: ! 'An extension to ActiveRecord to allow the duplication method to also
|
59
59
|
copy associated children, with recursive support for nested of grandchildren. The
|
60
60
|
behavior is controllable with a simple DSL both on your rails models and on the
|