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.
- checksums.yaml +5 -5
- data/.codeclimate.yml +7 -0
- data/.gitattributes +1 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +16 -33
- data/.travis.yml +14 -10
- data/CHANGELOG.md +39 -2
- data/Gemfile +11 -6
- data/README.md +48 -384
- data/Rakefile +3 -3
- data/clowne.gemspec +16 -8
- data/docs/.nojekyll +0 -0
- data/docs/.rubocop.yml +18 -0
- data/docs/CNAME +1 -0
- data/docs/README.md +131 -0
- data/docs/_sidebar.md +25 -0
- data/docs/active_record.md +33 -0
- data/docs/after_clone.md +53 -0
- data/docs/after_persist.md +77 -0
- data/docs/architecture.md +138 -0
- data/docs/assets/docsify.min.js +1 -0
- data/docs/assets/prism-ruby.min.js +1 -0
- data/docs/assets/styles.css +348 -0
- data/docs/assets/vue.css +1 -0
- data/docs/clone_mapper.md +59 -0
- data/docs/customization.md +63 -0
- data/docs/exclude_association.md +61 -0
- data/docs/finalize.md +31 -0
- data/docs/from_v02_to_v1.md +83 -0
- data/docs/getting_started.md +171 -0
- data/docs/implicit_cloner.md +33 -0
- data/docs/include_association.md +133 -0
- data/docs/index.html +29 -0
- data/docs/init_as.md +40 -0
- data/docs/inline_configuration.md +37 -0
- data/docs/nullify.md +33 -0
- data/docs/operation.md +55 -0
- data/docs/parameters.md +112 -0
- data/docs/sequel.md +50 -0
- data/docs/supported_adapters.md +10 -0
- data/docs/testing.md +194 -0
- data/docs/traits.md +25 -0
- data/gemfiles/activerecord42.gemfile +7 -4
- data/gemfiles/jruby.gemfile +8 -4
- data/gemfiles/railsmaster.gemfile +8 -5
- data/lib/clowne.rb +12 -7
- data/lib/clowne/adapters/active_record.rb +6 -16
- data/lib/clowne/adapters/active_record/associations.rb +8 -6
- data/lib/clowne/adapters/active_record/associations/base.rb +5 -49
- data/lib/clowne/adapters/active_record/associations/belongs_to.rb +29 -0
- data/lib/clowne/adapters/active_record/associations/has_one.rb +9 -1
- data/lib/clowne/adapters/active_record/associations/noop.rb +4 -1
- data/lib/clowne/adapters/active_record/dsl.rb +33 -0
- data/lib/clowne/adapters/active_record/resolvers/association.rb +38 -0
- data/lib/clowne/adapters/base.rb +53 -41
- data/lib/clowne/adapters/base/association.rb +78 -0
- data/lib/clowne/adapters/registry.rb +54 -11
- data/lib/clowne/adapters/sequel.rb +29 -0
- data/lib/clowne/adapters/sequel/associations.rb +26 -0
- data/lib/clowne/adapters/sequel/associations/base.rb +27 -0
- data/lib/clowne/adapters/sequel/associations/many_to_many.rb +23 -0
- data/lib/clowne/adapters/sequel/associations/noop.rb +16 -0
- data/lib/clowne/adapters/sequel/associations/one_to_many.rb +28 -0
- data/lib/clowne/adapters/sequel/associations/one_to_one.rb +28 -0
- data/lib/clowne/adapters/sequel/copier.rb +23 -0
- data/lib/clowne/adapters/sequel/operation.rb +35 -0
- data/lib/clowne/adapters/sequel/record_wrapper.rb +43 -0
- data/lib/clowne/adapters/sequel/resolvers/after_persist.rb +22 -0
- data/lib/clowne/adapters/sequel/resolvers/association.rb +51 -0
- data/lib/clowne/adapters/sequel/specifications/after_persist_does_not_support.rb +15 -0
- data/lib/clowne/cloner.rb +50 -20
- data/lib/clowne/declarations.rb +15 -11
- data/lib/clowne/declarations/after_clone.rb +21 -0
- data/lib/clowne/declarations/after_persist.rb +21 -0
- data/lib/clowne/declarations/base.rb +13 -0
- data/lib/clowne/declarations/exclude_association.rb +1 -6
- data/lib/clowne/declarations/finalize.rb +3 -2
- data/lib/clowne/declarations/include_association.rb +16 -4
- data/lib/clowne/declarations/init_as.rb +21 -0
- data/lib/clowne/declarations/nullify.rb +3 -2
- data/lib/clowne/declarations/trait.rb +3 -0
- data/lib/clowne/dsl.rb +9 -0
- data/lib/clowne/ext/lambda_as_proc.rb +17 -0
- data/lib/clowne/ext/orm_ext.rb +21 -0
- data/lib/clowne/ext/record_key.rb +12 -0
- data/lib/clowne/ext/string_constantize.rb +10 -4
- data/lib/clowne/ext/yield_self_then.rb +25 -0
- data/lib/clowne/planner.rb +27 -7
- data/lib/clowne/resolvers/after_clone.rb +17 -0
- data/lib/clowne/resolvers/after_persist.rb +18 -0
- data/lib/clowne/resolvers/finalize.rb +12 -0
- data/lib/clowne/resolvers/init_as.rb +13 -0
- data/lib/clowne/resolvers/nullify.rb +15 -0
- data/lib/clowne/rspec.rb +5 -0
- data/lib/clowne/rspec/clone_association.rb +99 -0
- data/lib/clowne/rspec/clone_associations.rb +26 -0
- data/lib/clowne/rspec/helpers.rb +35 -0
- data/lib/clowne/utils/clone_mapper.rb +26 -0
- data/lib/clowne/utils/operation.rb +95 -0
- data/lib/clowne/utils/options.rb +39 -0
- data/lib/clowne/utils/params.rb +64 -0
- data/lib/clowne/utils/plan.rb +90 -0
- data/lib/clowne/version.rb +1 -1
- metadata +140 -20
- data/lib/clowne/adapters/active_record/association.rb +0 -34
- data/lib/clowne/adapters/base/finalize.rb +0 -19
- data/lib/clowne/adapters/base/nullify.rb +0 -19
- data/lib/clowne/plan.rb +0 -81
data/Rakefile
CHANGED
data/clowne.gemspec
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
lib = File.expand_path('
|
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/
|
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.
|
20
|
-
|
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', '~>
|
24
|
-
spec.add_development_dependency 'rake', '~>
|
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.
|
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
|
data/docs/.nojekyll
ADDED
File without changes
|
data/docs/.rubocop.yml
ADDED
@@ -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
|
data/docs/CNAME
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
clowne.evilmartians.io
|
data/docs/README.md
ADDED
@@ -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).
|
data/docs/_sidebar.md
ADDED
@@ -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
|
+
```
|
data/docs/after_clone.md
ADDED
@@ -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
|
+
```
|