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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +6 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +49 -0
  5. data/.rufo +3 -0
  6. data/.travis.yml +37 -3
  7. data/CHANGELOG.md +11 -0
  8. data/Gemfile +12 -3
  9. data/LICENSE.txt +1 -1
  10. data/README.md +532 -14
  11. data/Rakefile +6 -8
  12. data/clowne.gemspec +16 -15
  13. data/gemfiles/activerecord42.gemfile +6 -0
  14. data/gemfiles/jruby.gemfile +6 -0
  15. data/gemfiles/railsmaster.gemfile +7 -0
  16. data/lib/clowne.rb +36 -2
  17. data/lib/clowne/adapters/active_record.rb +27 -0
  18. data/lib/clowne/adapters/active_record/association.rb +34 -0
  19. data/lib/clowne/adapters/active_record/associations.rb +30 -0
  20. data/lib/clowne/adapters/active_record/associations/base.rb +63 -0
  21. data/lib/clowne/adapters/active_record/associations/has_and_belongs_to_many.rb +20 -0
  22. data/lib/clowne/adapters/active_record/associations/has_many.rb +21 -0
  23. data/lib/clowne/adapters/active_record/associations/has_one.rb +30 -0
  24. data/lib/clowne/adapters/active_record/associations/noop.rb +19 -0
  25. data/lib/clowne/adapters/active_record/dsl.rb +33 -0
  26. data/lib/clowne/adapters/base.rb +69 -0
  27. data/lib/clowne/adapters/base/finalize.rb +19 -0
  28. data/lib/clowne/adapters/base/nullify.rb +19 -0
  29. data/lib/clowne/adapters/registry.rb +61 -0
  30. data/lib/clowne/cloner.rb +93 -0
  31. data/lib/clowne/declarations.rb +30 -0
  32. data/lib/clowne/declarations/exclude_association.rb +24 -0
  33. data/lib/clowne/declarations/finalize.rb +20 -0
  34. data/lib/clowne/declarations/include_association.rb +45 -0
  35. data/lib/clowne/declarations/nullify.rb +20 -0
  36. data/lib/clowne/declarations/trait.rb +44 -0
  37. data/lib/clowne/dsl.rb +14 -0
  38. data/lib/clowne/ext/string_constantize.rb +23 -0
  39. data/lib/clowne/plan.rb +81 -0
  40. data/lib/clowne/planner.rb +40 -0
  41. data/lib/clowne/version.rb +3 -1
  42. metadata +73 -12
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1ee04593c36f9b86a93581e16fb7ef42d65b387c
4
- data.tar.gz: 9bbe81f26c453f960c3a27f05a8c297e7a207ada
3
+ metadata.gz: 2d56daf439132f64870a49326ff8afe5465691b7
4
+ data.tar.gz: b273e7e236eee799de5a5ea7386c05d26a173b04
5
5
  SHA512:
6
- metadata.gz: 45ae06ab333657628e0ca429b05bc9214e46c619e56dcf89428cf1a57c2a74ef48c7c768f22c8c9c0a552e11605e1dbe9810428134627fba8cc4ce0c5f9dd627
7
- data.tar.gz: c583843f2b87e86ea54468ff6b9047596848c07c87a0da3a19bf12decb4f22cf04ded9bf33399854e21f181b4dc96d412e0225ae275a92cc4a8842739bb9d119
6
+ metadata.gz: d55650b452063d8eeadbcc3c4a2c1c648c3b5d7c18905ac2d0f7dee05e734358dd19798f7382b284676b693628dcde4fb237e937ce0fafd68f4b2ba860c183fd
7
+ data.tar.gz: 35463db710e77b7320ff114d1b1fd011ccf9d7405216565420b885ee9b989be8ce734b17ec7c3dbd07017326a1921b0fe4632afff141603f2b41520cce60d238
data/.gitignore CHANGED
@@ -1,3 +1,6 @@
1
+ .rspec_status
2
+ rubocop.html
3
+ /coverage/
1
4
  /.bundle/
2
5
  /.yardoc
3
6
  /Gemfile.lock
@@ -7,3 +10,6 @@
7
10
  /pkg/
8
11
  /spec/reports/
9
12
  /tmp/
13
+ *.gem
14
+ gemfiles/*.lock
15
+ Gemfile.local
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --require spec_helper
2
+ --format documentation
3
+ --color
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
@@ -0,0 +1,3 @@
1
+ trailing_commas :never
2
+ spaces_inside_hash_brace :always
3
+ spaces_after_comma :one
data/.travis.yml CHANGED
@@ -1,5 +1,39 @@
1
1
  sudo: false
2
2
  language: ruby
3
- rvm:
4
- - 2.4.2
5
- before_install: gem install bundler -v 1.15.4
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
@@ -0,0 +1,11 @@
1
+ # Change log
2
+
3
+ ## master
4
+
5
+ ## 0.1.0.beta1 (2018-01-08)
6
+
7
+ - Initial version. ([@ssnickolay][], [@palkan][])
8
+
9
+ [@palkan]: https://github.com/palkan
10
+ [@ssnickolay]: https://github.com/ssnickolay
11
+
data/Gemfile CHANGED
@@ -1,6 +1,15 @@
1
- source "https://rubygems.org"
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
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/clowne`. To experiment with that code, run `bin/console` for an interactive prompt.
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
- TODO: Delete this and the text above, and describe your gem
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
- Add this line to your application's Gemfile:
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
- And then execute:
38
+ ## Quick Start
16
39
 
17
- $ bundle
40
+ This is a basic example that demonstrates how to clone your ActiveRecord model. For detailed documentation see [Features](#features).
18
41
 
19
- Or install it yourself as:
42
+ At first, define your cloneable model
20
43
 
21
- $ gem install clowne
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
- ## Usage
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
- TODO: Write usage instructions here
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
- ## Development
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
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
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
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
549
+ ## Maintainers
32
550
 
33
- ## Contributing
551
+ - [Vladimir Dementyev](https://github.com/palkan)
34
552
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/clowne.
553
+ - [Sverchkov Nikolay](https://github.com/ssnickolay)
36
554
 
37
555
  ## License
38
556