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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: b833f076ba69c9b626d4038e977e4a40d8de8dbb
4
- data.tar.gz: 271303497d931c2773c8c4f948baacfa9819cd4a
2
+ SHA256:
3
+ metadata.gz: '0815485e47677a65023e13711ad70570b79c97057ad8d09bed83f45aa293f939'
4
+ data.tar.gz: d1f8a39135462f1bca941515d055f19f44cdf92c0152ea0c49d8dd3f39475fb7
5
5
  SHA512:
6
- metadata.gz: f4df02c8872ecdc0b1f70fd74a12549065aaf85cd6922b2648043905f3eba9d4bf415716a11b5f0a8f458d7c59d11edbe590e2e1c92f3328887ab9da38c7de1b
7
- data.tar.gz: '0683a5756b9797ac8c2a9cd60a4adbd29c214b9dbd55ef3fd736e2e05b414a7b0d92493edcde7e9a109434ceb67ca557cc55953ec08f1caad2a2c35e13ac1439'
6
+ metadata.gz: 809ecabf16dc071eda96f03853baaa42b5736804864e417e2d9a03ee3d37425b7e01c649cee2a2b8728813ffb09a3a4fb61a59e53ebdbf5f3da0b0a435a91807
7
+ data.tar.gz: 9c186a3f86f85f2a08eaea4612329a86f104328c99aa561f82a9b42f9b69f50b159f15b1426aec68585aca6f24c937b0967876689b84a0d04a64ac041169c175
@@ -0,0 +1,7 @@
1
+ version: "2"
2
+ checks:
3
+ method-complexity:
4
+ enabled: false
5
+ exclude_patterns:
6
+ - "docs/"
7
+ - "spec/"
@@ -0,0 +1 @@
1
+ docs/**/* linguist-vendored
data/.gitignore CHANGED
@@ -13,3 +13,4 @@ rubocop.html
13
13
  *.gem
14
14
  gemfiles/*.lock
15
15
  Gemfile.local
16
+ /docs/web/build/
@@ -1,45 +1,28 @@
1
+ require:
2
+ # add after moving docs to another tool
3
+ - 'standard/cop/semantic_blocks'
4
+ - 'rubocop-md'
5
+
6
+ inherit_gem:
7
+ standard: config/base.yml
8
+
1
9
  AllCops:
2
10
  Exclude:
3
11
  - 'bin/**/*'
4
12
  - 'tmp/**/*'
5
13
  - 'vendor/**/*'
6
14
  - 'gemfiles/vendor/**/*'
15
+ - 'clowne.gemspec'
7
16
  DisplayCopNames: true
8
- StyleGuideCopsOnly: false
9
- TargetRubyVersion: 2.3
10
-
11
- Rails:
12
- Enabled: false
13
-
14
- Naming/AccessorMethodName:
15
- Enabled: false
16
-
17
- Style/TrivialAccessors:
18
- Enabled: false
17
+ TargetRubyVersion: 2.5
19
18
 
20
- Metrics/LineLength:
21
- Max: 100
19
+ Markdown:
20
+ WarnInvalid: true
22
21
 
23
- Style/Documentation:
24
- Exclude:
25
- - 'spec/**/*.rb'
26
-
27
- Style/SymbolArray:
22
+ Standard/SemanticBlocks:
28
23
  Enabled: false
29
24
 
30
- Style/FrozenStringLiteralComment:
31
- Exclude:
32
- - 'spec/**/*.rb'
33
- - 'Gemfile'
34
- - 'Rakefile'
35
- - '*.gemspec'
36
-
37
- Metrics/BlockLength:
25
+ Lint/Void:
38
26
  Exclude:
39
- - 'spec/**/*.rb'
40
-
41
- Bundler/OrderedGems:
42
- Enabled: false
43
-
44
- Gemspec/OrderedDependencies:
45
- Enabled: false
27
+ - 'docs/README.md'
28
+ - 'README.md'
@@ -1,9 +1,13 @@
1
1
  sudo: false
2
2
  language: ruby
3
+ cache: bundler
3
4
 
4
5
  notifications:
5
6
  email: false
6
7
 
8
+ before_install:
9
+ - gem install bundler
10
+
7
11
  before_script:
8
12
  # Only generate coverage report for the specified job
9
13
  - if [ "$CC_REPORT" == "true" ]; then curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter; fi
@@ -11,7 +15,6 @@ before_script:
11
15
  - if [ "$CC_REPORT" == "true" ]; then ./cc-test-reporter before-build; fi
12
16
  script:
13
17
  - bundle exec rake
14
- after_script:
15
18
  - if [ "$CC_REPORT" == "true" ]; then ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT; fi
16
19
 
17
20
  matrix:
@@ -19,21 +22,22 @@ matrix:
19
22
  include:
20
23
  - rvm: ruby-head
21
24
  gemfile: gemfiles/railsmaster.gemfile
22
- - rvm: jruby-9.1.0.0
25
+ - rvm: jruby-9.2.8.0
23
26
  gemfile: gemfiles/jruby.gemfile
24
- - rvm: 2.5.0
27
+ - rvm: 2.7
25
28
  gemfile: Gemfile
26
- - rvm: 2.4.3
29
+ - rvm: 2.6.5
27
30
  gemfile: Gemfile
28
- env: CC_REPORT=true
29
- - rvm: 2.4.1
30
- gemfile: gemfiles/activerecord42.gemfile
31
- - rvm: 2.3.1
31
+ - rvm: 2.5.7
32
32
  gemfile: Gemfile
33
- - rvm: 2.2.0
33
+ - rvm: 2.4.9
34
34
  gemfile: gemfiles/activerecord42.gemfile
35
+ - rvm: truffleruby-head
36
+ gemfile: Gemfile
35
37
  allow_failures:
36
38
  - rvm: ruby-head
37
39
  gemfile: gemfiles/railsmaster.gemfile
38
- - rvm: jruby-9.1.0.0
40
+ - rvm: jruby-9.2.8.0
39
41
  gemfile: gemfiles/jruby.gemfile
42
+ - rvm: truffleruby-head
43
+ gemfile: Gemfile
@@ -1,9 +1,46 @@
1
1
  # Change log
2
2
 
3
- ## master branch
3
+ ## 1.1.0 (2019-03-20)
4
+
5
+
6
+ - Add `after_clone` declaration. ([@elardo][])
7
+ - Add opporotunity to include belongs_to association for active_record adapter. ([@madding][])
8
+
9
+ ## 1.0.0 (2019-02-26)
10
+
11
+ - Return `Operation` instance as a rusult of cloning. ([@ssnickolay][])
12
+
13
+ See [migration guide](https://clowne.evilmartians.io/docs/from_v02_to_v10.html)
14
+
15
+ - Add `after_persist` declaration. ([@ssnickolay][], [@palkan][])
16
+
17
+ - Unify interface between adapters. ([@ssnickolay][])
18
+
19
+ - Deprecate `Operation#save` and `Operation#save!` methods. ([@ssnickolay][])
20
+
21
+ - Improve Docs ([@ssnickolay][], [@palkan][])
22
+
23
+ ## 0.2.0 (2018-02-21)
24
+
25
+ - Add `Cloner#partial_apply` method. ([@palkan][])
26
+
27
+ - Add RSpec matchers `clone_association` / `clone_associations`. ([@palkan][])
28
+
29
+ - [[#15](https://github.com/palkan/clowne/issues/15)] Add control over nested params. ([@ssnickolay][])
30
+
31
+ ## 0.1.0 (2018-02-01)
32
+
33
+ - Add `init_as` declaration. ([@palkan][])
34
+
35
+ - Support [Sequel](https://github.com/jeremyevans/sequel). ([@ssnickolay][])
36
+
37
+ - Support passing a block to `#clowne` for inline configuration. ([@palkan][])
38
+
39
+ ## 0.1.0.beta1 (2018-01-08)
4
40
 
5
41
  - Initial version. ([@ssnickolay][], [@palkan][])
6
42
 
7
43
  [@palkan]: https://github.com/palkan
8
44
  [@ssnickolay]: https://github.com/ssnickolay
9
-
45
+ [@elardo]: https://github.com/elardo
46
+ [@madding]: https://github.com/madding
data/Gemfile CHANGED
@@ -1,14 +1,19 @@
1
- source 'https://rubygems.org'
1
+ source "https://rubygems.org"
2
2
 
3
3
  # Specify your gem's dependencies in clowne.gemspec
4
4
  gemspec
5
5
 
6
- gem 'pry-byebug'
7
- gem 'sqlite3'
8
- gem 'activerecord', '>= 5.0'
9
- gem 'simplecov'
6
+ gem "pry-byebug", platform: :mri
10
7
 
11
- local_gemfile = 'Gemfile.local'
8
+ gem "sqlite3", "~> 1.4.1", platform: :ruby
9
+ gem "activerecord-jdbcsqlite3-adapter", "~> 50.0", platform: :jruby
10
+ gem "jdbc-sqlite3", platform: :jruby
11
+
12
+ gem "activerecord", "~> 5.2"
13
+ gem "sequel", ">= 5.0"
14
+ gem "simplecov"
15
+
16
+ local_gemfile = "Gemfile.local"
12
17
 
13
18
  if File.exist?(local_gemfile)
14
19
  eval(File.read(local_gemfile)) # rubocop:disable Security/Eval
data/README.md CHANGED
@@ -1,26 +1,20 @@
1
1
  [![Gem Version](https://badge.fury.io/rb/clowne.svg)](https://badge.fury.io/rb/clowne)
2
- [![Build Status](https://travis-ci.org/palkan/clowne.svg?branch=master)](https://travis-ci.org/palkan/clowne)
3
- [![Code Climate](https://codeclimate.com/github/palkan/clowne.svg)](https://codeclimate.com/github/palkan/clowne)
4
- [![Test Coverage](https://codeclimate.com/github/palkan/clowne/badges/coverage.svg)](https://codeclimate.com/github/palkan/clowne/coverage)
2
+ [![Build Status](https://travis-ci.org/clowne-rb/clowne.svg?branch=master)](https://travis-ci.org/clowne-rb/clowne)
3
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/9143c4f91e9d1d2a4bd1/test_coverage)](https://codeclimate.com/github/clowne-rb/clowne/test_coverage)
4
+ [![Maintainability](https://api.codeclimate.com/v1/badges/9143c4f91e9d1d2a4bd1/maintainability)](https://codeclimate.com/github/clowne-rb/clowne/maintainability)
5
+ [![Docs](https://img.shields.io/badge/docs-link-brightgreen.svg)](https://clowne.evilmartians.io)
5
6
 
6
7
  # Clowne
7
8
 
8
- **NOTICE**: gem is currently under heavy development, we plan to release the first version 'till the end of the year.
9
+ A flexible gem for cloning your models. Clowne focuses on ease of use and provides the ability to connect various ORM adapters.
9
10
 
10
- A flexible gem for cloning your models. Clowne focuses on ease of use and provides the ability to connect various ORM adapters (currently only ActiveRecord is supported).
11
+ 📖 Read [Evil Martians Chronicles](https://evilmartians.com/chronicles/clowne-clone-ruby-models-with-a-smile) to learn about possible use cases.
12
+
13
+ 📑 [Documentation](https://clowne.evilmartians.io)
11
14
 
12
15
  <a href="https://evilmartians.com/">
13
16
  <img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54"></a>
14
17
 
15
- ### Alternatives
16
-
17
- Why did we decide to build our own cloning gem?
18
-
19
- First, existing solutions turned out not stable and flexible enough for us.
20
-
21
- Secondly, they are Rails-only. And we are not.
22
-
23
- Nevertheless, thanks to [amoeba](https://github.com/amoeba-rb/amoeba) and [deep_cloneable](https://github.com/moiristo/deep_cloneable) for inspiration.
24
18
 
25
19
  ## Installation
26
20
 
@@ -33,14 +27,12 @@ gem install clowne
33
27
  Or add this line to your application's Gemfile:
34
28
 
35
29
  ```ruby
36
- gem 'clowne'
30
+ gem "clowne"
37
31
  ```
38
32
 
39
33
  ## Quick Start
40
34
 
41
- This is a basic example that demonstrates how to clone your ActiveRecord model. For detailed documentation see [Features](#features).
42
-
43
- At first, define your cloneable model
35
+ Assume that you have the following model:
44
36
 
45
37
  ```ruby
46
38
  class User < ActiveRecord::Base
@@ -53,9 +45,19 @@ class User < ActiveRecord::Base
53
45
  has_one :profile
54
46
  has_many :posts
55
47
  end
48
+
49
+ class Profile < ActiveRecord::Base
50
+ # create_table :profiles do |t|
51
+ # t.string :name
52
+ # end
53
+ end
54
+
55
+ class Post < ActiveRecord::Base
56
+ # create_table :posts
57
+ end
56
58
  ```
57
59
 
58
- The next step is to declare cloner
60
+ Let's declare our cloners first:
59
61
 
60
62
  ```ruby
61
63
  class UserCloner < Clowne::Cloner
@@ -65,8 +67,8 @@ class UserCloner < Clowne::Cloner
65
67
  include_association :posts
66
68
 
67
69
  nullify :login
68
-
69
- # params here is an arbitrary hash passed into cloner
70
+
71
+ # params here is an arbitrary Hash passed into cloner
70
72
  finalize do |_source, record, params|
71
73
  record.email = params[:email]
72
74
  end
@@ -79,388 +81,50 @@ class SpecialProfileCloner < Clowne::Cloner
79
81
  end
80
82
  ```
81
83
 
82
- and call it
83
-
84
- ```ruby
85
- clone = UserCloner.call(User.last, { email: "fake@example.com" })
86
- clone.persisted?
87
- # => false
88
- clone.save!
89
- clone.login
90
- # => nil
91
- clone.email
92
- # => "fake@example.com"
93
-
94
- # associations:
95
- clone.posts.count == User.last.posts.count
96
- # => true
97
- clone.profile.name
98
- # => nil
99
- ```
100
-
101
- ## <a name="features">Features
102
-
103
- - [Configuration](#configuration)
104
- - [Include one association](#include_association)
105
- - - [Scope](#include_association_scope)
106
- - - [Options](#include_association_options)
107
- - [Exclude association](#exclude_association)
108
- - [Nullify attribute(s)](#nullify)
109
- - [Execute finalize block](#finalize)
110
- - [Traits](#traits)
111
- - [Execution order](#execution_order)
112
- - [Customization](#customization)
113
-
114
- ### <a name="configuration"></a>Configuration
115
-
116
- You can configure the default adapter for cloners:
117
-
118
- ```ruby
119
- # somewhere in initializers
120
- Clowne.default_adapter = :active_record
121
- ```
122
-
123
- ### <a name="include_association"></a>Include one association
124
-
125
- Powerful declaration for including model's association.
126
-
127
- ```ruby
128
- class User < ActiveRecord::Base
129
- has_one :profile
130
- end
131
-
132
- class UserCloner < Clowne::Cloner
133
- adapter Clowne::ActiveRecord::Adapter
134
-
135
- include_association :profile
136
- end
137
- ```
138
-
139
- But it's not all! :) The DSL looks like
140
-
141
- ```ruby
142
- include_association name, scope, options
143
- ```
144
-
145
- #### <a name="include_association_scope"></a>Include one association: Scope
146
- Scope can be a:
147
-
148
- `Symbol` - named scope.
149
-
150
- `Proc` - custom scope (supports parameter passing).
151
-
152
- Example:
153
-
154
- ```ruby
155
- class User < ActiveRecord::Base
156
- has_many :accounts
157
- has_many :posts
158
- end
159
-
160
- class Account < ActiveRecord::Base
161
- scope :active, -> where(active: true)
162
- end
163
-
164
- class Post < ActiveRecord::Base
165
- # t.string :status
166
- end
167
-
168
- class UserCloner < Clowne::Cloner
169
- adapter Clowne::ActiveRecord::Adapter
170
-
171
- include_association :accounts, :active
172
- include_association :posts, ->(params) { where(state: params[:post_status] }
173
- end
174
-
175
- # posts will be cloned only with draft status
176
- UserCloner.call(user, { post_status: :draft })
177
- # => <#User...
178
- ```
179
-
180
- #### <a name="include_association_options"></a>Include one association: Options
181
-
182
- Options keys can be a:
183
-
184
- `:clone_with` - use custom cloner for all children.
185
-
186
- `:traits` - define special traits.
187
-
188
- Example:
189
-
190
- ```ruby
191
- class User < ActiveRecord::Base
192
- has_many :posts
193
- end
194
-
195
- class Post < ActiveRecord::Base
196
- # t.string :title
197
- has_many :tags
198
- end
199
- ```
84
+ Now you can use `UserCloner` to clone existing records:
200
85
 
201
86
  ```ruby
202
- class PostSpecialCloner < Clowne::Cloner
203
- adapter :active_record
204
-
205
- nullify :title
206
-
207
- trait :with_tags do
208
- include_association :tags
209
- end
210
- end
211
-
212
- class UserCloner < Clowne::Cloner
213
- adapter :active_record
214
-
215
- include_association :posts, clone_with: PostSpecialCloner
216
- # or clone user's posts with tags!
217
- # include_association :posts, clone_with: PostSpecialCloner, traits: :with_tags
218
- end
219
-
220
- UserCloner.call(user)
221
- # => <#User...
222
- ```
223
-
224
- **Notice: if custom cloner is not defined, clowne tries to find default cloner and use it. (PostCloner for previous example)**
87
+ user = User.last
88
+ # => <#User id: 1, login: 'clown', email: 'clown@circus.example.com'>
225
89
 
226
- ### <a name="exclude_association"></a>Exclude association
90
+ operation = UserCloner.call(user, email: "fake@example.com")
91
+ # => <#Clowne::Utils::Operation...>
227
92
 
228
- Exclude association from copying
93
+ operation.to_record
94
+ # => <#User id: nil, login: nil, email: 'fake@example.com'>
229
95
 
230
- ```ruby
231
- class UserCloner < Clowne::Cloner
232
- adapter Clowne::ActiveRecord::Adapter
233
-
234
- include_association :posts
235
-
236
- trait :without_posts do
237
- exclude_association :posts
238
- end
239
- end
240
-
241
- # copy user and posts
242
- clone = UserCloner.call(user)
243
- clone.posts.count == user.posts.count
96
+ operation.persist!
244
97
  # => true
245
98
 
246
- # copy only user
247
- clone2 = UserCloner.call(user, traits: :without_posts)
248
- clone2.posts
249
- # => []
250
- ```
251
-
252
- **NOTE**: once excluded association cannot be re-included, e.g. the following cloner:
253
-
254
- ```ruby
255
- class UserCloner < Clowne::Cloner
256
- exclude_association :comments
257
-
258
- trait :with_comments do
259
- # That wouldn't work
260
- include_association :comments
261
- end
262
- end
263
-
264
- clone = UserCloner.call(user, traits: :with_comments)
265
- clone.comments.empty? #=> true
266
- ```
267
-
268
- Why so? That allows to have deterministic cloning plans when combining multiple traits
269
- (or inheriting cloners).
270
-
271
- ### <a name="nullify"></a>Nullify attribute(s)
272
-
273
- Nullify attributes:
274
-
275
- ```ruby
276
- class User < ActiveRecord::Base
277
- # t.string :name
278
- # t.string :surename
279
- # t.string :email
280
- end
281
-
282
- class UserCloner < Clowne::Cloner
283
- adapter Clowne::ActiveRecord::Adapter
284
-
285
- nullify :name, :email
286
-
287
- trait :nullify_surename do
288
- nullify :surename
289
- end
290
- end
99
+ cloned = operation.to_record
100
+ # => <#User id: 2, login: nil, email: 'fake@example.com'>
291
101
 
292
- # nullify only name
293
- clone = UserCloner.call(user)
294
- clone.name.nil?
295
- # => true
296
- clone.email.nil?
297
- # => true
298
- clone.surename.nil?
299
- # => false
102
+ cloned.login
103
+ # => nil
104
+ cloned.email
105
+ # => "fake@example.com"
300
106
 
301
- # nullify name and surename
302
- clone2 = UserCloner.call(user, traits: :nullify_surename)
303
- clone.name.nil?
304
- # => true
305
- clone.surename.nil?
107
+ # associations:
108
+ cloned.posts.count == user.posts.count
306
109
  # => true
110
+ cloned.profile.name
111
+ # => nil
307
112
  ```
308
113
 
309
- ### <a name="finalize"></a>Execute finalize block
310
-
311
- Simple callback for changing record manually.
312
-
313
- ```ruby
314
- class UserCloner < Clowne::Cloner
315
- adapter Clowne::ActiveRecord::Adapter
316
-
317
- finalize do |source, record, params|
318
- record.name = 'This is copy!'
319
- end
320
-
321
- trait :change_email do
322
- finalize do |source, record, params|
323
- record.email = params[:email]
324
- end
325
- end
326
- end
327
-
328
- # execute first finalize
329
- clone = UserCloner.call(user)
330
- clone.name
331
- # => 'This is copy!'
332
- clone.email == 'clone@example.com'
333
- # => false
334
-
335
- # execute both finalizes
336
- clone2 = UserCloner.call(user, traits: :change_email)
337
- clone.name
338
- # => 'This is copy!'
339
- clone.email
340
- # => 'clone@example.com'
341
- ```
342
-
343
- ### <a name="traits"></a>Traits
344
-
345
- Traits allow you to group cloner declarations together and then apply them (like in factory_bot).
346
-
347
- ```ruby
348
- class UserCloner < Clowne::Cloner
349
- adapter Clowne::ActiveRecord::Adapter
350
-
351
- trait :with_posts do
352
- include_association :posts
353
- end
354
-
355
- trait :with_profile do
356
- include_association :profile
357
- end
358
-
359
- trait :nullify_name do
360
- nullify :name
361
- end
362
- end
363
-
364
- # execute first finalize
365
- UserCloner.call(user, traits: [:with_posts, :with_profile, :nullify_name])
366
- # or
367
- UserCloner.call(user, traits: :nullify_name)
368
- # or
369
- # ...
370
- ```
371
-
372
- ### <a name="execution_order"></a>Execution order
373
-
374
- The order of cloning actions depends on the adapter.
375
-
376
- For ActiveRecord:
377
- - clone associations
378
- - nullify attributes
379
- - run `finalize` blocks
380
- The order of `finalize` blocks is the order they've been written.
381
-
382
- ### <a name="customization"></a>Customization
383
-
384
- Clowne is built with extensibility in mind. You can create your own DSL commands and resolvers.
385
-
386
- Let's consider an example.
387
-
388
- Suppose that you want to add `include_all` declaration to automagically include all associations (for ActiveRecord).
389
-
390
- First, you should add a custom declaration:
391
-
392
- ```ruby
393
- class IncludeAll # :nodoc: all
394
- def compile(plan)
395
- # Just add all_associations object to plan
396
- plan.set(:all_associations, self)
397
- # Plan supports 3 types of registers:
398
- #
399
- # 1) Scalar
400
- #
401
- # plan.set(key, value)
402
- # plan.remove(key)
403
- #
404
- # 2) Append-only lists
405
- #
406
- # plan.add(key, value)
407
- #
408
- # 3) Two-phase set (2P-Set) (see below)
409
- #
410
- # plan.add_to(type, key, value)
411
- # plan.remove_from(type, key)
412
- end
413
- end
414
-
415
- # Register our declrations, i.e. extend DSL
416
- Clowne::Declarations.add :include_all, Clowne::Declarations::IncludeAll
417
- ```
418
-
419
- \* Operations over [2P-Set](https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type#2P-Set_(Two-Phase_Set)) (adding/removing) do not depend on the order of execution; we use "remove-wins" semantics, i.e. when a key has been removed, it cannot be re-added.
420
-
421
- Secondly, register a resolver:
422
-
423
- ```ruby
424
- class AllAssociations
425
- # This method is called when all_associations command is applied.
426
- #
427
- # source – source record
428
- # record – target record (our clone)
429
- # declaration – declaration object
430
- # params – custom params passed to cloner
431
- def call(source, record, declaration, params:)
432
- source.class.reflections.each do |name, reflection|
433
- # Exclude belongs_to associations
434
- next if reflection.macro == :belongs_to
435
- # Resolve and apply association cloner
436
- cloner_class = Clowne::Adapters::ActiveRecord::Associations.cloner_for(reflection)
437
- cloner_class.new(reflection, source, declaration, params).call(record)
438
- end
439
- record
440
- end
441
- end
442
-
443
- # Finally, register the resolver
444
- Clowne::Adapters::ActiveRecord.register_resolver(
445
- :all_associations, AllAssociations
446
- )
447
- ```
448
-
449
- Now you can use it likes this:
114
+ Take a look at our [documentation](https://clowne.evilmartians.io) for more info!
450
115
 
451
- ```ruby
452
- class UserCloner < Clowne::Cloner
453
- adapter :active_record
116
+ ### Supported ORM adapters
454
117
 
455
- include_all
456
- end
457
- ```
118
+ Adapter |1:1 |*:1 | 1:M | M:M |
119
+ ------------------------------------------|------------|------------|-------------|-------------------------|
120
+ [Active Record](https://clowne.evilmartians.io/#/active_record) | has_one | belongs_to | has_many | has_and_belongs_to|
121
+ [Sequel](https://clowne.evilmartians.io/#/sequel) | one_to_one | - | one_to_many | many_to_many |
458
122
 
459
123
  ## Maintainers
460
124
 
461
125
  - [Vladimir Dementyev](https://github.com/palkan)
462
126
 
463
- - [Sverchkov Nikolay](https://github.com/ssnickolay)
127
+ - [Nikolay Sverchkov](https://github.com/ssnickolay)
464
128
 
465
129
  ## License
466
130