amoeba 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/.rvmrc +1 -0
- data/Gemfile +4 -0
- data/README.md +448 -0
- data/Rakefile +1 -0
- data/amoeba.gemspec +29 -0
- data/lib/amoeba.rb +260 -0
- data/lib/amoeba/version.rb +3 -0
- data/spec/lib/amoeba_spec.rb +45 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/support/data.rb +41 -0
- data/spec/support/models.rb +43 -0
- data/spec/support/schema.rb +61 -0
- data/spec/test.sqlite3 +0 -0
- metadata +107 -0
data/.gitignore
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm use --create 1.9.3-p0@amoeba
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -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?
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/amoeba.gemspec
ADDED
@@ -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
|
data/lib/amoeba.rb
ADDED
@@ -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,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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|
data/spec/test.sqlite3
ADDED
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: []
|