clowne 0.1.0 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.rubocop.yml +11 -45
- data/.travis.yml +14 -21
- data/CHANGELOG.md +30 -0
- data/Gemfile +11 -7
- data/README.md +38 -18
- data/Rakefile +3 -3
- data/clowne.gemspec +17 -10
- data/docs/.nojekyll +0 -0
- data/docs/.rubocop.yml +8 -2
- data/docs/CNAME +1 -0
- data/docs/README.md +131 -0
- data/docs/_sidebar.md +25 -0
- data/docs/active_record.md +4 -7
- data/docs/after_clone.md +53 -0
- data/docs/after_persist.md +77 -0
- data/docs/architecture.md +2 -5
- 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 +6 -7
- data/docs/exclude_association.md +6 -8
- data/docs/finalize.md +11 -15
- data/docs/from_v02_to_v1.md +83 -0
- data/docs/getting_started.md +171 -0
- data/docs/implicit_cloner.md +2 -5
- data/docs/include_association.md +24 -10
- data/docs/index.html +29 -0
- data/docs/init_as.md +13 -9
- data/docs/inline_configuration.md +5 -6
- data/docs/nullify.md +3 -7
- data/docs/operation.md +55 -0
- data/docs/parameters.md +112 -0
- data/docs/sequel.md +16 -22
- data/docs/supported_adapters.md +3 -6
- data/docs/testing.md +194 -0
- data/docs/traits.md +1 -4
- data/gemfiles/activerecord42.gemfile +7 -5
- data/gemfiles/jruby.gemfile +8 -6
- data/gemfiles/railsmaster.gemfile +8 -6
- data/lib/clowne.rb +12 -9
- data/lib/clowne/adapters/active_record.rb +4 -5
- data/lib/clowne/adapters/active_record/associations.rb +8 -6
- data/lib/clowne/adapters/active_record/associations/base.rb +1 -5
- data/lib/clowne/adapters/active_record/associations/belongs_to.rb +29 -0
- data/lib/clowne/adapters/active_record/associations/has_one.rb +3 -2
- data/lib/clowne/adapters/active_record/dsl.rb +2 -2
- data/lib/clowne/adapters/active_record/resolvers/association.rb +38 -0
- data/lib/clowne/adapters/base.rb +49 -44
- data/lib/clowne/adapters/base/association.rb +24 -15
- data/lib/clowne/adapters/registry.rb +49 -0
- data/lib/clowne/adapters/sequel.rb +14 -10
- data/lib/clowne/adapters/sequel/associations.rb +6 -6
- data/lib/clowne/adapters/sequel/associations/base.rb +9 -5
- data/lib/clowne/adapters/sequel/associations/many_to_many.rb +6 -2
- data/lib/clowne/adapters/sequel/associations/one_to_many.rb +7 -2
- data/lib/clowne/adapters/sequel/associations/one_to_one.rb +7 -2
- data/lib/clowne/adapters/sequel/operation.rb +35 -0
- data/lib/clowne/adapters/sequel/record_wrapper.rb +0 -16
- 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 +49 -21
- data/lib/clowne/declarations.rb +15 -12
- 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 +2 -2
- data/lib/clowne/declarations/finalize.rb +5 -4
- data/lib/clowne/declarations/include_association.rb +16 -2
- data/lib/clowne/declarations/init_as.rb +5 -4
- data/lib/clowne/declarations/nullify.rb +4 -3
- data/lib/clowne/declarations/trait.rb +1 -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 +1 -1
- data/lib/clowne/ext/record_key.rb +12 -0
- data/lib/clowne/ext/string_constantize.rb +9 -3
- data/lib/clowne/ext/yield_self_then.rb +25 -0
- data/lib/clowne/planner.rb +16 -3
- 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 +99 -45
- data/docs/alternatives.md +0 -26
- data/docs/basic_example.md +0 -66
- data/docs/configuration.md +0 -29
- data/docs/execution_order.md +0 -14
- data/docs/installation.md +0 -16
- data/docs/web/.gitignore +0 -11
- data/docs/web/core/Footer.js +0 -92
- data/docs/web/i18n/en.json +0 -134
- data/docs/web/package.json +0 -14
- data/docs/web/pages/en/help.js +0 -50
- data/docs/web/pages/en/index.js +0 -231
- data/docs/web/pages/en/users.js +0 -47
- data/docs/web/sidebars.json +0 -30
- data/docs/web/siteConfig.js +0 -44
- data/docs/web/static/css/custom.css +0 -229
- data/docs/web/static/fonts/FiraCode-Medium.woff +0 -0
- data/docs/web/static/fonts/FiraCode-Regular.woff +0 -0
- data/docs/web/static/fonts/StemText.woff +0 -0
- data/docs/web/static/fonts/StemTextBold.woff +0 -0
- data/docs/web/static/img/favicon/favicon.ico +0 -0
- data/docs/web/yarn.lock +0 -1741
- data/lib/clowne/adapters/active_record/association.rb +0 -34
- data/lib/clowne/adapters/base/finalize.rb +0 -19
- data/lib/clowne/adapters/base/init_as.rb +0 -21
- data/lib/clowne/adapters/base/nullify.rb +0 -19
- data/lib/clowne/adapters/sequel/association.rb +0 -47
- data/lib/clowne/plan.rb +0 -81
data/docs/include_association.md
CHANGED
@@ -1,7 +1,4 @@
|
|
1
|
-
|
2
|
-
id: include_association
|
3
|
-
title: Include Association
|
4
|
-
---
|
1
|
+
# Include Association
|
5
2
|
|
6
3
|
Use this declaration to clone model's associations:
|
7
4
|
|
@@ -23,10 +20,17 @@ The declaration supports additional arguments:
|
|
23
20
|
include_association name, scope, options
|
24
21
|
```
|
25
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
|
+
|
26
30
|
## Scope
|
27
31
|
|
28
32
|
Scope can be a:
|
29
|
-
- `Symbol` - named scope
|
33
|
+
- `Symbol` - named scope.
|
30
34
|
- `Proc` - custom scope (supports parameters).
|
31
35
|
|
32
36
|
Example:
|
@@ -47,12 +51,12 @@ end
|
|
47
51
|
|
48
52
|
class UserCloner < Clowne::Cloner
|
49
53
|
include_association :accounts, :active
|
50
|
-
include_association :posts, ->(params) { where(state: params[:
|
54
|
+
include_association :posts, ->(params) { where(state: params[:state]) }
|
51
55
|
end
|
52
56
|
|
53
57
|
# Clone only draft posts
|
54
|
-
UserCloner.call(user,
|
55
|
-
# => <#User...
|
58
|
+
UserCloner.call(user, state: :draft).to_record
|
59
|
+
# => <#User id: nil, ... >
|
56
60
|
```
|
57
61
|
|
58
62
|
## Options
|
@@ -93,12 +97,16 @@ class UserCloner < Clowne::Cloner
|
|
93
97
|
# include_association :posts, clone_with: PostSpecialCloner, traits: :with_tags
|
94
98
|
end
|
95
99
|
|
96
|
-
UserCloner.call(user)
|
97
|
-
# => <#User...
|
100
|
+
UserCloner.call(user).to_record
|
101
|
+
# => <#User id: nil, ... >
|
98
102
|
```
|
99
103
|
|
100
104
|
**NOTE**: if custom cloner is not defined, Clowne tries to infer the [implicit cloner](implicit_cloner.md).
|
101
105
|
|
106
|
+
## Nested parameters
|
107
|
+
|
108
|
+
Follow to [documentation page](parameters.md).
|
109
|
+
|
102
110
|
## Include multiple associations
|
103
111
|
|
104
112
|
You can include multiple associations at once too:
|
@@ -117,3 +125,9 @@ end
|
|
117
125
|
```
|
118
126
|
|
119
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
ADDED
@@ -0,0 +1,29 @@
|
|
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
CHANGED
@@ -1,8 +1,4 @@
|
|
1
|
-
|
2
|
-
id: init_as
|
3
|
-
title: Initialize Cloning Target
|
4
|
-
sidebar_label: Init As
|
5
|
-
---
|
1
|
+
# Initialize Cloning Target
|
6
2
|
|
7
3
|
You can override the default Clowne method which generates a _plain_ copy of a source object.
|
8
4
|
By default, Clowne initiates the cloned record using a `#dup` method.
|
@@ -27,10 +23,18 @@ class UserCloner < Clowne::Cloner
|
|
27
23
|
end
|
28
24
|
end
|
29
25
|
|
30
|
-
jack = User.find_by(email:
|
31
|
-
|
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', ...>
|
32
30
|
|
33
|
-
|
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,
|
34
35
|
# without creating a new one
|
35
|
-
UserCloner.call(jack, traits: :copy_settings, target: john)
|
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',...>
|
36
40
|
```
|
@@ -1,12 +1,9 @@
|
|
1
|
-
|
2
|
-
id: inline_configuration
|
3
|
-
title: Inline Configuration
|
4
|
-
---
|
1
|
+
# Inline Configuration
|
5
2
|
|
6
3
|
You can also enhance the cloner configuration inline (i.e., add declarations dynamically):
|
7
4
|
|
8
5
|
```ruby
|
9
|
-
|
6
|
+
operation = UserCloner.call(User.last) do
|
10
7
|
exclude_association :profile
|
11
8
|
|
12
9
|
finalize do |source, record|
|
@@ -14,6 +11,8 @@ cloned = UserCloner.call(User.last) do
|
|
14
11
|
end
|
15
12
|
end
|
16
13
|
|
14
|
+
cloned = operation.to_record
|
15
|
+
|
17
16
|
cloned.email
|
18
17
|
# => "clone_of_john@example.com"
|
19
18
|
|
@@ -31,7 +30,7 @@ Thus it's also possible to clone objects without any cloner classes at all by us
|
|
31
30
|
```ruby
|
32
31
|
cloned = Clowne::Cloner.call(user) do
|
33
32
|
# anything you want!
|
34
|
-
end
|
33
|
+
end.to_record
|
35
34
|
|
36
35
|
cloned
|
37
36
|
# => <#User..
|
data/docs/nullify.md
CHANGED
@@ -1,8 +1,4 @@
|
|
1
|
-
|
2
|
-
id: nullify
|
3
|
-
title: Nullify Attributes
|
4
|
-
sidebar_label: Nullify
|
5
|
-
---
|
1
|
+
# Nullify Attributes
|
6
2
|
|
7
3
|
To set a bunch of attributes to `nil` you can use the `nullify` declaration:
|
8
4
|
|
@@ -21,7 +17,7 @@ class UserCloner < Clowne::Cloner
|
|
21
17
|
end
|
22
18
|
end
|
23
19
|
|
24
|
-
clone = UserCloner.call(user)
|
20
|
+
clone = UserCloner.call(user).to_record
|
25
21
|
clone.name.nil?
|
26
22
|
# => true
|
27
23
|
clone.email.nil?
|
@@ -29,7 +25,7 @@ clone.email.nil?
|
|
29
25
|
clone.surname.nil?
|
30
26
|
# => false
|
31
27
|
|
32
|
-
clone2 = UserCloner.call(user, traits: :nullify_surname)
|
28
|
+
clone2 = UserCloner.call(user, traits: :nullify_surname).to_record
|
33
29
|
clone2.name.nil?
|
34
30
|
# => true
|
35
31
|
clone2.surname.nil?
|
data/docs/operation.md
ADDED
@@ -0,0 +1,55 @@
|
|
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
ADDED
@@ -0,0 +1,112 @@
|
|
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
CHANGED
@@ -1,11 +1,6 @@
|
|
1
|
-
|
2
|
-
id: sequel
|
3
|
-
title: Sequel
|
4
|
-
---
|
1
|
+
# Sequel
|
5
2
|
|
6
|
-
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.
|
7
|
-
|
8
|
-
Also, Sequel target record wrapped into a special class for implementation full Clowne's behavior. You need to use method `to_model` for getting final cloned `Sequel::Model` object (or you can use `save` for saving the cloned object to DB).
|
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.
|
9
4
|
|
10
5
|
Example:
|
11
6
|
|
@@ -29,13 +24,11 @@ and get cloned user
|
|
29
24
|
|
30
25
|
```ruby
|
31
26
|
user = User.last
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
# => User
|
38
|
-
cloned_record.new?
|
27
|
+
operation = UserCloner.call(user)
|
28
|
+
# => <#Clowne::Adapters::Sequel::Operation...>
|
29
|
+
cloned = operation.to_record
|
30
|
+
# => <#User id: nil, ...>
|
31
|
+
cloned.new?
|
39
32
|
# => true
|
40
33
|
```
|
41
34
|
|
@@ -43,14 +36,15 @@ or you can save it immediately
|
|
43
36
|
|
44
37
|
```ruby
|
45
38
|
user = User.last
|
46
|
-
|
47
|
-
|
48
|
-
# => Clowne::Adapters::Sequel::
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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?
|
53
47
|
# => false
|
54
48
|
```
|
55
49
|
|
56
|
-
If you try to clone
|
50
|
+
If you try to clone association without `NestedAttributes` plugin, Clowne will skip this declaration.
|
data/docs/supported_adapters.md
CHANGED
@@ -1,13 +1,10 @@
|
|
1
|
-
|
2
|
-
id: supported_adapters
|
3
|
-
title: Supported Adapters
|
4
|
-
---
|
1
|
+
# Supported Adapters
|
5
2
|
|
6
3
|
Clowne supports the following ORM adapters (and associations):
|
7
4
|
|
8
5
|
Adapter |1:1 | 1:M | M:M |
|
9
6
|
---------------------------------------------------|------------|-------------|-------------------------|
|
10
|
-
[:active_record](
|
11
|
-
[:sequel](
|
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 |
|
12
9
|
|
13
10
|
For more information see the corresponding adapter documentation.
|
data/docs/testing.md
ADDED
@@ -0,0 +1,194 @@
|
|
1
|
+
# Testing
|
2
|
+
|
3
|
+
Clowne provides specific tools to help you test your cloners.
|
4
|
+
|
5
|
+
The main goal is to make it possible to test different cloning phases separately and avoid _heavy_ tests setup phases.
|
6
|
+
|
7
|
+
Let's consider the following models and cloners:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
# app/models/user.rb
|
11
|
+
class User < ApplicationRecord
|
12
|
+
has_one :profile
|
13
|
+
has_many :posts
|
14
|
+
end
|
15
|
+
|
16
|
+
# app/models/post.rb
|
17
|
+
class Post < ApplicationRecord
|
18
|
+
has_many :comments
|
19
|
+
has_many :votes
|
20
|
+
|
21
|
+
scope :draft, -> { where(draft: true) }
|
22
|
+
end
|
23
|
+
|
24
|
+
# app/cloners/user_cloner.rb
|
25
|
+
class UserCloner < Clowne::Cloner
|
26
|
+
class ProfileCloner
|
27
|
+
nullify :rating
|
28
|
+
end
|
29
|
+
|
30
|
+
include_association :profile, clone_with: ProfileCloner
|
31
|
+
|
32
|
+
nullify :email
|
33
|
+
|
34
|
+
finalize do |_, record, name: nil, **|
|
35
|
+
record.name = name unless name.nil?
|
36
|
+
end
|
37
|
+
|
38
|
+
trait :copy do
|
39
|
+
init_as do |user, target:, **|
|
40
|
+
# copy name
|
41
|
+
target.name = user.name
|
42
|
+
target
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
trait :with_posts do
|
47
|
+
include_association :posts, :draft, traits: :mark_as_copy
|
48
|
+
end
|
49
|
+
|
50
|
+
trait :with_popular_posts do
|
51
|
+
include_association :posts, (lambda do |params|
|
52
|
+
where("rating > ?", params[:min_rating])
|
53
|
+
end)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# app/cloners/post_cloner.rb
|
58
|
+
class PostCloner < Clowne::Cloner
|
59
|
+
include_association :comments
|
60
|
+
|
61
|
+
trait :mark_as_copy do |_, record|
|
62
|
+
record.title += " (copy)"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
```
|
66
|
+
|
67
|
+
## Getting started
|
68
|
+
|
69
|
+
Currently, only [RSpec](http://rspec.info/) is supported.
|
70
|
+
|
71
|
+
Add this line to your `spec_helper.rb` (or `rails_helper.rb`):
|
72
|
+
|
73
|
+
```ruby
|
74
|
+
require "clowne/rspec"
|
75
|
+
```
|
76
|
+
|
77
|
+
## Configuration matchers
|
78
|
+
|
79
|
+
There are several matchers that allow you to verify the cloner configuration.
|
80
|
+
|
81
|
+
### `clone_associations`
|
82
|
+
|
83
|
+
This matcher vefifies that your cloner includes the specified associations:
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
# spec/cloners/user_cloner_spec.rb
|
87
|
+
RSpec.describe UserCloner, type: :cloner do
|
88
|
+
subject { described_class }
|
89
|
+
|
90
|
+
specify do
|
91
|
+
# checks that only the specified associations is included
|
92
|
+
is_expected.to clone_associations(:profile)
|
93
|
+
|
94
|
+
# with traits
|
95
|
+
is_expected.to clone_associations(:profile, :posts)
|
96
|
+
.with_traits(:with_posts)
|
97
|
+
|
98
|
+
# raises when there are some unspecified associations
|
99
|
+
is_expected.to clone_associations(:profile)
|
100
|
+
.with_traits(:with_posts)
|
101
|
+
#=> raises RSpec::Expectations::ExpectationNotMetError
|
102
|
+
end
|
103
|
+
end
|
104
|
+
```
|
105
|
+
|
106
|
+
### `clone_association`
|
107
|
+
|
108
|
+
This matcher allows to verify the specified association options:
|
109
|
+
|
110
|
+
```ruby
|
111
|
+
# spec/cloners/user_cloner_spec.rb
|
112
|
+
RSpec.describe UserCloner, type: :cloner do
|
113
|
+
subject { described_class }
|
114
|
+
|
115
|
+
specify do
|
116
|
+
# simply check that association is included
|
117
|
+
is_expected.to clone_association(:profile)
|
118
|
+
|
119
|
+
# check options
|
120
|
+
is_expected.to clone_association(
|
121
|
+
:profile,
|
122
|
+
clone_with: described_class::ProfileCloner
|
123
|
+
)
|
124
|
+
|
125
|
+
# with traits, scope and activated trait
|
126
|
+
is_expected.to clone_association(
|
127
|
+
:posts,
|
128
|
+
traits: :mark_as_copy,
|
129
|
+
scope: :draft
|
130
|
+
).with_traits(:with_posts)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
```
|
134
|
+
|
135
|
+
**NOTE:** `clone_associations`/`clone_association` matchers are only available in groups marked with `type: :cloner` tag.
|
136
|
+
|
137
|
+
Clowne automaticaly marks all specs in `spec/cloners` folder with `type: :cloner`. Otherwise you have to add this tag you.
|
138
|
+
|
139
|
+
|
140
|
+
## Using partial cloning
|
141
|
+
|
142
|
+
Under the hood, Clowne builds a [compilation plan](architecture.md) which is used to clone the record.
|
143
|
+
|
144
|
+
Plan is a set of _actions_ (such as `nullify`, `finalize`, `association`, `init_as`) which are applied to the record.
|
145
|
+
|
146
|
+
Most of the time these actions don't depend on each other, thus we can test them separately:
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
# spec/cloners/user_cloner_spec.rb
|
150
|
+
RSpec.describe UserCloner, type: :cloner do
|
151
|
+
subject(:user) { create :user, name: "Bombon" }
|
152
|
+
|
153
|
+
specify "simple case" do
|
154
|
+
# apply only the specified part of the plan
|
155
|
+
cloned_user = described_class.partial_apply(:nullify, user).to_record
|
156
|
+
expect(cloned_user.email).to be_nil
|
157
|
+
# finalize wasn't applied
|
158
|
+
expect(cloned_user.name).to eq "Bombon"
|
159
|
+
end
|
160
|
+
|
161
|
+
specify "with params" do
|
162
|
+
cloned_user = described_class.partial_apply(:finalize, user, name: "new name").to_record
|
163
|
+
# nullify actions were not applied!
|
164
|
+
expect(cloned_user.email).to eq user.email
|
165
|
+
# finalize was applied
|
166
|
+
expect(cloned_user.name).to eq "new name"
|
167
|
+
end
|
168
|
+
|
169
|
+
specify "with traits" do
|
170
|
+
a_user = create(:user, name: "Dindon")
|
171
|
+
cloned_user = described_class.partial_apply(
|
172
|
+
:init_as, user, traits: :copy, target: a_user
|
173
|
+
).to_record
|
174
|
+
# returned user is the same as target
|
175
|
+
expect(cloned_user).to be_eql(a_user)
|
176
|
+
expect(cloned_user.name).to eq "Bombon"
|
177
|
+
end
|
178
|
+
|
179
|
+
specify "associations" do
|
180
|
+
create(:post, user: user, rating: 1, text: "Boom Boom")
|
181
|
+
create(:post, user: user, rating: 2, text: "Flying Dumplings")
|
182
|
+
|
183
|
+
# you can specify which associations to include (you can use array)
|
184
|
+
# to apply all associations write:
|
185
|
+
# plan.apply(:association)
|
186
|
+
cloned_user = described_class.partial_apply(
|
187
|
+
"association.posts", user, traits: :with_popular_posts, min_rating: 1
|
188
|
+
).to_record
|
189
|
+
|
190
|
+
expect(cloned_user.posts.size).to eq 1
|
191
|
+
expect(cloned_user.posts.first.text).to eq "Flying Dumplings"
|
192
|
+
end
|
193
|
+
end
|
194
|
+
```
|