clowne 0.0.1 → 0.1.0.beta1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +6 -0
- data/.rspec +3 -0
- data/.rubocop.yml +49 -0
- data/.rufo +3 -0
- data/.travis.yml +37 -3
- data/CHANGELOG.md +11 -0
- data/Gemfile +12 -3
- data/LICENSE.txt +1 -1
- data/README.md +532 -14
- data/Rakefile +6 -8
- data/clowne.gemspec +16 -15
- data/gemfiles/activerecord42.gemfile +6 -0
- data/gemfiles/jruby.gemfile +6 -0
- data/gemfiles/railsmaster.gemfile +7 -0
- data/lib/clowne.rb +36 -2
- data/lib/clowne/adapters/active_record.rb +27 -0
- data/lib/clowne/adapters/active_record/association.rb +34 -0
- data/lib/clowne/adapters/active_record/associations.rb +30 -0
- data/lib/clowne/adapters/active_record/associations/base.rb +63 -0
- data/lib/clowne/adapters/active_record/associations/has_and_belongs_to_many.rb +20 -0
- data/lib/clowne/adapters/active_record/associations/has_many.rb +21 -0
- data/lib/clowne/adapters/active_record/associations/has_one.rb +30 -0
- data/lib/clowne/adapters/active_record/associations/noop.rb +19 -0
- data/lib/clowne/adapters/active_record/dsl.rb +33 -0
- data/lib/clowne/adapters/base.rb +69 -0
- data/lib/clowne/adapters/base/finalize.rb +19 -0
- data/lib/clowne/adapters/base/nullify.rb +19 -0
- data/lib/clowne/adapters/registry.rb +61 -0
- data/lib/clowne/cloner.rb +93 -0
- data/lib/clowne/declarations.rb +30 -0
- data/lib/clowne/declarations/exclude_association.rb +24 -0
- data/lib/clowne/declarations/finalize.rb +20 -0
- data/lib/clowne/declarations/include_association.rb +45 -0
- data/lib/clowne/declarations/nullify.rb +20 -0
- data/lib/clowne/declarations/trait.rb +44 -0
- data/lib/clowne/dsl.rb +14 -0
- data/lib/clowne/ext/string_constantize.rb +23 -0
- data/lib/clowne/plan.rb +81 -0
- data/lib/clowne/planner.rb +40 -0
- data/lib/clowne/version.rb +3 -1
- metadata +73 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2d56daf439132f64870a49326ff8afe5465691b7
|
4
|
+
data.tar.gz: b273e7e236eee799de5a5ea7386c05d26a173b04
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d55650b452063d8eeadbcc3c4a2c1c648c3b5d7c18905ac2d0f7dee05e734358dd19798f7382b284676b693628dcde4fb237e937ce0fafd68f4b2ba860c183fd
|
7
|
+
data.tar.gz: 35463db710e77b7320ff114d1b1fd011ccf9d7405216565420b885ee9b989be8ce734b17ec7c3dbd07017326a1921b0fe4632afff141603f2b41520cce60d238
|
data/.gitignore
CHANGED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
AllCops:
|
2
|
+
Exclude:
|
3
|
+
- 'bin/**/*'
|
4
|
+
- 'tmp/**/*'
|
5
|
+
- 'vendor/**/*'
|
6
|
+
- 'gemfiles/vendor/**/*'
|
7
|
+
DisplayCopNames: true
|
8
|
+
StyleGuideCopsOnly: false
|
9
|
+
TargetRubyVersion: 2.3
|
10
|
+
|
11
|
+
Rails:
|
12
|
+
Enabled: false
|
13
|
+
|
14
|
+
Naming/AccessorMethodName:
|
15
|
+
Enabled: false
|
16
|
+
|
17
|
+
Naming/ClassAndModuleCamelCase:
|
18
|
+
Exclude:
|
19
|
+
- 'spec/**/*.rb'
|
20
|
+
|
21
|
+
Style/TrivialAccessors:
|
22
|
+
Enabled: false
|
23
|
+
|
24
|
+
Metrics/LineLength:
|
25
|
+
Max: 100
|
26
|
+
|
27
|
+
Style/Documentation:
|
28
|
+
Exclude:
|
29
|
+
- 'spec/**/*.rb'
|
30
|
+
|
31
|
+
Style/SymbolArray:
|
32
|
+
Enabled: false
|
33
|
+
|
34
|
+
Style/FrozenStringLiteralComment:
|
35
|
+
Exclude:
|
36
|
+
- 'spec/**/*.rb'
|
37
|
+
- 'Gemfile'
|
38
|
+
- 'Rakefile'
|
39
|
+
- '*.gemspec'
|
40
|
+
|
41
|
+
Metrics/BlockLength:
|
42
|
+
Exclude:
|
43
|
+
- 'spec/**/*.rb'
|
44
|
+
|
45
|
+
Bundler/OrderedGems:
|
46
|
+
Enabled: false
|
47
|
+
|
48
|
+
Gemspec/OrderedDependencies:
|
49
|
+
Enabled: false
|
data/.rufo
ADDED
data/.travis.yml
CHANGED
@@ -1,5 +1,39 @@
|
|
1
1
|
sudo: false
|
2
2
|
language: ruby
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
|
4
|
+
notifications:
|
5
|
+
email: false
|
6
|
+
|
7
|
+
before_script:
|
8
|
+
# Only generate coverage report for the specified job
|
9
|
+
- if [ "$CC_REPORT" == "true" ]; then curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter; fi
|
10
|
+
- if [ "$CC_REPORT" == "true" ]; then chmod +x ./cc-test-reporter; fi
|
11
|
+
- if [ "$CC_REPORT" == "true" ]; then ./cc-test-reporter before-build; fi
|
12
|
+
script:
|
13
|
+
- bundle exec rake
|
14
|
+
after_script:
|
15
|
+
- if [ "$CC_REPORT" == "true" ]; then ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT; fi
|
16
|
+
|
17
|
+
matrix:
|
18
|
+
fast_finish: true
|
19
|
+
include:
|
20
|
+
- rvm: ruby-head
|
21
|
+
gemfile: gemfiles/railsmaster.gemfile
|
22
|
+
- rvm: jruby-9.1.0.0
|
23
|
+
gemfile: gemfiles/jruby.gemfile
|
24
|
+
- rvm: 2.5.0
|
25
|
+
gemfile: Gemfile
|
26
|
+
- rvm: 2.4.3
|
27
|
+
gemfile: Gemfile
|
28
|
+
env: CC_REPORT=true
|
29
|
+
- rvm: 2.4.1
|
30
|
+
gemfile: gemfiles/activerecord42.gemfile
|
31
|
+
- rvm: 2.3.1
|
32
|
+
gemfile: Gemfile
|
33
|
+
- rvm: 2.2.0
|
34
|
+
gemfile: gemfiles/activerecord42.gemfile
|
35
|
+
allow_failures:
|
36
|
+
- rvm: ruby-head
|
37
|
+
gemfile: gemfiles/railsmaster.gemfile
|
38
|
+
- rvm: jruby-9.1.0.0
|
39
|
+
gemfile: gemfiles/jruby.gemfile
|
data/CHANGELOG.md
ADDED
data/Gemfile
CHANGED
@@ -1,6 +1,15 @@
|
|
1
|
-
source
|
2
|
-
|
3
|
-
git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
|
1
|
+
source 'https://rubygems.org'
|
4
2
|
|
5
3
|
# Specify your gem's dependencies in clowne.gemspec
|
6
4
|
gemspec
|
5
|
+
|
6
|
+
gem 'pry-byebug'
|
7
|
+
gem 'sqlite3'
|
8
|
+
gem 'activerecord', '>= 5.0'
|
9
|
+
gem 'simplecov'
|
10
|
+
|
11
|
+
local_gemfile = 'Gemfile.local'
|
12
|
+
|
13
|
+
if File.exist?(local_gemfile)
|
14
|
+
eval(File.read(local_gemfile)) # rubocop:disable Security/Eval
|
15
|
+
end
|
data/LICENSE.txt
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
The MIT License (MIT)
|
2
2
|
|
3
|
-
Copyright (c) 2017 Vladimir Dementyev
|
3
|
+
Copyright (c) 2017 Sverchkov Nikolay, Vladimir Dementyev
|
4
4
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
data/README.md
CHANGED
@@ -1,38 +1,556 @@
|
|
1
|
+
[](https://badge.fury.io/rb/clowne)
|
2
|
+
[](https://travis-ci.org/palkan/clowne)
|
3
|
+
[](https://codeclimate.com/github/palkan/clowne/coverage)
|
4
|
+
|
1
5
|
# Clowne
|
2
6
|
|
3
|
-
|
7
|
+
**NOTE**: this is the documentation for pre-release version **0.1.0.beta1**.
|
8
|
+
|
9
|
+
A flexible gem for cloning your models. Clowne focuses on ease of use and provides the ability to connect various ORM adapters (currently only ActiveRecord is supported).
|
10
|
+
|
11
|
+
<a href="https://evilmartians.com/">
|
12
|
+
<img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54"></a>
|
13
|
+
|
14
|
+
### Alternatives
|
15
|
+
|
16
|
+
Why did we decide to build our own cloning gem?
|
17
|
+
|
18
|
+
First, existing solutions turned out not stable and flexible enough for us.
|
4
19
|
|
5
|
-
|
20
|
+
Secondly, they are Rails-only. And we are not.
|
21
|
+
|
22
|
+
Nevertheless, thanks to [amoeba](https://github.com/amoeba-rb/amoeba) and [deep_cloneable](https://github.com/moiristo/deep_cloneable) for inspiration.
|
6
23
|
|
7
24
|
## Installation
|
8
25
|
|
9
|
-
|
26
|
+
To install Clowne with RubyGems:
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
gem install clowne
|
30
|
+
```
|
31
|
+
|
32
|
+
Or add this line to your application's Gemfile:
|
10
33
|
|
11
34
|
```ruby
|
12
35
|
gem 'clowne'
|
13
36
|
```
|
14
37
|
|
15
|
-
|
38
|
+
## Quick Start
|
16
39
|
|
17
|
-
|
40
|
+
This is a basic example that demonstrates how to clone your ActiveRecord model. For detailed documentation see [Features](#features).
|
18
41
|
|
19
|
-
|
42
|
+
At first, define your cloneable model
|
20
43
|
|
21
|
-
|
44
|
+
```ruby
|
45
|
+
class User < ActiveRecord::Base
|
46
|
+
# create_table :users do |t|
|
47
|
+
# t.string :login
|
48
|
+
# t.string :email
|
49
|
+
# t.timestamps null: false
|
50
|
+
# end
|
22
51
|
|
23
|
-
|
52
|
+
has_one :profile
|
53
|
+
has_many :posts
|
54
|
+
end
|
55
|
+
```
|
56
|
+
|
57
|
+
The next step is to declare cloner
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
class UserCloner < Clowne::Cloner
|
61
|
+
adapter :active_record
|
62
|
+
|
63
|
+
include_association :profile, clone_with: SpecialProfileCloner
|
64
|
+
include_association :posts
|
65
|
+
|
66
|
+
nullify :login
|
24
67
|
|
25
|
-
|
68
|
+
# params here is an arbitrary hash passed into cloner
|
69
|
+
finalize do |_source, record, params|
|
70
|
+
record.email = params[:email]
|
71
|
+
end
|
72
|
+
end
|
26
73
|
|
27
|
-
|
74
|
+
class SpecialProfileCloner < Clowne::Cloner
|
75
|
+
adapter :active_record
|
76
|
+
|
77
|
+
nullify :name
|
78
|
+
end
|
79
|
+
```
|
80
|
+
|
81
|
+
and call it
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
cloned = UserCloner.call(User.last, { email: "fake@example.com" })
|
85
|
+
cloned.persisted?
|
86
|
+
# => false
|
87
|
+
cloned.save!
|
88
|
+
cloned.login
|
89
|
+
# => nil
|
90
|
+
cloned.email
|
91
|
+
# => "fake@example.com"
|
92
|
+
|
93
|
+
# associations:
|
94
|
+
cloned.posts.count == User.last.posts.count
|
95
|
+
# => true
|
96
|
+
cloned.profile.name
|
97
|
+
# => nil
|
98
|
+
```
|
99
|
+
|
100
|
+
## <a name="features">Features
|
101
|
+
|
102
|
+
- [Configuration](#configuration)
|
103
|
+
- [Include association](#include_association)
|
104
|
+
- - [Inline configuration](#config-inline)
|
105
|
+
- [Include one association](#include_association)
|
106
|
+
- - [Scope](#include_association_scope)
|
107
|
+
- - [Options](#include_association_options)
|
108
|
+
- - [Multiple associations](#include_associations)
|
109
|
+
- [Exclude association](#exclude_association)
|
110
|
+
- - [Multiple associations](#exclude_associations)
|
111
|
+
- [Nullify attribute(s)](#nullify)
|
112
|
+
- [Execute finalize block](#finalize)
|
113
|
+
- [Traits](#traits)
|
114
|
+
- [Execution order](#execution_order)
|
115
|
+
- [ActiveRecord DSL](#ar_dsl)
|
116
|
+
- [Customization](#customization)
|
117
|
+
|
118
|
+
### <a name="configuration"></a>Configuration
|
119
|
+
|
120
|
+
You can configure the default adapter for cloners:
|
121
|
+
|
122
|
+
```ruby
|
123
|
+
# somewhere in initializers
|
124
|
+
Clowne.default_adapter = :active_record
|
125
|
+
```
|
126
|
+
|
127
|
+
#### <a name="config-inline"></a>Inline Configuration
|
128
|
+
|
129
|
+
You can also enhance the cloner configuration inline (i.e. add dynamic declarations):
|
130
|
+
|
131
|
+
```ruby
|
132
|
+
cloned = UserCloner.call(User.last) do
|
133
|
+
exclude_association :profile
|
134
|
+
|
135
|
+
finalize do |source, record|
|
136
|
+
record.email = "clone_of_#{source.email}"
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
cloned.email
|
141
|
+
# => "clone_of_john@example.com"
|
142
|
+
|
143
|
+
# associations:
|
144
|
+
cloned.posts.size == User.last.posts.size
|
145
|
+
# => true
|
146
|
+
cloned.profile
|
147
|
+
# => nil
|
148
|
+
```
|
149
|
+
|
150
|
+
Inline enhancement doesn't affect the _global_ configuration, so you can use it without any fear.
|
151
|
+
|
152
|
+
Thus it's also possible to clone objects without any cloner classes at all by using `Clowne::Cloner`:
|
153
|
+
|
154
|
+
```ruby
|
155
|
+
cloned = Clowne::Cloner.call(user) do
|
156
|
+
# anything you want!
|
157
|
+
end
|
158
|
+
```
|
28
159
|
|
29
|
-
|
160
|
+
### <a name="include_association"></a>Include one association
|
161
|
+
|
162
|
+
Powerful declaration for including model's association.
|
163
|
+
|
164
|
+
```ruby
|
165
|
+
class User < ActiveRecord::Base
|
166
|
+
has_one :profile
|
167
|
+
end
|
168
|
+
|
169
|
+
class UserCloner < Clowne::Cloner
|
170
|
+
adapter Clowne::ActiveRecord::Adapter
|
171
|
+
|
172
|
+
include_association :profile
|
173
|
+
end
|
174
|
+
```
|
175
|
+
|
176
|
+
But it's not all! :) The DSL looks like
|
177
|
+
|
178
|
+
```ruby
|
179
|
+
include_association name, scope, options
|
180
|
+
```
|
181
|
+
|
182
|
+
#### <a name="include_association_scope"></a>Include one association: Scope
|
183
|
+
Scope can be a:
|
184
|
+
|
185
|
+
`Symbol` - named scope.
|
186
|
+
|
187
|
+
`Proc` - custom scope (supports parameter passing).
|
188
|
+
|
189
|
+
Example:
|
190
|
+
|
191
|
+
```ruby
|
192
|
+
class User < ActiveRecord::Base
|
193
|
+
has_many :accounts
|
194
|
+
has_many :posts
|
195
|
+
end
|
196
|
+
|
197
|
+
class Account < ActiveRecord::Base
|
198
|
+
scope :active, -> where(active: true)
|
199
|
+
end
|
200
|
+
|
201
|
+
class Post < ActiveRecord::Base
|
202
|
+
# t.string :status
|
203
|
+
end
|
204
|
+
|
205
|
+
class UserCloner < Clowne::Cloner
|
206
|
+
adapter Clowne::ActiveRecord::Adapter
|
207
|
+
|
208
|
+
include_association :accounts, :active
|
209
|
+
include_association :posts, ->(params) { where(state: params[:post_status] }
|
210
|
+
end
|
211
|
+
|
212
|
+
# posts will be cloned only with draft status
|
213
|
+
UserCloner.call(user, { post_status: :draft })
|
214
|
+
# => <#User...
|
215
|
+
```
|
216
|
+
|
217
|
+
#### <a name="include_association_options"></a>Include one association: Options
|
218
|
+
|
219
|
+
Options keys can be a:
|
220
|
+
|
221
|
+
`:clone_with` - use custom cloner for all children.
|
222
|
+
|
223
|
+
`:traits` - define special traits.
|
224
|
+
|
225
|
+
Example:
|
226
|
+
|
227
|
+
```ruby
|
228
|
+
class User < ActiveRecord::Base
|
229
|
+
has_many :posts
|
230
|
+
end
|
231
|
+
|
232
|
+
class Post < ActiveRecord::Base
|
233
|
+
# t.string :title
|
234
|
+
has_many :tags
|
235
|
+
end
|
236
|
+
```
|
237
|
+
|
238
|
+
```ruby
|
239
|
+
class PostSpecialCloner < Clowne::Cloner
|
240
|
+
adapter :active_record
|
241
|
+
|
242
|
+
nullify :title
|
243
|
+
|
244
|
+
trait :with_tags do
|
245
|
+
include_association :tags
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
class UserCloner < Clowne::Cloner
|
250
|
+
adapter :active_record
|
251
|
+
|
252
|
+
include_association :posts, clone_with: PostSpecialCloner
|
253
|
+
# or clone user's posts with tags!
|
254
|
+
# include_association :posts, clone_with: PostSpecialCloner, traits: :with_tags
|
255
|
+
end
|
256
|
+
|
257
|
+
UserCloner.call(user)
|
258
|
+
# => <#User...
|
259
|
+
```
|
260
|
+
|
261
|
+
**Notice: if custom cloner is not defined, clowne tries to find default cloner and use it. (PostCloner for previous example)**
|
262
|
+
|
263
|
+
#### <a name="include_associations"></a>Include multiple association
|
264
|
+
|
265
|
+
It's possible to include multiple associations at once with default options and scope
|
266
|
+
|
267
|
+
```ruby
|
268
|
+
class User < ActiveRecord::Base
|
269
|
+
has_many :accounts
|
270
|
+
has_many :posts
|
271
|
+
end
|
272
|
+
|
273
|
+
class UserCloner < Clowne::Cloner
|
274
|
+
adapter :active_record
|
275
|
+
|
276
|
+
include_associations :accounts, :posts
|
277
|
+
end
|
278
|
+
```
|
279
|
+
|
280
|
+
### <a name="exclude_association"></a>Exclude association
|
281
|
+
|
282
|
+
Exclude association from copying
|
283
|
+
|
284
|
+
```ruby
|
285
|
+
class UserCloner < Clowne::Cloner
|
286
|
+
adapter Clowne::ActiveRecord::Adapter
|
287
|
+
|
288
|
+
include_association :posts
|
289
|
+
|
290
|
+
trait :without_posts do
|
291
|
+
exclude_association :posts
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
# copy user and posts
|
296
|
+
clone = UserCloner.call(user)
|
297
|
+
clone.posts.count == user.posts.count
|
298
|
+
# => true
|
299
|
+
|
300
|
+
# copy only user
|
301
|
+
clone2 = UserCloner.call(user, traits: :without_posts)
|
302
|
+
clone2.posts
|
303
|
+
# => []
|
304
|
+
```
|
305
|
+
|
306
|
+
**NOTE**: once excluded association cannot be re-included, e.g. the following cloner:
|
307
|
+
|
308
|
+
```ruby
|
309
|
+
class UserCloner < Clowne::Cloner
|
310
|
+
exclude_association :comments
|
311
|
+
|
312
|
+
trait :with_comments do
|
313
|
+
# That wouldn't work
|
314
|
+
include_association :comments
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
clone = UserCloner.call(user, traits: :with_comments)
|
319
|
+
clone.comments.empty? #=> true
|
320
|
+
```
|
321
|
+
|
322
|
+
Why so? That allows to have deterministic cloning plans when combining multiple traits
|
323
|
+
(or inheriting cloners).
|
324
|
+
|
325
|
+
#### <a name="exclude_associations"></a>Exclude multiple association
|
326
|
+
|
327
|
+
It's possible to exclude multiple associations the same way as `include_associations` but with `exclude_associations`
|
328
|
+
|
329
|
+
### <a name="nullify"></a>Nullify attribute(s)
|
330
|
+
|
331
|
+
Nullify attributes:
|
332
|
+
|
333
|
+
```ruby
|
334
|
+
class User < ActiveRecord::Base
|
335
|
+
# t.string :name
|
336
|
+
# t.string :surename
|
337
|
+
# t.string :email
|
338
|
+
end
|
339
|
+
|
340
|
+
class UserCloner < Clowne::Cloner
|
341
|
+
adapter Clowne::ActiveRecord::Adapter
|
342
|
+
|
343
|
+
nullify :name, :email
|
344
|
+
|
345
|
+
trait :nullify_surename do
|
346
|
+
nullify :surename
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
# nullify only name
|
351
|
+
clone = UserCloner.call(user)
|
352
|
+
clone.name.nil?
|
353
|
+
# => true
|
354
|
+
clone.email.nil?
|
355
|
+
# => true
|
356
|
+
clone.surename.nil?
|
357
|
+
# => false
|
358
|
+
|
359
|
+
# nullify name and surename
|
360
|
+
clone2 = UserCloner.call(user, traits: :nullify_surename)
|
361
|
+
clone.name.nil?
|
362
|
+
# => true
|
363
|
+
clone.surename.nil?
|
364
|
+
# => true
|
365
|
+
```
|
366
|
+
|
367
|
+
### <a name="finalize"></a>Execute finalize block
|
368
|
+
|
369
|
+
Simple callback for changing record manually.
|
370
|
+
|
371
|
+
```ruby
|
372
|
+
class UserCloner < Clowne::Cloner
|
373
|
+
adapter Clowne::ActiveRecord::Adapter
|
374
|
+
|
375
|
+
finalize do |source, record, params|
|
376
|
+
record.name = 'This is copy!'
|
377
|
+
end
|
378
|
+
|
379
|
+
trait :change_email do
|
380
|
+
finalize do |source, record, params|
|
381
|
+
record.email = params[:email]
|
382
|
+
end
|
383
|
+
end
|
384
|
+
end
|
385
|
+
|
386
|
+
# execute first finalize
|
387
|
+
clone = UserCloner.call(user)
|
388
|
+
clone.name
|
389
|
+
# => 'This is copy!'
|
390
|
+
clone.email == 'clone@example.com'
|
391
|
+
# => false
|
392
|
+
|
393
|
+
# execute both finalizes
|
394
|
+
clone2 = UserCloner.call(user, traits: :change_email)
|
395
|
+
clone.name
|
396
|
+
# => 'This is copy!'
|
397
|
+
clone.email
|
398
|
+
# => 'clone@example.com'
|
399
|
+
```
|
400
|
+
|
401
|
+
### <a name="traits"></a>Traits
|
402
|
+
|
403
|
+
Traits allow you to group cloner declarations together and then apply them (like in factory_bot).
|
404
|
+
|
405
|
+
```ruby
|
406
|
+
class UserCloner < Clowne::Cloner
|
407
|
+
adapter Clowne::ActiveRecord::Adapter
|
408
|
+
|
409
|
+
trait :with_posts do
|
410
|
+
include_association :posts
|
411
|
+
end
|
412
|
+
|
413
|
+
trait :with_profile do
|
414
|
+
include_association :profile
|
415
|
+
end
|
416
|
+
|
417
|
+
trait :nullify_name do
|
418
|
+
nullify :name
|
419
|
+
end
|
420
|
+
end
|
421
|
+
|
422
|
+
# execute first finalize
|
423
|
+
UserCloner.call(user, traits: [:with_posts, :with_profile, :nullify_name])
|
424
|
+
# or
|
425
|
+
UserCloner.call(user, traits: :nullify_name)
|
426
|
+
# or
|
427
|
+
# ...
|
428
|
+
```
|
429
|
+
|
430
|
+
### <a name="execution_order"></a>Execution order
|
431
|
+
|
432
|
+
The order of cloning actions depends on the adapter.
|
433
|
+
|
434
|
+
For ActiveRecord:
|
435
|
+
- clone associations
|
436
|
+
- nullify attributes
|
437
|
+
- run `finalize` blocks
|
438
|
+
The order of `finalize` blocks is the order they've been written.
|
439
|
+
|
440
|
+
### <a name="ar_dsl"></a>Active Record DSL
|
441
|
+
|
442
|
+
Clowne provides an optional ActiveRecord integration which allows you to configure cloners in your models and adds a shortcut to invoke cloners (`#clowne` method). (Note: that's exactly the way [`amoeba`](https://github.com/amoeba-rb/amoeba) works).
|
443
|
+
|
444
|
+
To enable this integration you must require `"clowne/adapters/active_record/dsl"` somewhere in your app, e.g. in initializer:
|
445
|
+
|
446
|
+
```ruby
|
447
|
+
# config/initializers/clowne.rb
|
448
|
+
require "clowne/adapters/active_record/dsl"
|
449
|
+
```
|
450
|
+
|
451
|
+
Now you can specify cloning configs in your AR models:
|
452
|
+
|
453
|
+
```ruby
|
454
|
+
class User < ActiveRecord::Base
|
455
|
+
clowne_config do
|
456
|
+
include_associations :profile
|
457
|
+
|
458
|
+
nullify :email
|
459
|
+
|
460
|
+
# whatever available for your cloners,
|
461
|
+
# active_record adapter is set implicitly here
|
462
|
+
end
|
463
|
+
end
|
464
|
+
```
|
465
|
+
|
466
|
+
And then you can clone objects like this:
|
467
|
+
|
468
|
+
```ruby
|
469
|
+
cloned_user = user.clowne(traits: my_traits, **params)
|
470
|
+
```
|
471
|
+
|
472
|
+
### <a name="customization"></a>Customization
|
473
|
+
|
474
|
+
Clowne is built with extensibility in mind. You can create your own DSL commands and resolvers.
|
475
|
+
|
476
|
+
Let's consider an example.
|
477
|
+
|
478
|
+
Suppose that you want to add `include_all` declaration to automagically include all associations (for ActiveRecord).
|
479
|
+
|
480
|
+
First, you should add a custom declaration:
|
481
|
+
|
482
|
+
```ruby
|
483
|
+
class IncludeAll # :nodoc: all
|
484
|
+
def compile(plan)
|
485
|
+
# Just add all_associations object to plan
|
486
|
+
plan.set(:all_associations, self)
|
487
|
+
# Plan supports 3 types of registers:
|
488
|
+
#
|
489
|
+
# 1) Scalar
|
490
|
+
#
|
491
|
+
# plan.set(key, value)
|
492
|
+
# plan.remove(key)
|
493
|
+
#
|
494
|
+
# 2) Append-only lists
|
495
|
+
#
|
496
|
+
# plan.add(key, value)
|
497
|
+
#
|
498
|
+
# 3) Two-phase set (2P-Set) (see below)
|
499
|
+
#
|
500
|
+
# plan.add_to(type, key, value)
|
501
|
+
# plan.remove_from(type, key)
|
502
|
+
end
|
503
|
+
end
|
504
|
+
|
505
|
+
# Register our declrations, i.e. extend DSL
|
506
|
+
Clowne::Declarations.add :include_all, Clowne::Declarations::IncludeAll
|
507
|
+
```
|
508
|
+
|
509
|
+
\* Operations over [2P-Set](https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type#2P-Set_(Two-Phase_Set)) (adding/removing) do not depend on the order of execution; we use "remove-wins" semantics, i.e. when a key has been removed, it cannot be re-added.
|
510
|
+
|
511
|
+
Secondly, register a resolver:
|
512
|
+
|
513
|
+
```ruby
|
514
|
+
class AllAssociations
|
515
|
+
# This method is called when all_associations command is applied.
|
516
|
+
#
|
517
|
+
# source – source record
|
518
|
+
# record – target record (our clone)
|
519
|
+
# declaration – declaration object
|
520
|
+
# params – custom params passed to cloner
|
521
|
+
def call(source, record, declaration, params:)
|
522
|
+
source.class.reflections.each do |name, reflection|
|
523
|
+
# Exclude belongs_to associations
|
524
|
+
next if reflection.macro == :belongs_to
|
525
|
+
# Resolve and apply association cloner
|
526
|
+
cloner_class = Clowne::Adapters::ActiveRecord::Associations.cloner_for(reflection)
|
527
|
+
cloner_class.new(reflection, source, declaration, params).call(record)
|
528
|
+
end
|
529
|
+
record
|
530
|
+
end
|
531
|
+
end
|
532
|
+
|
533
|
+
# Finally, register the resolver
|
534
|
+
Clowne::Adapters::ActiveRecord.register_resolver(
|
535
|
+
:all_associations, AllAssociations
|
536
|
+
)
|
537
|
+
```
|
538
|
+
|
539
|
+
Now you can use it likes this:
|
540
|
+
|
541
|
+
```ruby
|
542
|
+
class UserCloner < Clowne::Cloner
|
543
|
+
adapter :active_record
|
544
|
+
|
545
|
+
include_all
|
546
|
+
end
|
547
|
+
```
|
30
548
|
|
31
|
-
|
549
|
+
## Maintainers
|
32
550
|
|
33
|
-
|
551
|
+
- [Vladimir Dementyev](https://github.com/palkan)
|
34
552
|
|
35
|
-
|
553
|
+
- [Sverchkov Nikolay](https://github.com/ssnickolay)
|
36
554
|
|
37
555
|
## License
|
38
556
|
|