clowne 0.1.0.pre1 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (108) hide show
  1. checksums.yaml +5 -5
  2. data/.codeclimate.yml +7 -0
  3. data/.gitattributes +1 -0
  4. data/.gitignore +1 -0
  5. data/.rubocop.yml +16 -33
  6. data/.travis.yml +14 -10
  7. data/CHANGELOG.md +39 -2
  8. data/Gemfile +11 -6
  9. data/README.md +48 -384
  10. data/Rakefile +3 -3
  11. data/clowne.gemspec +16 -8
  12. data/docs/.nojekyll +0 -0
  13. data/docs/.rubocop.yml +18 -0
  14. data/docs/CNAME +1 -0
  15. data/docs/README.md +131 -0
  16. data/docs/_sidebar.md +25 -0
  17. data/docs/active_record.md +33 -0
  18. data/docs/after_clone.md +53 -0
  19. data/docs/after_persist.md +77 -0
  20. data/docs/architecture.md +138 -0
  21. data/docs/assets/docsify.min.js +1 -0
  22. data/docs/assets/prism-ruby.min.js +1 -0
  23. data/docs/assets/styles.css +348 -0
  24. data/docs/assets/vue.css +1 -0
  25. data/docs/clone_mapper.md +59 -0
  26. data/docs/customization.md +63 -0
  27. data/docs/exclude_association.md +61 -0
  28. data/docs/finalize.md +31 -0
  29. data/docs/from_v02_to_v1.md +83 -0
  30. data/docs/getting_started.md +171 -0
  31. data/docs/implicit_cloner.md +33 -0
  32. data/docs/include_association.md +133 -0
  33. data/docs/index.html +29 -0
  34. data/docs/init_as.md +40 -0
  35. data/docs/inline_configuration.md +37 -0
  36. data/docs/nullify.md +33 -0
  37. data/docs/operation.md +55 -0
  38. data/docs/parameters.md +112 -0
  39. data/docs/sequel.md +50 -0
  40. data/docs/supported_adapters.md +10 -0
  41. data/docs/testing.md +194 -0
  42. data/docs/traits.md +25 -0
  43. data/gemfiles/activerecord42.gemfile +7 -4
  44. data/gemfiles/jruby.gemfile +8 -4
  45. data/gemfiles/railsmaster.gemfile +8 -5
  46. data/lib/clowne.rb +12 -7
  47. data/lib/clowne/adapters/active_record.rb +6 -16
  48. data/lib/clowne/adapters/active_record/associations.rb +8 -6
  49. data/lib/clowne/adapters/active_record/associations/base.rb +5 -49
  50. data/lib/clowne/adapters/active_record/associations/belongs_to.rb +29 -0
  51. data/lib/clowne/adapters/active_record/associations/has_one.rb +9 -1
  52. data/lib/clowne/adapters/active_record/associations/noop.rb +4 -1
  53. data/lib/clowne/adapters/active_record/dsl.rb +33 -0
  54. data/lib/clowne/adapters/active_record/resolvers/association.rb +38 -0
  55. data/lib/clowne/adapters/base.rb +53 -41
  56. data/lib/clowne/adapters/base/association.rb +78 -0
  57. data/lib/clowne/adapters/registry.rb +54 -11
  58. data/lib/clowne/adapters/sequel.rb +29 -0
  59. data/lib/clowne/adapters/sequel/associations.rb +26 -0
  60. data/lib/clowne/adapters/sequel/associations/base.rb +27 -0
  61. data/lib/clowne/adapters/sequel/associations/many_to_many.rb +23 -0
  62. data/lib/clowne/adapters/sequel/associations/noop.rb +16 -0
  63. data/lib/clowne/adapters/sequel/associations/one_to_many.rb +28 -0
  64. data/lib/clowne/adapters/sequel/associations/one_to_one.rb +28 -0
  65. data/lib/clowne/adapters/sequel/copier.rb +23 -0
  66. data/lib/clowne/adapters/sequel/operation.rb +35 -0
  67. data/lib/clowne/adapters/sequel/record_wrapper.rb +43 -0
  68. data/lib/clowne/adapters/sequel/resolvers/after_persist.rb +22 -0
  69. data/lib/clowne/adapters/sequel/resolvers/association.rb +51 -0
  70. data/lib/clowne/adapters/sequel/specifications/after_persist_does_not_support.rb +15 -0
  71. data/lib/clowne/cloner.rb +50 -20
  72. data/lib/clowne/declarations.rb +15 -11
  73. data/lib/clowne/declarations/after_clone.rb +21 -0
  74. data/lib/clowne/declarations/after_persist.rb +21 -0
  75. data/lib/clowne/declarations/base.rb +13 -0
  76. data/lib/clowne/declarations/exclude_association.rb +1 -6
  77. data/lib/clowne/declarations/finalize.rb +3 -2
  78. data/lib/clowne/declarations/include_association.rb +16 -4
  79. data/lib/clowne/declarations/init_as.rb +21 -0
  80. data/lib/clowne/declarations/nullify.rb +3 -2
  81. data/lib/clowne/declarations/trait.rb +3 -0
  82. data/lib/clowne/dsl.rb +9 -0
  83. data/lib/clowne/ext/lambda_as_proc.rb +17 -0
  84. data/lib/clowne/ext/orm_ext.rb +21 -0
  85. data/lib/clowne/ext/record_key.rb +12 -0
  86. data/lib/clowne/ext/string_constantize.rb +10 -4
  87. data/lib/clowne/ext/yield_self_then.rb +25 -0
  88. data/lib/clowne/planner.rb +27 -7
  89. data/lib/clowne/resolvers/after_clone.rb +17 -0
  90. data/lib/clowne/resolvers/after_persist.rb +18 -0
  91. data/lib/clowne/resolvers/finalize.rb +12 -0
  92. data/lib/clowne/resolvers/init_as.rb +13 -0
  93. data/lib/clowne/resolvers/nullify.rb +15 -0
  94. data/lib/clowne/rspec.rb +5 -0
  95. data/lib/clowne/rspec/clone_association.rb +99 -0
  96. data/lib/clowne/rspec/clone_associations.rb +26 -0
  97. data/lib/clowne/rspec/helpers.rb +35 -0
  98. data/lib/clowne/utils/clone_mapper.rb +26 -0
  99. data/lib/clowne/utils/operation.rb +95 -0
  100. data/lib/clowne/utils/options.rb +39 -0
  101. data/lib/clowne/utils/params.rb +64 -0
  102. data/lib/clowne/utils/plan.rb +90 -0
  103. data/lib/clowne/version.rb +1 -1
  104. metadata +140 -20
  105. data/lib/clowne/adapters/active_record/association.rb +0 -34
  106. data/lib/clowne/adapters/base/finalize.rb +0 -19
  107. data/lib/clowne/adapters/base/nullify.rb +0 -19
  108. data/lib/clowne/plan.rb +0 -81
data/Rakefile CHANGED
@@ -1,6 +1,6 @@
1
- require 'bundler/gem_tasks'
2
- require 'rspec/core/rake_task'
3
- require 'rubocop/rake_task'
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+ require "rubocop/rake_task"
4
4
 
5
5
  RSpec::Core::RakeTask.new(:spec)
6
6
  RuboCop::RakeTask.new
@@ -1,4 +1,4 @@
1
- lib = File.expand_path('../lib', __FILE__)
1
+ lib = File.expand_path('lib', __dir__)
2
2
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
3
  require 'clowne/version'
4
4
 
@@ -8,21 +8,29 @@ Gem::Specification.new do |spec|
8
8
  spec.authors = ['Vladimir Dementyev', 'Sverchkov Nikolay']
9
9
  spec.email = ['palkan@evilmartians.com', 'ssnikolay@gmail.com']
10
10
 
11
- spec.summary = 'A flexible gem for cloning your models.'
11
+ spec.summary = 'A flexible gem for cloning your models'
12
12
  spec.description = 'A flexible gem for cloning your models.'
13
- spec.homepage = 'https://github.com/palkan/clowne'
13
+ spec.homepage = 'https://github.com/clowne-rb/clowne'
14
14
  spec.license = 'MIT'
15
15
 
16
16
  spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
17
  f.match(%r{^(test|spec|features)/})
18
18
  end
19
- spec.bindir = 'exe'
20
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.metadata = {
20
+ "bug_tracker_uri" => "http://github.com/clowne-rb/clowne/issues",
21
+ "changelog_uri" => "https://github.com/clowne-rb/clowne/blob/master/CHANGELOG.md",
22
+ "documentation_uri" => "https://clowne.evilmartians.io/",
23
+ "homepage_uri" => "https://clowne.evilmartians.io/",
24
+ "source_code_uri" => "http://github.com/clowne-rb/clowne"
25
+ }
21
26
  spec.require_paths = ['lib']
22
27
 
23
- spec.add_development_dependency 'bundler', '~> 1.14'
24
- spec.add_development_dependency 'rake', '~> 10.0'
28
+ spec.add_development_dependency 'bundler', '~> 2.0'
29
+ spec.add_development_dependency 'rake', '~> 12.3', '>= 12.3.3'
25
30
  spec.add_development_dependency 'rspec', '~> 3.0'
26
31
  spec.add_development_dependency 'factory_bot', '~> 4.8'
27
- spec.add_development_dependency 'rubocop', '~> 0.51'
32
+ spec.add_development_dependency 'rubocop', '~> 0.75.0'
33
+ spec.add_development_dependency 'rubocop-md', '~> 0.3.0'
34
+ spec.add_development_dependency 'rubocop-rspec', '~> 1.36.0'
35
+ spec.add_development_dependency 'standard', '~> 0.1.5'
28
36
  end
File without changes
@@ -0,0 +1,18 @@
1
+ inherit_gem:
2
+ standard: config/base.yml
3
+
4
+ Lint/Void:
5
+ Exclude:
6
+ - '*.md'
7
+
8
+ Metrics/AbcSize:
9
+ Enabled: false
10
+
11
+ Standard/SemanticBlocks:
12
+ Enabled: false
13
+
14
+ Metrics/BlockLength:
15
+ Enabled: false
16
+
17
+ Metrics/LineLength:
18
+ Enabled: false
@@ -0,0 +1 @@
1
+ clowne.evilmartians.io
@@ -0,0 +1,131 @@
1
+ [![Gem Version](https://badge.fury.io/rb/clowne.svg)](https://badge.fury.io/rb/clowne)
2
+ [![Build Status](https://travis-ci.org/clowne-rb/clowne.svg?branch=master)](https://travis-ci.org/clowne-rb/clowne)
3
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/9143c4f91e9d1d2a4bd1/test_coverage)](https://codeclimate.com/github/clowne-rb/clowne/test_coverage)
4
+ [![Maintainability](https://api.codeclimate.com/v1/badges/9143c4f91e9d1d2a4bd1/maintainability)](https://codeclimate.com/github/clowne-rb/clowne/maintainability)
5
+ [![Docs](https://img.shields.io/badge/docs-link-brightgreen.svg)](https://clowne.evilmartians.io)
6
+
7
+ # Clowne
8
+
9
+ A flexible gem for cloning your models. Clowne focuses on ease of use and provides the ability to connect various ORM adapters.
10
+
11
+ 📖 Read [Evil Martians Chronicles](https://evilmartians.com/chronicles/clowne-clone-ruby-models-with-a-smile) to learn about possible use cases.
12
+
13
+ 📑 [Documentation](https://clowne.evilmartians.io)
14
+
15
+ <a href="https://evilmartians.com/">
16
+ <img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54"></a>
17
+
18
+
19
+ ## Installation
20
+
21
+ To install Clowne with RubyGems:
22
+
23
+ ```ruby
24
+ gem install clowne
25
+ ```
26
+
27
+ Or add this line to your application's Gemfile:
28
+
29
+ ```ruby
30
+ gem "clowne"
31
+ ```
32
+
33
+ ## Quick Start
34
+
35
+ Assume that you have the following model:
36
+
37
+ ```ruby
38
+ class User < ActiveRecord::Base
39
+ # create_table :users do |t|
40
+ # t.string :login
41
+ # t.string :email
42
+ # t.timestamps null: false
43
+ # end
44
+
45
+ has_one :profile
46
+ has_many :posts
47
+ end
48
+
49
+ class Profile < ActiveRecord::Base
50
+ # create_table :profiles do |t|
51
+ # t.string :name
52
+ # end
53
+ end
54
+
55
+ class Post < ActiveRecord::Base
56
+ # create_table :posts
57
+ end
58
+ ```
59
+
60
+ Let's declare our cloners first:
61
+
62
+ ```ruby
63
+ class UserCloner < Clowne::Cloner
64
+ adapter :active_record
65
+
66
+ include_association :profile, clone_with: SpecialProfileCloner
67
+ include_association :posts
68
+
69
+ nullify :login
70
+
71
+ # params here is an arbitrary Hash passed into cloner
72
+ finalize do |_source, record, params|
73
+ record.email = params[:email]
74
+ end
75
+ end
76
+
77
+ class SpecialProfileCloner < Clowne::Cloner
78
+ adapter :active_record
79
+
80
+ nullify :name
81
+ end
82
+ ```
83
+
84
+ Now you can use `UserCloner` to clone existing records:
85
+
86
+ ```ruby
87
+ user = User.last
88
+ # => <#User id: 1, login: 'clown', email: 'clown@circus.example.com'>
89
+
90
+ operation = UserCloner.call(user, email: "fake@example.com")
91
+ # => <#Clowne::Utils::Operation...>
92
+
93
+ operation.to_record
94
+ # => <#User id: nil, login: nil, email: 'fake@example.com'>
95
+
96
+ operation.persist!
97
+ # => true
98
+
99
+ cloned = operation.to_record
100
+ # => <#User id: 2, login: nil, email: 'fake@example.com'>
101
+
102
+ cloned.login
103
+ # => nil
104
+ cloned.email
105
+ # => "fake@example.com"
106
+
107
+ # associations:
108
+ cloned.posts.count == user.posts.count
109
+ # => true
110
+ cloned.profile.name
111
+ # => nil
112
+ ```
113
+
114
+ Take a look at our [documentation](https://clowne.evilmartians.io) for more info!
115
+
116
+ ### Supported ORM adapters
117
+
118
+ Adapter |1:1 |*:1 | 1:M | M:M |
119
+ ------------------------------------------|------------|------------|-------------|-------------------------|
120
+ [Active Record](active_record) | has_one | belongs_to | has_many | has_and_belongs_to|
121
+ [Sequel](sequel) | one_to_one | - | one_to_many | many_to_many |
122
+
123
+ ## Maintainers
124
+
125
+ - [Vladimir Dementyev](https://github.com/palkan)
126
+
127
+ - [Nikolay Sverchkov](https://github.com/ssnickolay)
128
+
129
+ ## License
130
+
131
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
@@ -0,0 +1,25 @@
1
+ * [Getting Started](getting_started.md)
2
+ * DSL
3
+ * [Operation](operation.md)
4
+ * [Include Association](include_association.md)
5
+ * [Exclude Association](exclude_association.md)
6
+ * [Nullify Attributes](nullify.md)
7
+ * [Finalization](finalize.md)
8
+ * [After Clone](after_clone.md)
9
+ * [After Persist](after_persist.md)
10
+ * [Initialize Cloning Target](init_as.md)
11
+ * [Traits](traits.md)
12
+ * [Parameters](parameters.md)
13
+ * Adapters
14
+ * [Supported Adapters](supported_adapters.md)
15
+ * [ActiveRecord](active_record.md)
16
+ * [Sequel](sequel.md)
17
+ * Advanced Options
18
+ * [Implicit Cloner](implicit_cloner.md)
19
+ * [Inline Configuration](inline_configuration.md)
20
+ * [Clone mapper](clone_mapper.md)
21
+ * [Architecture](architecture.md)
22
+ * [Testing](testing.md)
23
+ * [Customization](customization.md)
24
+ * Upgrade Notes
25
+ * [From v0.2.x to v1.0.0](from_v02_to_v1.md)
@@ -0,0 +1,33 @@
1
+ # Active Record
2
+
3
+ Clowne provides an optional ActiveRecord integration which allows you to configure cloners in your models and adds a shortcut to invoke cloners (`#clowne` method).
4
+
5
+ To enable this integration, you must require `"clowne/adapters/active_record/dsl"` somewhere in your app, e.g., in the initializer:
6
+
7
+ ```ruby
8
+ # config/initializers/clowne.rb
9
+ require "clowne/adapters/active_record/dsl"
10
+ Clowne.default_adapter = :active_record
11
+ ```
12
+
13
+ Now you can specify cloning configs in your AR models:
14
+
15
+ ```ruby
16
+ class User < ActiveRecord::Base
17
+ clowne_config do
18
+ include_associations :profile
19
+
20
+ nullify :email
21
+
22
+ # whatever available for your cloners,
23
+ # active_record adapter is set implicitly here
24
+ end
25
+ end
26
+ ```
27
+
28
+ And then you can clone objects like this:
29
+
30
+ ```ruby
31
+ user.clowne(traits: my_traits, **params).to_record
32
+ # => <#User id: nil...>
33
+ ```
@@ -0,0 +1,53 @@
1
+ # After Clone
2
+
3
+ The `after_clone` callbacks can help you to make additional operations on cloned record, like checking it with some business logic or actualizing cloned record attributes, before it will be saved to the database. Also it can help to avoid unneeded usage of [`after_persist`](after_persist) callbacks, and additional queries to database.
4
+
5
+ Examples:
6
+
7
+ ```ruby
8
+ class User < ActiveRecord::Base
9
+ # create_table :users do |t|
10
+ # t.string :login
11
+ # t.integer :draft_count
12
+ # end
13
+
14
+ has_many :posts # all user's posts
15
+ end
16
+
17
+ class Post < ActiveRecord::Base
18
+ # create_table :posts do |t|
19
+ # t.integer :user_id
20
+ # t.boolean :is_draft
21
+ # end
22
+
23
+ scope :draft, -> { where is_draft: true }
24
+ end
25
+
26
+ class UserCloner < Clowne::Cloner
27
+ # clone user and his posts, which is drafts
28
+ include_association :posts, scope: :draft
29
+
30
+ after_clone do |_origin, clone, **|
31
+ # actualize user attribute
32
+ clone.draft_count = clone.posts.count
33
+ end
34
+ end
35
+ ```
36
+
37
+ `after_clone` runs when you call `Operation#to_record` or [`Operation#persist`](operation) (or `Operation#persist!`)
38
+
39
+ ```ruby
40
+ # prepare data
41
+ user = User.create
42
+ 3.times { Post.create(user: user, is_draft: false) }
43
+ 2.times { Post.create(user: user, is_draft: true) }
44
+
45
+ operation = UserCloner.call(user)
46
+ # => <#Clowne::Utils::Operation ...>
47
+
48
+ clone = operation.to_record
49
+ # => <#User id: nil, draft_count: 2 ...>
50
+
51
+ clone.draft_count == user.posts.draft.count
52
+ # => true
53
+ ```
@@ -0,0 +1,77 @@
1
+ # After Persist
2
+
3
+ *Notice: `after_persist` supported only with [`active_record`](active_record.md) adapter.*
4
+
5
+ The special mechanism for transformation of cloned record. In contradistinction to [`finalize`](finalize) executes with a saved record. This type of callbacks provides a default `mapper:` parameter which contains a relation between origin and cloned objects.
6
+
7
+ `after_persist` helps to restore broken _relationships_ while cloning associations and implement some logic with already persisted clone record. (_Inspired by [issues#19](https://github.com/palkan/clowne/issues/19)_)
8
+
9
+ Examples:
10
+
11
+ ```ruby
12
+ class User < ActiveRecord::Base
13
+ # create_table :users do |t|
14
+ # t.string :login
15
+ # t.integer :bio_id
16
+ # end
17
+
18
+ has_many :posts # all user's posts including BIO
19
+ belongs_to :bio, class_name: "Post"
20
+ end
21
+
22
+ class Post < ActiveRecord::Base
23
+ # create_table :posts do |t|
24
+ # t.integer :user_id
25
+ # end
26
+ end
27
+
28
+ class UserCloner < Clowne::Cloner
29
+ include_association :posts, params: true
30
+
31
+ after_persist do |origin, clone, mapper:, **|
32
+ cloned_bio = mapper.clone_of(origin.bio)
33
+ clone.update_attributes(bio_id: cloned_bio.id)
34
+ end
35
+ end
36
+
37
+ class PostCloner < Clowne::Cloner
38
+ after_persist do |_, clone, run_job:, **|
39
+ PostBackgroundJob.perform_async(clone.id) if run_job
40
+ end
41
+ end
42
+ ```
43
+
44
+ *Notice: See more about `mapper:` [`here`](clone_mapper.md).*
45
+
46
+ `after_persist` runs when you call [`Operation#persist`](operation) (or `Operation#persist!`)
47
+
48
+ ```ruby
49
+ # prepare data
50
+ user = User.create
51
+ posts = Array.new(3) { Post.create(user: user) }
52
+ bio = posts.sample
53
+ user.update_attributes(bio_id: bio.id)
54
+
55
+ operation = UserCloner.call(user, run_job: true)
56
+ # => <#Clowne::Utils::Operation ...>
57
+
58
+ clone = operation.to_record
59
+ # => <#User id: nil, ...>
60
+
61
+ # we copy all user attributes including bio_id
62
+ # but this is wrong because bio refers to the source user's bio
63
+ # and we can fix it using after_persist when posts already saved
64
+ clone.bio_id == bio.id
65
+ # => true
66
+
67
+ # save clone and run after_persist callbacks
68
+ operation.persit
69
+
70
+ clone.bio_id == bio.id
71
+ # => false
72
+
73
+ clone.posts.pluck(:id).include?(clone.bio_id)
74
+ # => true
75
+ ```
76
+
77
+ *Notice: be careful while using after_persist feature! If you clone a fat record (with a lot of associations) and will implement complex logic inside `after_persist` callback, it may affect your system.*
@@ -0,0 +1,138 @@
1
+ # Architecture
2
+
3
+ This post aims to help developers and contributors to understand how Clowne works under the hood.
4
+
5
+ There are two main notions we want to talk about: **declaration set** and **cloning plan**.
6
+
7
+ ## Declaration set
8
+
9
+ _Declaration set_ is a result of calling Clowne DSL methods in the cloner class. There is (almost) one-to-one correspondence between configuration calls and declarations.
10
+
11
+ Consider an example:
12
+
13
+ ```ruby
14
+ class UserCloner < Clowne::Cloner
15
+ include_association :profile
16
+ include_association :posts
17
+
18
+ nullify :external_id, :slug
19
+
20
+ trait :with_social_profile do
21
+ include_association :social_profiles
22
+ end
23
+ end
24
+ ```
25
+
26
+ This cloner's declaration set contains 3 declarations. You can access it through `.declarations` method:
27
+
28
+ ```ruby
29
+ UserCloner.declarations #=>
30
+ # [
31
+ # <#Clowne::Declarations::IncludeAssociation...>,
32
+ # <#Clowne::Declarations::IncludeAssociation...>,
33
+ # <#Clowne::Declarations::Nullify...>
34
+ # ]
35
+ ```
36
+
37
+ **NOTE:** `include_associations` and `exclude_associations` methods are just syntactic sugar, so they produce multiple declarations.
38
+
39
+ Traits are not included in the declaration set. Moreover, each trait is just a wrapper over a declaration set. You can access traits through `.traits` method:
40
+
41
+ ```ruby
42
+ UserCloner.traits #=>
43
+ # {
44
+ # with_social_profiles: <#Clowne::Declarations::Trait...>
45
+ # }
46
+
47
+ UserCloner.traits[:with_social_profiles].declarations #=>
48
+ # [
49
+ # <#Clowne::Declarations::IncludeAssociation...>
50
+ # ]
51
+ ```
52
+
53
+ ## Cloning plan
54
+
55
+ Every time you use a cloner the corresponding declaration set (which is the cloner itself declarations combined with all the specified traits declaration sets) is compiled into a _cloning plan_.
56
+
57
+ The process is straightforward: starting with an empty `Plan`, we're applying every declaration to it.
58
+ Every declaration object must implement `#compile(plan)` method, which populates the plan with _actions_. For example:
59
+
60
+ ```ruby
61
+ class IncludeAssociation
62
+ def compile(plan)
63
+ # add a `name` key with the specified value (self)
64
+ # to :association set
65
+ plan.add_to(:association, name, self)
66
+ end
67
+ end
68
+ ```
69
+
70
+ `Plan` supports 3 types of registers:
71
+
72
+ 1) Scalars:
73
+
74
+ ```ruby
75
+ plan.set(key, value)
76
+ plan.remove(key)
77
+ ```
78
+
79
+ Used by [`init_as`](init_as.md) declaration.
80
+
81
+ 2) Append-only lists:
82
+
83
+ ```ruby
84
+ plan.add(key, value)
85
+ ```
86
+
87
+ Used by [`nullify`](nullify.md) and [`finalize`](finalize.md) declarations.
88
+
89
+ 3) Two-phase set (2P-Set\*):
90
+
91
+ ```ruby
92
+ plan.add_to(type, key, value)
93
+ plan.remove_from(type, key)
94
+ ```
95
+
96
+ Used by [`include_association`](include_association.md) and [`exclude_association`](exclude_association.md).
97
+
98
+
99
+ \* 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.
100
+
101
+ Thus, the resulting plan is just a key-value storage.
102
+
103
+ ## Plan resolving
104
+
105
+ The final step in the cloning process is to apply the compiled plan to a record.
106
+
107
+ Every adapter registers its own resolvers for each plan _key_ (or _action_). The [order of execution](getting_started?id=execution-order) is also specified by the adapter.
108
+
109
+ For example, you can override `:nullify` resolver to handle associations:
110
+
111
+ ```ruby
112
+ module NullifyWithAssociation
113
+ def self.call(source, record, declaration, _params)
114
+ reflection = source.class.reflections[declaration.name.to_s]
115
+
116
+ # fallback to built-in nullify
117
+ if reflection.nil?
118
+ Clowne::Adapters::Base::Nullify.call(source, record, declaration)
119
+ else
120
+ association = record.__send__(declaration.name)
121
+ next record if association.nil?
122
+
123
+ association.is_a?(ActiveRecord::Relation) ? association.destroy_all : association.destroy
124
+ record.association(declaration.name).reset
125
+
126
+ # resolver must return cloned record
127
+ record
128
+ end
129
+ end
130
+ end
131
+
132
+ Clowne::Adapters::ActiveRecord.register_resolver(
133
+ :nullify,
134
+ NullifyWithAssociation,
135
+ # specify the order of execution (available options are `prepend`, `before`, `after`)
136
+ before: :finalize
137
+ )
138
+ ```