clowne 1.4.0 → 1.5.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +0 -2
- data/lib/clowne/adapters/active_record/associations/noop.rb +1 -1
- data/lib/clowne/adapters/base/association.rb +1 -1
- data/lib/clowne/adapters/sequel/associations/one_to_many.rb +0 -4
- data/lib/clowne/ext/record_key.rb +1 -1
- data/lib/clowne/version.rb +1 -1
- metadata +7 -113
- data/.codeclimate.yml +0 -7
- data/.gitattributes +0 -1
- data/.github/workflows/rspec-jruby.yml +0 -33
- data/.github/workflows/rspec-truffle.yml +0 -35
- data/.github/workflows/rspec.yml +0 -51
- data/.github/workflows/rubocop.yml +0 -20
- data/.gitignore +0 -16
- data/.rspec +0 -3
- data/.rubocop.yml +0 -29
- data/.rufo +0 -3
- data/Gemfile +0 -20
- data/Rakefile +0 -8
- data/bin/console +0 -14
- data/bin/setup +0 -8
- data/clowne.gemspec +0 -36
- data/docs/.nojekyll +0 -0
- data/docs/.rubocop.yml +0 -15
- data/docs/CNAME +0 -1
- data/docs/README.md +0 -131
- data/docs/_sidebar.md +0 -25
- data/docs/active_record.md +0 -33
- data/docs/after_clone.md +0 -53
- data/docs/after_persist.md +0 -77
- data/docs/architecture.md +0 -138
- data/docs/assets/docsify.min.js +0 -1
- data/docs/assets/prism-ruby.min.js +0 -1
- data/docs/assets/styles.css +0 -348
- data/docs/assets/vue.css +0 -1
- data/docs/clone_mapper.md +0 -59
- data/docs/customization.md +0 -63
- data/docs/exclude_association.md +0 -61
- data/docs/finalize.md +0 -31
- data/docs/from_v02_to_v1.md +0 -83
- data/docs/getting_started.md +0 -171
- data/docs/implicit_cloner.md +0 -33
- data/docs/include_association.md +0 -133
- data/docs/index.html +0 -29
- data/docs/init_as.md +0 -40
- data/docs/inline_configuration.md +0 -37
- data/docs/nullify.md +0 -33
- data/docs/operation.md +0 -55
- data/docs/parameters.md +0 -112
- data/docs/sequel.md +0 -50
- data/docs/supported_adapters.md +0 -10
- data/docs/testing.md +0 -194
- data/docs/traits.md +0 -25
- data/gemfiles/activerecord42.gemfile +0 -9
- data/gemfiles/jruby.gemfile +0 -10
- data/gemfiles/railsmaster.gemfile +0 -10
- data/lib/clowne/ext/yield_self_then.rb +0 -25
data/docs/getting_started.md
DELETED
@@ -1,171 +0,0 @@
|
|
1
|
-
# Getting Started
|
2
|
-
|
3
|
-
## Installation
|
4
|
-
|
5
|
-
To install Clowne with RubyGems:
|
6
|
-
|
7
|
-
```ruby
|
8
|
-
gem install clowne
|
9
|
-
```
|
10
|
-
|
11
|
-
Or add this line to your application's Gemfile:
|
12
|
-
|
13
|
-
```ruby
|
14
|
-
gem "clowne"
|
15
|
-
```
|
16
|
-
|
17
|
-
## Configuration
|
18
|
-
|
19
|
-
Basic cloner implementation looks like:
|
20
|
-
|
21
|
-
```ruby
|
22
|
-
class SomeCloner < Clowne::Cloner
|
23
|
-
adapter :active_record # or adapter Clowne::Adapters::ActiveRecord
|
24
|
-
# some implementation ...
|
25
|
-
end
|
26
|
-
```
|
27
|
-
|
28
|
-
You can configure the default adapter for cloners:
|
29
|
-
|
30
|
-
```ruby
|
31
|
-
# put to initializer
|
32
|
-
# e.g. config/initializers/clowne.rb
|
33
|
-
Clowne.default_adapter = :active_record
|
34
|
-
```
|
35
|
-
|
36
|
-
and skip explicit adapter declaration
|
37
|
-
|
38
|
-
```ruby
|
39
|
-
class SomeCloner < Clowne::Cloner
|
40
|
-
# some implementation ...
|
41
|
-
end
|
42
|
-
```
|
43
|
-
See the list of [available adapters](supported_adapters.md).
|
44
|
-
|
45
|
-
## Basic Example
|
46
|
-
|
47
|
-
Assume that you have the following model:
|
48
|
-
|
49
|
-
```ruby
|
50
|
-
class User < ActiveRecord::Base
|
51
|
-
# create_table :users do |t|
|
52
|
-
# t.string :login
|
53
|
-
# t.string :email
|
54
|
-
# t.timestamps null: false
|
55
|
-
# end
|
56
|
-
|
57
|
-
has_one :profile
|
58
|
-
has_many :posts
|
59
|
-
end
|
60
|
-
|
61
|
-
class Profile < ActiveRecord::Base
|
62
|
-
# create_table :profiles do |t|
|
63
|
-
# t.string :name
|
64
|
-
# end
|
65
|
-
end
|
66
|
-
|
67
|
-
class Post < ActiveRecord::Base
|
68
|
-
# create_table :posts
|
69
|
-
end
|
70
|
-
```
|
71
|
-
|
72
|
-
Let's declare our cloners first:
|
73
|
-
|
74
|
-
```ruby
|
75
|
-
class UserCloner < Clowne::Cloner
|
76
|
-
adapter :active_record
|
77
|
-
|
78
|
-
include_association :profile, clone_with: SpecialProfileCloner
|
79
|
-
include_association :posts
|
80
|
-
|
81
|
-
nullify :login
|
82
|
-
|
83
|
-
# params here is an arbitrary Hash passed into cloner
|
84
|
-
finalize do |_source, record, **params|
|
85
|
-
record.email = params[:email]
|
86
|
-
end
|
87
|
-
end
|
88
|
-
|
89
|
-
class SpecialProfileCloner < Clowne::Cloner
|
90
|
-
adapter :active_record
|
91
|
-
|
92
|
-
nullify :name
|
93
|
-
end
|
94
|
-
```
|
95
|
-
|
96
|
-
Now you can use `UserCloner` to clone existing records:
|
97
|
-
|
98
|
-
```ruby
|
99
|
-
user = User.last
|
100
|
-
# => <#User id: 1, login: 'clown', email: 'clown@circus.example.com'>
|
101
|
-
|
102
|
-
operation = UserCloner.call(user, email: "fake@example.com")
|
103
|
-
# => <#Clowne::Utils::Operation...>
|
104
|
-
|
105
|
-
operation.to_record
|
106
|
-
# => <#User id: nil, login: nil, email: 'fake@example.com'>
|
107
|
-
|
108
|
-
operation.persist!
|
109
|
-
# => true
|
110
|
-
|
111
|
-
cloned = operation.to_record
|
112
|
-
# => <#User id: 2, login: nil, email: 'fake@example.com'>
|
113
|
-
|
114
|
-
cloned.login
|
115
|
-
# => nil
|
116
|
-
cloned.email
|
117
|
-
# => "fake@example.com"
|
118
|
-
|
119
|
-
# associations:
|
120
|
-
cloned.posts.count == user.posts.count
|
121
|
-
# => true
|
122
|
-
cloned.profile.name
|
123
|
-
# => nil
|
124
|
-
```
|
125
|
-
|
126
|
-
## Overview
|
127
|
-
|
128
|
-
In [the basic example](#basic-example), you can see that Clowne consists of flexible DSL which is used in a class inherited of `Clowne::Cloner`.
|
129
|
-
|
130
|
-
You can combinate this DSL via [`traits`](traits.md) and make a cloning plan which exactly you want.
|
131
|
-
|
132
|
-
**We strongly recommend [`write tests`](testing.md) to cover resulting cloner logic**
|
133
|
-
|
134
|
-
Cloner class returns [`Operation`](operation.md) instance as a result of cloning. The operation provides methods to save cloned record. You can wrap this call to a transaction if it is necessary.
|
135
|
-
|
136
|
-
### Execution Order
|
137
|
-
|
138
|
-
The order of cloning actions depends on the adapter (i.e., could be customized).
|
139
|
-
|
140
|
-
All built-in adapters have the same order and what happens when you call `Operation#persist`:
|
141
|
-
- init clone (see [`init_as`](init_as.md)) (empty by default)
|
142
|
-
- [`clone associations`](include_association.md)
|
143
|
-
- [`nullify`](nullify.md) attributes
|
144
|
-
- run [`finalize`](finalize.md) blocks. _The order of [`finalize`](finalize.md) blocks is the order they've been written._
|
145
|
-
- run [`after_clone`](after_clone.md) callbacks
|
146
|
-
- __SAVE CLONED RECORD__
|
147
|
-
- run [`after_persist`](after_persist.md) callbacks
|
148
|
-
|
149
|
-
## Motivation & Alternatives
|
150
|
-
|
151
|
-
### Why did we decide to build our own cloning gem instead of using the existing solutions?
|
152
|
-
|
153
|
-
First, the existing solutions turned out not to be stable and flexible enough for us.
|
154
|
-
|
155
|
-
Secondly, they are Rails-only (or, more precisely, ActiveRecord-only).
|
156
|
-
|
157
|
-
Nevertheless, thanks to [amoeba](https://github.com/amoeba-rb/amoeba) and [deep_cloneable](https://github.com/moiristo/deep_cloneable) for inspiration.
|
158
|
-
|
159
|
-
For ActiveRecord we support amoeba-like [in-model configuration](active_record.md) and you can add missing DSL declarations yourself [easily](customization.md).
|
160
|
-
|
161
|
-
We also provide an ability to specify cloning [configuration in-place](inline_configuration.md) like `deep_clonable` does.
|
162
|
-
|
163
|
-
So, we took the best of these too and brought to the outside-of-Rails world.
|
164
|
-
|
165
|
-
### Why build a gem to clone models at all?
|
166
|
-
|
167
|
-
That's a good question. Of course, you can write plain old Ruby services do handle the cloning logic. But for complex models hierarchies, this approach has major disadvantages: high code complexity and lack of re-usability.
|
168
|
-
|
169
|
-
The things become even worse when you deal with STI models and different cloning contexts.
|
170
|
-
|
171
|
-
That's why we decided to build a specific cloning tool.
|
data/docs/implicit_cloner.md
DELETED
@@ -1,33 +0,0 @@
|
|
1
|
-
# Implicit Cloner
|
2
|
-
|
3
|
-
When [cloning associations](include_association.md) Clowne tries to infer an appropriate cloner class for the records (unless `clone_with` specified).
|
4
|
-
|
5
|
-
It relies on the naming convention: `MyModel` -> `MyModelCloner`.
|
6
|
-
|
7
|
-
Consider an example:
|
8
|
-
|
9
|
-
```ruby
|
10
|
-
class User < ActiveRecord::Base
|
11
|
-
has_one :profile
|
12
|
-
end
|
13
|
-
|
14
|
-
class UserCloner < Clowne::Cloner
|
15
|
-
include_association :profile
|
16
|
-
end
|
17
|
-
|
18
|
-
class ProfileCloner < Clowne::Cloner
|
19
|
-
finalize do |source, record|
|
20
|
-
record.name = "Clone of #{source.name}"
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
user = User.last
|
25
|
-
user.profile.name
|
26
|
-
#=> "Bimbo"
|
27
|
-
|
28
|
-
cloned = UserCloner.call(user).to_record
|
29
|
-
cloned.profile.name
|
30
|
-
# => "Clone of Bimbo"
|
31
|
-
```
|
32
|
-
|
33
|
-
**NOTE:** when using [in-model cloner](active_record.md) for ActiveRecord it is used by default.
|
data/docs/include_association.md
DELETED
@@ -1,133 +0,0 @@
|
|
1
|
-
# Include Association
|
2
|
-
|
3
|
-
Use this declaration to clone model's associations:
|
4
|
-
|
5
|
-
```ruby
|
6
|
-
class User < ActiveRecord::Base
|
7
|
-
has_one :profile
|
8
|
-
end
|
9
|
-
|
10
|
-
class UserCloner < Clowne::Cloner
|
11
|
-
include_association :profile
|
12
|
-
end
|
13
|
-
```
|
14
|
-
|
15
|
-
Looks pretty simple, right? But that's not all we may offer you! :)
|
16
|
-
|
17
|
-
The declaration supports additional arguments:
|
18
|
-
|
19
|
-
```ruby
|
20
|
-
include_association name, scope, options
|
21
|
-
```
|
22
|
-
|
23
|
-
### Supported Associations
|
24
|
-
|
25
|
-
Adapter |1:1 |*:1 | 1:M | M:M |
|
26
|
-
------------------------------------------|------------|------------|-------------|-------------------------|
|
27
|
-
[Active Record](active_record) | has_one | belongs_to | has_many | has_and_belongs_to|
|
28
|
-
[Sequel](sequel) | one_to_one | - | one_to_many | many_to_many |
|
29
|
-
|
30
|
-
## Scope
|
31
|
-
|
32
|
-
Scope can be a:
|
33
|
-
- `Symbol` - named scope.
|
34
|
-
- `Proc` - custom scope (supports parameters).
|
35
|
-
|
36
|
-
Example:
|
37
|
-
|
38
|
-
```ruby
|
39
|
-
class User < ActiveRecord::Base
|
40
|
-
has_many :accounts
|
41
|
-
has_many :posts
|
42
|
-
end
|
43
|
-
|
44
|
-
class Account < ActiveRecord::Base
|
45
|
-
scope :active, -> { where(active: true) }
|
46
|
-
end
|
47
|
-
|
48
|
-
class Post < ActiveRecord::Base
|
49
|
-
# t.string :status
|
50
|
-
end
|
51
|
-
|
52
|
-
class UserCloner < Clowne::Cloner
|
53
|
-
include_association :accounts, :active
|
54
|
-
include_association :posts, ->(params) { where(state: params[:state]) }
|
55
|
-
end
|
56
|
-
|
57
|
-
# Clone only draft posts
|
58
|
-
UserCloner.call(user, state: :draft).to_record
|
59
|
-
# => <#User id: nil, ... >
|
60
|
-
```
|
61
|
-
|
62
|
-
## Options
|
63
|
-
|
64
|
-
The following options are available:
|
65
|
-
- `:clone_with` - use custom cloner\*
|
66
|
-
- `:traits` - define special traits.
|
67
|
-
|
68
|
-
\* **NOTE:** the same cloner class would be used for **all children**
|
69
|
-
|
70
|
-
Example:
|
71
|
-
|
72
|
-
```ruby
|
73
|
-
class User < ActiveRecord::Base
|
74
|
-
has_many :posts
|
75
|
-
end
|
76
|
-
|
77
|
-
class Post < ActiveRecord::Base
|
78
|
-
# t.string :title
|
79
|
-
has_many :tags
|
80
|
-
end
|
81
|
-
```
|
82
|
-
|
83
|
-
```ruby
|
84
|
-
class PostSpecialCloner < Clowne::Cloner
|
85
|
-
nullify :title
|
86
|
-
|
87
|
-
trait :with_tags do
|
88
|
-
include_association :tags
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
class UserCloner < Clowne::Cloner
|
93
|
-
adapter :active_record
|
94
|
-
|
95
|
-
include_association :posts, clone_with: PostSpecialCloner
|
96
|
-
# or clone user's posts with tags!
|
97
|
-
# include_association :posts, clone_with: PostSpecialCloner, traits: :with_tags
|
98
|
-
end
|
99
|
-
|
100
|
-
UserCloner.call(user).to_record
|
101
|
-
# => <#User id: nil, ... >
|
102
|
-
```
|
103
|
-
|
104
|
-
**NOTE**: if custom cloner is not defined, Clowne tries to infer the [implicit cloner](implicit_cloner.md).
|
105
|
-
|
106
|
-
## Nested parameters
|
107
|
-
|
108
|
-
Follow to [documentation page](parameters.md).
|
109
|
-
|
110
|
-
## Include multiple associations
|
111
|
-
|
112
|
-
You can include multiple associations at once too:
|
113
|
-
|
114
|
-
```ruby
|
115
|
-
class User < ActiveRecord::Base
|
116
|
-
has_many :accounts
|
117
|
-
has_many :posts
|
118
|
-
end
|
119
|
-
|
120
|
-
class UserCloner < Clowne::Cloner
|
121
|
-
adapter :active_record
|
122
|
-
|
123
|
-
include_associations :accounts, :posts
|
124
|
-
end
|
125
|
-
```
|
126
|
-
|
127
|
-
**NOTE:** in that case, it's not possible to provide custom scopes and options.
|
128
|
-
|
129
|
-
### Belongs To association
|
130
|
-
|
131
|
-
You can include belongs_to association, but will do it carefully.
|
132
|
-
If you have loop by relations in your models, when you clone it will raise SystemStackError.
|
133
|
-
Check this [test](https://github.com/palkan/clowne/blob/master/spec/clowne/integrations/active_record_belongs_to_spec.rb) for instance.
|
data/docs/index.html
DELETED
@@ -1,29 +0,0 @@
|
|
1
|
-
<!DOCTYPE html>
|
2
|
-
<html lang="en">
|
3
|
-
<head>
|
4
|
-
<meta charset="UTF-8">
|
5
|
-
<title>Document</title>
|
6
|
-
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
7
|
-
<meta name="description" content="Description">
|
8
|
-
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
9
|
-
<link rel="stylesheet" href="assets/vue.css">
|
10
|
-
<link rel="stylesheet" href="assets/styles.css">
|
11
|
-
</head>
|
12
|
-
<body>
|
13
|
-
<div id="app"></div>
|
14
|
-
<script>
|
15
|
-
window.$docsify = {
|
16
|
-
name: 'Clowne',
|
17
|
-
repo: 'https://github.com/clowne-rb/clowne',
|
18
|
-
loadSidebar: true,
|
19
|
-
subMaxLevel: 2,
|
20
|
-
auto2top: true,
|
21
|
-
search: {
|
22
|
-
namespace: 'clowne'
|
23
|
-
}
|
24
|
-
}
|
25
|
-
</script>
|
26
|
-
<script src="assets/docsify.min.js"></script>
|
27
|
-
<script src="assets/prism-ruby.min.js"></script>
|
28
|
-
</body>
|
29
|
-
</html>
|
data/docs/init_as.md
DELETED
@@ -1,40 +0,0 @@
|
|
1
|
-
# Initialize Cloning Target
|
2
|
-
|
3
|
-
You can override the default Clowne method which generates a _plain_ copy of a source object.
|
4
|
-
By default, Clowne initiates the cloned record using a `#dup` method.
|
5
|
-
|
6
|
-
For example, Cloners could be used not only to generate _fresh_ new models but to apply some transformations to the existing record:
|
7
|
-
|
8
|
-
|
9
|
-
```ruby
|
10
|
-
class User < ApplicationRecord
|
11
|
-
has_one :profile
|
12
|
-
has_many :posts
|
13
|
-
end
|
14
|
-
|
15
|
-
class UserCloner < Clowne::Cloner
|
16
|
-
adapter :active_record
|
17
|
-
|
18
|
-
include_association :profile
|
19
|
-
|
20
|
-
trait :copy_settings do
|
21
|
-
# Use a `target` for all the actions
|
22
|
-
init_as { |_source, target:| target }
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
jack = User.find_by(email: "jack@evl.ms")
|
27
|
-
# => <#User id: 1, ...>
|
28
|
-
jack.create_profile(name: "Jack")
|
29
|
-
# => <#Profile id: 1, name: 'Jack', ...>
|
30
|
-
|
31
|
-
john = User.find_by(email: "john@evl.ms")
|
32
|
-
# => <#User id: 2, ...>
|
33
|
-
|
34
|
-
# we want to clone Jack's profile to John's user,
|
35
|
-
# without creating a new one
|
36
|
-
john_with_profile = UserCloner.call(jack, traits: :copy_settings, target: john).to_record
|
37
|
-
# => <#User id: 2, ...>
|
38
|
-
john_with_profile.profile
|
39
|
-
#=> <#Profile id: nil, name: 'Jack',...>
|
40
|
-
```
|
@@ -1,37 +0,0 @@
|
|
1
|
-
# Inline Configuration
|
2
|
-
|
3
|
-
You can also enhance the cloner configuration inline (i.e., add declarations dynamically):
|
4
|
-
|
5
|
-
```ruby
|
6
|
-
operation = UserCloner.call(User.last) do
|
7
|
-
exclude_association :profile
|
8
|
-
|
9
|
-
finalize do |source, record|
|
10
|
-
record.email = "clone_of_#{source.email}"
|
11
|
-
end
|
12
|
-
end
|
13
|
-
|
14
|
-
cloned = operation.to_record
|
15
|
-
|
16
|
-
cloned.email
|
17
|
-
# => "clone_of_john@example.com"
|
18
|
-
|
19
|
-
# associations:
|
20
|
-
cloned.posts.size == User.last.posts.size
|
21
|
-
# => true
|
22
|
-
cloned.profile
|
23
|
-
# => nil
|
24
|
-
```
|
25
|
-
|
26
|
-
Inline enhancement doesn't affect the _global_ configuration so that you can use it without any fear.
|
27
|
-
|
28
|
-
Thus it's also possible to clone objects without any cloner classes at all by using `Clowne::Cloner`:
|
29
|
-
|
30
|
-
```ruby
|
31
|
-
cloned = Clowne::Cloner.call(user) do
|
32
|
-
# anything you want!
|
33
|
-
end.to_record
|
34
|
-
|
35
|
-
cloned
|
36
|
-
# => <#User..
|
37
|
-
```
|
data/docs/nullify.md
DELETED
@@ -1,33 +0,0 @@
|
|
1
|
-
# Nullify Attributes
|
2
|
-
|
3
|
-
To set a bunch of attributes to `nil` you can use the `nullify` declaration:
|
4
|
-
|
5
|
-
```ruby
|
6
|
-
class User < ActiveRecord::Base
|
7
|
-
# t.string :name
|
8
|
-
# t.string :surname
|
9
|
-
# t.string :email
|
10
|
-
end
|
11
|
-
|
12
|
-
class UserCloner < Clowne::Cloner
|
13
|
-
nullify :name, :email
|
14
|
-
|
15
|
-
trait :nullify_surname do
|
16
|
-
nullify :surname
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
clone = UserCloner.call(user).to_record
|
21
|
-
clone.name.nil?
|
22
|
-
# => true
|
23
|
-
clone.email.nil?
|
24
|
-
# => true
|
25
|
-
clone.surname.nil?
|
26
|
-
# => false
|
27
|
-
|
28
|
-
clone2 = UserCloner.call(user, traits: :nullify_surname).to_record
|
29
|
-
clone2.name.nil?
|
30
|
-
# => true
|
31
|
-
clone2.surname.nil?
|
32
|
-
# => true
|
33
|
-
```
|
data/docs/operation.md
DELETED
@@ -1,55 +0,0 @@
|
|
1
|
-
# Operation
|
2
|
-
|
3
|
-
Since version 1.0 Clowne has been returning specific result object instead of a raw cloned object. It has allowed unifying interface between adapters and has opened an opportunity to implement new features. We call this object `Operation`.
|
4
|
-
|
5
|
-
An instance of `Operation` has a very clear interface:
|
6
|
-
|
7
|
-
```ruby
|
8
|
-
class User < ActiveRecord::Base; end
|
9
|
-
|
10
|
-
class UserCloner < Clowne::Cloner
|
11
|
-
nullify :email
|
12
|
-
|
13
|
-
after_persist do |_origin, cloned, **|
|
14
|
-
cloned.update(email: "evl-#{cloned.id}.ms")
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
user = User.create(email: "evl.ms")
|
19
|
-
# => <#User id: 1, email: 'evl.ms', ...>
|
20
|
-
|
21
|
-
operation = UserCloner.call(user)
|
22
|
-
|
23
|
-
# Return resulted (non saved) object:
|
24
|
-
operation.to_record
|
25
|
-
# => <#User id: nil, email: nil, ...>
|
26
|
-
|
27
|
-
# Save cloned object and call after_persist callbacks:
|
28
|
-
operation.persist # or operation.persist!
|
29
|
-
# => true
|
30
|
-
|
31
|
-
operation.to_record
|
32
|
-
# => <#User id: 2, email: 'evl-2.ms', ...>
|
33
|
-
|
34
|
-
# Call only after_persist callbacks:
|
35
|
-
user2 = operation.to_record
|
36
|
-
# => <#User id: 2, email: 'evl-2.ms', ...>
|
37
|
-
user2.update(email: "admin@example.com")
|
38
|
-
# => <#User id: 2, email: 'admin@example.com' ...>
|
39
|
-
operation.run_after_persist
|
40
|
-
# => <#User id: 2, email: 'evl-2.ms', ...>
|
41
|
-
```
|
42
|
-
|
43
|
-
The last example is weird, but it can be helpful when you need to execute `save` (or `save!`) separately from `after_persist` callbacks:
|
44
|
-
|
45
|
-
```ruby
|
46
|
-
operation = UserClone.call(user)
|
47
|
-
|
48
|
-
# Wrap main cloning into the transaction
|
49
|
-
ActiveRecord::Base.transaction do
|
50
|
-
operation.to_record.save!
|
51
|
-
end
|
52
|
-
|
53
|
-
# And after that execute after_persist without transaction
|
54
|
-
operation.run_after_persist
|
55
|
-
```
|
data/docs/parameters.md
DELETED
@@ -1,112 +0,0 @@
|
|
1
|
-
# Parameters
|
2
|
-
|
3
|
-
Clowne provides parameters for make your cloning logic more flexible. You can see their using in [`include_association`](include_association.md#scope) and [`finalize`](finalize.md) documentation pages.
|
4
|
-
|
5
|
-
Example:
|
6
|
-
|
7
|
-
```ruby
|
8
|
-
class UserCloner < Clowne::Cloner
|
9
|
-
include_association :posts, ->(params) { where(state: params[:state]) }
|
10
|
-
|
11
|
-
finalize do |_source, record, **params|
|
12
|
-
record.email = params[:email]
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
operation = UserCloner.call(user, state: :draft, email: "cloned@example.com")
|
17
|
-
cloned = operation.to_record
|
18
|
-
cloned.email
|
19
|
-
# => 'cloned@example.com'
|
20
|
-
```
|
21
|
-
|
22
|
-
## Potential Problems
|
23
|
-
|
24
|
-
Clowne is born as a part of our big project and we use it for cloning really deep object relations. When we started to use params and forwarding them between parent-child cloners we got a nasty bugs.
|
25
|
-
|
26
|
-
As result we strongly recommend to use ruby keyword arguments instead of params hash:
|
27
|
-
|
28
|
-
```ruby
|
29
|
-
# Bad
|
30
|
-
finalize do |_source, record, **params|
|
31
|
-
record.email = params[:email]
|
32
|
-
end
|
33
|
-
|
34
|
-
# Good
|
35
|
-
finalize do |_source, record, email:, **|
|
36
|
-
record.email = email
|
37
|
-
end
|
38
|
-
```
|
39
|
-
|
40
|
-
## Nested Parameters
|
41
|
-
|
42
|
-
Also we implemented control over the parameters for cloning associations (you can read more [here](https://github.com/clowne-rb/clowne/issues/15)).
|
43
|
-
|
44
|
-
Let's explain what the difference:
|
45
|
-
|
46
|
-
```ruby
|
47
|
-
class UserCloner < Clowne::Cloner
|
48
|
-
# Don't pass parameters to associations
|
49
|
-
trait :default do
|
50
|
-
include_association :profile
|
51
|
-
# equal to include_association :profile, params: false
|
52
|
-
end
|
53
|
-
|
54
|
-
# Pass all parameters to associations
|
55
|
-
trait :all_params do
|
56
|
-
include_association :profile, params: true
|
57
|
-
end
|
58
|
-
|
59
|
-
# Filter parameters by key.
|
60
|
-
# Notice: value by key must be a Hash.
|
61
|
-
|
62
|
-
trait :by_key do
|
63
|
-
include_association :profile, params: :profile
|
64
|
-
end
|
65
|
-
|
66
|
-
# Execute custom block with params as argument
|
67
|
-
trait :by_block do
|
68
|
-
include_association :profile, params: Proc.new do |params|
|
69
|
-
params[:profile].map { |k, v| [k, v.upcase] }.to_h
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
# Execute custom block with params and parent record as arguments
|
74
|
-
trait :by_block_with_parent do
|
75
|
-
include_association :profile, params: Proc.new do |params, user|
|
76
|
-
{
|
77
|
-
name: params[:profile][:name],
|
78
|
-
email: user.email
|
79
|
-
}
|
80
|
-
end
|
81
|
-
end
|
82
|
-
end
|
83
|
-
|
84
|
-
class ProfileCloner < Clowne::Cloner
|
85
|
-
finalize do |_source, record, **params|
|
86
|
-
record.jsonb_field = params
|
87
|
-
end
|
88
|
-
end
|
89
|
-
|
90
|
-
# Execute:
|
91
|
-
|
92
|
-
def get_profile_jsonb(user, trait)
|
93
|
-
params = {profile: {name: "John", surname: "Cena"}}
|
94
|
-
cloned = UserCloner.call(user, traits: trait, **params).to_record
|
95
|
-
cloned.profile.jsonb_field
|
96
|
-
end
|
97
|
-
|
98
|
-
get_profile_jsonb(user, :default)
|
99
|
-
# => {}
|
100
|
-
|
101
|
-
get_profile_jsonb(user, :all_params)
|
102
|
-
# => { profile: { name: 'John', surname: 'Cena' } }
|
103
|
-
|
104
|
-
get_profile_jsonb(user, :by_key)
|
105
|
-
# => { name: 'John', surname: 'Cena' }
|
106
|
-
|
107
|
-
get_profile_jsonb(user, :by_block)
|
108
|
-
# => { name: 'JOHN', surname: 'CENA' }
|
109
|
-
|
110
|
-
get_profile_jsonb(user, :by_block_with_parent)
|
111
|
-
# => { name: 'JOHN', email: user.email }
|
112
|
-
```
|
data/docs/sequel.md
DELETED
@@ -1,50 +0,0 @@
|
|
1
|
-
# Sequel
|
2
|
-
|
3
|
-
Under the hood, Clowne uses Sequel [`NestedAttributes` plugin](http://sequel.jeremyevans.net/rdoc-plugins/classes/Sequel/Plugins/NestedAttributes.html) for cloning source's associations, and you need to configure it.
|
4
|
-
|
5
|
-
Example:
|
6
|
-
|
7
|
-
```ruby
|
8
|
-
class UserCloner < Clowne::Cloner
|
9
|
-
adapter :sequel
|
10
|
-
|
11
|
-
include_association :account
|
12
|
-
end
|
13
|
-
|
14
|
-
class User < Sequel::Model
|
15
|
-
# Configure NestedAttributes plugin
|
16
|
-
plugin :nested_attributes
|
17
|
-
|
18
|
-
one_to_one :account
|
19
|
-
nested_attributes :account
|
20
|
-
end
|
21
|
-
```
|
22
|
-
|
23
|
-
and get cloned user
|
24
|
-
|
25
|
-
```ruby
|
26
|
-
user = User.last
|
27
|
-
operation = UserCloner.call(user)
|
28
|
-
# => <#Clowne::Adapters::Sequel::Operation...>
|
29
|
-
cloned = operation.to_record
|
30
|
-
# => <#User id: nil, ...>
|
31
|
-
cloned.new?
|
32
|
-
# => true
|
33
|
-
```
|
34
|
-
|
35
|
-
or you can save it immediately
|
36
|
-
|
37
|
-
```ruby
|
38
|
-
user = User.last
|
39
|
-
# => <#User id: 1, ...>
|
40
|
-
operation = UserCloner.call(user)
|
41
|
-
# => <#Clowne::Adapters::Sequel::Operation...>
|
42
|
-
operation.persist
|
43
|
-
# => true
|
44
|
-
cloned = operation.to_record
|
45
|
-
# => <#User id: 2, ...>
|
46
|
-
cloned.new?
|
47
|
-
# => false
|
48
|
-
```
|
49
|
-
|
50
|
-
If you try to clone association without `NestedAttributes` plugin, Clowne will skip this declaration.
|
data/docs/supported_adapters.md
DELETED
@@ -1,10 +0,0 @@
|
|
1
|
-
# Supported Adapters
|
2
|
-
|
3
|
-
Clowne supports the following ORM adapters (and associations):
|
4
|
-
|
5
|
-
Adapter |1:1 | 1:M | M:M |
|
6
|
-
---------------------------------------------------|------------|-------------|-------------------------|
|
7
|
-
[:active_record](active_record) | has_one | has_many | has_and_belongs_to_many |
|
8
|
-
[:sequel](sequel) | one_to_one | one_to_many | many_to_many |
|
9
|
-
|
10
|
-
For more information see the corresponding adapter documentation.
|