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
@@ -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>
@@ -0,0 +1,40 @@
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
+ ```
@@ -0,0 +1,37 @@
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
+ ```
@@ -0,0 +1,33 @@
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
+ ```
@@ -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_attributes(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_attributes(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
+ ```
@@ -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
+ ```
@@ -0,0 +1,50 @@
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.
@@ -0,0 +1,10 @@
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.
@@ -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
+ ```