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.
- checksums.yaml +5 -5
- data/.codeclimate.yml +7 -0
- data/.gitattributes +1 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +16 -33
- data/.travis.yml +14 -10
- data/CHANGELOG.md +39 -2
- data/Gemfile +11 -6
- data/README.md +48 -384
- data/Rakefile +3 -3
- data/clowne.gemspec +16 -8
- data/docs/.nojekyll +0 -0
- data/docs/.rubocop.yml +18 -0
- data/docs/CNAME +1 -0
- data/docs/README.md +131 -0
- data/docs/_sidebar.md +25 -0
- data/docs/active_record.md +33 -0
- data/docs/after_clone.md +53 -0
- data/docs/after_persist.md +77 -0
- data/docs/architecture.md +138 -0
- 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 +63 -0
- data/docs/exclude_association.md +61 -0
- data/docs/finalize.md +31 -0
- data/docs/from_v02_to_v1.md +83 -0
- data/docs/getting_started.md +171 -0
- data/docs/implicit_cloner.md +33 -0
- data/docs/include_association.md +133 -0
- data/docs/index.html +29 -0
- data/docs/init_as.md +40 -0
- data/docs/inline_configuration.md +37 -0
- data/docs/nullify.md +33 -0
- data/docs/operation.md +55 -0
- data/docs/parameters.md +112 -0
- data/docs/sequel.md +50 -0
- data/docs/supported_adapters.md +10 -0
- data/docs/testing.md +194 -0
- data/docs/traits.md +25 -0
- data/gemfiles/activerecord42.gemfile +7 -4
- data/gemfiles/jruby.gemfile +8 -4
- data/gemfiles/railsmaster.gemfile +8 -5
- data/lib/clowne.rb +12 -7
- data/lib/clowne/adapters/active_record.rb +6 -16
- data/lib/clowne/adapters/active_record/associations.rb +8 -6
- data/lib/clowne/adapters/active_record/associations/base.rb +5 -49
- data/lib/clowne/adapters/active_record/associations/belongs_to.rb +29 -0
- data/lib/clowne/adapters/active_record/associations/has_one.rb +9 -1
- data/lib/clowne/adapters/active_record/associations/noop.rb +4 -1
- data/lib/clowne/adapters/active_record/dsl.rb +33 -0
- data/lib/clowne/adapters/active_record/resolvers/association.rb +38 -0
- data/lib/clowne/adapters/base.rb +53 -41
- data/lib/clowne/adapters/base/association.rb +78 -0
- data/lib/clowne/adapters/registry.rb +54 -11
- data/lib/clowne/adapters/sequel.rb +29 -0
- data/lib/clowne/adapters/sequel/associations.rb +26 -0
- data/lib/clowne/adapters/sequel/associations/base.rb +27 -0
- data/lib/clowne/adapters/sequel/associations/many_to_many.rb +23 -0
- data/lib/clowne/adapters/sequel/associations/noop.rb +16 -0
- data/lib/clowne/adapters/sequel/associations/one_to_many.rb +28 -0
- data/lib/clowne/adapters/sequel/associations/one_to_one.rb +28 -0
- data/lib/clowne/adapters/sequel/copier.rb +23 -0
- data/lib/clowne/adapters/sequel/operation.rb +35 -0
- data/lib/clowne/adapters/sequel/record_wrapper.rb +43 -0
- 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 +50 -20
- data/lib/clowne/declarations.rb +15 -11
- 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 +1 -6
- data/lib/clowne/declarations/finalize.rb +3 -2
- data/lib/clowne/declarations/include_association.rb +16 -4
- data/lib/clowne/declarations/init_as.rb +21 -0
- data/lib/clowne/declarations/nullify.rb +3 -2
- data/lib/clowne/declarations/trait.rb +3 -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 +21 -0
- data/lib/clowne/ext/record_key.rb +12 -0
- data/lib/clowne/ext/string_constantize.rb +10 -4
- data/lib/clowne/ext/yield_self_then.rb +25 -0
- data/lib/clowne/planner.rb +27 -7
- 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 +140 -20
- data/lib/clowne/adapters/active_record/association.rb +0 -34
- data/lib/clowne/adapters/base/finalize.rb +0 -19
- data/lib/clowne/adapters/base/nullify.rb +0 -19
- data/lib/clowne/plan.rb +0 -81
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: '0815485e47677a65023e13711ad70570b79c97057ad8d09bed83f45aa293f939'
|
4
|
+
data.tar.gz: d1f8a39135462f1bca941515d055f19f44cdf92c0152ea0c49d8dd3f39475fb7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 809ecabf16dc071eda96f03853baaa42b5736804864e417e2d9a03ee3d37425b7e01c649cee2a2b8728813ffb09a3a4fb61a59e53ebdbf5f3da0b0a435a91807
|
7
|
+
data.tar.gz: 9c186a3f86f85f2a08eaea4612329a86f104328c99aa561f82a9b42f9b69f50b159f15b1426aec68585aca6f24c937b0967876689b84a0d04a64ac041169c175
|
data/.codeclimate.yml
ADDED
data/.gitattributes
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
docs/**/* linguist-vendored
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
@@ -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
|
-
|
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
|
-
|
21
|
-
|
19
|
+
Markdown:
|
20
|
+
WarnInvalid: true
|
22
21
|
|
23
|
-
|
24
|
-
Exclude:
|
25
|
-
- 'spec/**/*.rb'
|
26
|
-
|
27
|
-
Style/SymbolArray:
|
22
|
+
Standard/SemanticBlocks:
|
28
23
|
Enabled: false
|
29
24
|
|
30
|
-
|
31
|
-
Exclude:
|
32
|
-
- 'spec/**/*.rb'
|
33
|
-
- 'Gemfile'
|
34
|
-
- 'Rakefile'
|
35
|
-
- '*.gemspec'
|
36
|
-
|
37
|
-
Metrics/BlockLength:
|
25
|
+
Lint/Void:
|
38
26
|
Exclude:
|
39
|
-
- '
|
40
|
-
|
41
|
-
Bundler/OrderedGems:
|
42
|
-
Enabled: false
|
43
|
-
|
44
|
-
Gemspec/OrderedDependencies:
|
45
|
-
Enabled: false
|
27
|
+
- 'docs/README.md'
|
28
|
+
- 'README.md'
|
data/.travis.yml
CHANGED
@@ -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.
|
25
|
+
- rvm: jruby-9.2.8.0
|
23
26
|
gemfile: gemfiles/jruby.gemfile
|
24
|
-
- rvm: 2.
|
27
|
+
- rvm: 2.7
|
25
28
|
gemfile: Gemfile
|
26
|
-
- rvm: 2.
|
29
|
+
- rvm: 2.6.5
|
27
30
|
gemfile: Gemfile
|
28
|
-
|
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.
|
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.
|
40
|
+
- rvm: jruby-9.2.8.0
|
39
41
|
gemfile: gemfiles/jruby.gemfile
|
42
|
+
- rvm: truffleruby-head
|
43
|
+
gemfile: Gemfile
|
data/CHANGELOG.md
CHANGED
@@ -1,9 +1,46 @@
|
|
1
1
|
# Change log
|
2
2
|
|
3
|
-
##
|
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
|
1
|
+
source "https://rubygems.org"
|
2
2
|
|
3
3
|
# Specify your gem's dependencies in clowne.gemspec
|
4
4
|
gemspec
|
5
5
|
|
6
|
-
gem
|
7
|
-
gem 'sqlite3'
|
8
|
-
gem 'activerecord', '>= 5.0'
|
9
|
-
gem 'simplecov'
|
6
|
+
gem "pry-byebug", platform: :mri
|
10
7
|
|
11
|
-
|
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/
|
3
|
-
[![
|
4
|
-
[![
|
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
|
-
|
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
|
-
|
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
|
30
|
+
gem "clowne"
|
37
31
|
```
|
38
32
|
|
39
33
|
## Quick Start
|
40
34
|
|
41
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
203
|
-
|
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
|
-
|
90
|
+
operation = UserCloner.call(user, email: "fake@example.com")
|
91
|
+
# => <#Clowne::Utils::Operation...>
|
227
92
|
|
228
|
-
|
93
|
+
operation.to_record
|
94
|
+
# => <#User id: nil, login: nil, email: 'fake@example.com'>
|
229
95
|
|
230
|
-
|
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
|
-
|
247
|
-
|
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
|
-
|
293
|
-
|
294
|
-
|
295
|
-
# =>
|
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
|
-
#
|
302
|
-
|
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
|
-
|
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
|
-
|
452
|
-
class UserCloner < Clowne::Cloner
|
453
|
-
adapter :active_record
|
116
|
+
### Supported ORM adapters
|
454
117
|
|
455
|
-
|
456
|
-
|
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
|
127
|
+
- [Nikolay Sverchkov](https://github.com/ssnickolay)
|
464
128
|
|
465
129
|
## License
|
466
130
|
|