clowne 0.0.1 → 0.1.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Gem Version](https://badge.fury.io/rb/clowne.svg)](https://badge.fury.io/rb/clowne)
|
2
|
+
[![Build Status](https://travis-ci.org/palkan/clowne.svg?branch=master)](https://travis-ci.org/palkan/clowne)
|
3
|
+
[![Test Coverage](https://codeclimate.com/github/palkan/clowne/badges/coverage.svg)](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
|
|