clowne 0.1.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (125) hide show
  1. checksums.yaml +5 -5
  2. data/.rubocop.yml +11 -45
  3. data/.travis.yml +14 -21
  4. data/CHANGELOG.md +30 -0
  5. data/Gemfile +11 -7
  6. data/README.md +38 -18
  7. data/Rakefile +3 -3
  8. data/clowne.gemspec +17 -10
  9. data/docs/.nojekyll +0 -0
  10. data/docs/.rubocop.yml +8 -2
  11. data/docs/CNAME +1 -0
  12. data/docs/README.md +131 -0
  13. data/docs/_sidebar.md +25 -0
  14. data/docs/active_record.md +4 -7
  15. data/docs/after_clone.md +53 -0
  16. data/docs/after_persist.md +77 -0
  17. data/docs/architecture.md +2 -5
  18. data/docs/assets/docsify.min.js +1 -0
  19. data/docs/assets/prism-ruby.min.js +1 -0
  20. data/docs/assets/styles.css +348 -0
  21. data/docs/assets/vue.css +1 -0
  22. data/docs/clone_mapper.md +59 -0
  23. data/docs/customization.md +6 -7
  24. data/docs/exclude_association.md +6 -8
  25. data/docs/finalize.md +11 -15
  26. data/docs/from_v02_to_v1.md +83 -0
  27. data/docs/getting_started.md +171 -0
  28. data/docs/implicit_cloner.md +2 -5
  29. data/docs/include_association.md +24 -10
  30. data/docs/index.html +29 -0
  31. data/docs/init_as.md +13 -9
  32. data/docs/inline_configuration.md +5 -6
  33. data/docs/nullify.md +3 -7
  34. data/docs/operation.md +55 -0
  35. data/docs/parameters.md +112 -0
  36. data/docs/sequel.md +16 -22
  37. data/docs/supported_adapters.md +3 -6
  38. data/docs/testing.md +194 -0
  39. data/docs/traits.md +1 -4
  40. data/gemfiles/activerecord42.gemfile +7 -5
  41. data/gemfiles/jruby.gemfile +8 -6
  42. data/gemfiles/railsmaster.gemfile +8 -6
  43. data/lib/clowne.rb +12 -9
  44. data/lib/clowne/adapters/active_record.rb +4 -5
  45. data/lib/clowne/adapters/active_record/associations.rb +8 -6
  46. data/lib/clowne/adapters/active_record/associations/base.rb +1 -5
  47. data/lib/clowne/adapters/active_record/associations/belongs_to.rb +29 -0
  48. data/lib/clowne/adapters/active_record/associations/has_one.rb +3 -2
  49. data/lib/clowne/adapters/active_record/dsl.rb +2 -2
  50. data/lib/clowne/adapters/active_record/resolvers/association.rb +38 -0
  51. data/lib/clowne/adapters/base.rb +49 -44
  52. data/lib/clowne/adapters/base/association.rb +24 -15
  53. data/lib/clowne/adapters/registry.rb +49 -0
  54. data/lib/clowne/adapters/sequel.rb +14 -10
  55. data/lib/clowne/adapters/sequel/associations.rb +6 -6
  56. data/lib/clowne/adapters/sequel/associations/base.rb +9 -5
  57. data/lib/clowne/adapters/sequel/associations/many_to_many.rb +6 -2
  58. data/lib/clowne/adapters/sequel/associations/one_to_many.rb +7 -2
  59. data/lib/clowne/adapters/sequel/associations/one_to_one.rb +7 -2
  60. data/lib/clowne/adapters/sequel/operation.rb +35 -0
  61. data/lib/clowne/adapters/sequel/record_wrapper.rb +0 -16
  62. data/lib/clowne/adapters/sequel/resolvers/after_persist.rb +22 -0
  63. data/lib/clowne/adapters/sequel/resolvers/association.rb +51 -0
  64. data/lib/clowne/adapters/sequel/specifications/after_persist_does_not_support.rb +15 -0
  65. data/lib/clowne/cloner.rb +49 -21
  66. data/lib/clowne/declarations.rb +15 -12
  67. data/lib/clowne/declarations/after_clone.rb +21 -0
  68. data/lib/clowne/declarations/after_persist.rb +21 -0
  69. data/lib/clowne/declarations/base.rb +13 -0
  70. data/lib/clowne/declarations/exclude_association.rb +2 -2
  71. data/lib/clowne/declarations/finalize.rb +5 -4
  72. data/lib/clowne/declarations/include_association.rb +16 -2
  73. data/lib/clowne/declarations/init_as.rb +5 -4
  74. data/lib/clowne/declarations/nullify.rb +4 -3
  75. data/lib/clowne/declarations/trait.rb +1 -0
  76. data/lib/clowne/dsl.rb +9 -0
  77. data/lib/clowne/ext/lambda_as_proc.rb +17 -0
  78. data/lib/clowne/ext/orm_ext.rb +1 -1
  79. data/lib/clowne/ext/record_key.rb +12 -0
  80. data/lib/clowne/ext/string_constantize.rb +9 -3
  81. data/lib/clowne/ext/yield_self_then.rb +25 -0
  82. data/lib/clowne/planner.rb +16 -3
  83. data/lib/clowne/resolvers/after_clone.rb +17 -0
  84. data/lib/clowne/resolvers/after_persist.rb +18 -0
  85. data/lib/clowne/resolvers/finalize.rb +12 -0
  86. data/lib/clowne/resolvers/init_as.rb +13 -0
  87. data/lib/clowne/resolvers/nullify.rb +15 -0
  88. data/lib/clowne/rspec.rb +5 -0
  89. data/lib/clowne/rspec/clone_association.rb +99 -0
  90. data/lib/clowne/rspec/clone_associations.rb +26 -0
  91. data/lib/clowne/rspec/helpers.rb +35 -0
  92. data/lib/clowne/utils/clone_mapper.rb +26 -0
  93. data/lib/clowne/utils/operation.rb +95 -0
  94. data/lib/clowne/utils/options.rb +39 -0
  95. data/lib/clowne/utils/params.rb +64 -0
  96. data/lib/clowne/utils/plan.rb +90 -0
  97. data/lib/clowne/version.rb +1 -1
  98. metadata +99 -45
  99. data/docs/alternatives.md +0 -26
  100. data/docs/basic_example.md +0 -66
  101. data/docs/configuration.md +0 -29
  102. data/docs/execution_order.md +0 -14
  103. data/docs/installation.md +0 -16
  104. data/docs/web/.gitignore +0 -11
  105. data/docs/web/core/Footer.js +0 -92
  106. data/docs/web/i18n/en.json +0 -134
  107. data/docs/web/package.json +0 -14
  108. data/docs/web/pages/en/help.js +0 -50
  109. data/docs/web/pages/en/index.js +0 -231
  110. data/docs/web/pages/en/users.js +0 -47
  111. data/docs/web/sidebars.json +0 -30
  112. data/docs/web/siteConfig.js +0 -44
  113. data/docs/web/static/css/custom.css +0 -229
  114. data/docs/web/static/fonts/FiraCode-Medium.woff +0 -0
  115. data/docs/web/static/fonts/FiraCode-Regular.woff +0 -0
  116. data/docs/web/static/fonts/StemText.woff +0 -0
  117. data/docs/web/static/fonts/StemTextBold.woff +0 -0
  118. data/docs/web/static/img/favicon/favicon.ico +0 -0
  119. data/docs/web/yarn.lock +0 -1741
  120. data/lib/clowne/adapters/active_record/association.rb +0 -34
  121. data/lib/clowne/adapters/base/finalize.rb +0 -19
  122. data/lib/clowne/adapters/base/init_as.rb +0 -21
  123. data/lib/clowne/adapters/base/nullify.rb +0 -19
  124. data/lib/clowne/adapters/sequel/association.rb +0 -47
  125. data/lib/clowne/plan.rb +0 -81
@@ -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[:status]) if params[:status] }
54
+ include_association :posts, ->(params) { where(state: params[:state]) }
51
55
  end
52
56
 
53
57
  # Clone only draft posts
54
- UserCloner.call(user, status: :draft)
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: 'jack@evl.ms')
31
- john = User.find_by(email: 'john@evl.ms')
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
- # we want to clone Jack's profile settings to another user,
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
- cloned = UserCloner.call(User.last) do
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
+ ```
@@ -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
- wrapper = UserCloner.call(user)
33
- wrapper.class
34
- # => Clowne::Adapters::Sequel::RecordWrapper
35
- cloned_record = wrapper.to_model
36
- cloned_record.class
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
- wrapper = UserCloner.call(user)
47
- wrapper.class
48
- # => Clowne::Adapters::Sequel::RecordWrapper
49
- cloned_record = wrapper.save
50
- cloned_record.class
51
- # => User
52
- cloned_record.new?
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 associations without `NestedAttributes` plugin, Clowne will skip this declaration.
50
+ If you try to clone association without `NestedAttributes` plugin, Clowne will skip this declaration.
@@ -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](/clowne/docs/active_record.html) | has_one | has_many | has_and_belongs_to_many |
11
- [:sequel](/clowne/docs/sequel.html) | one_to_one | one_to_many | many_to_many |
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
+ ```