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.
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