clowne 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +11 -51
- data/.travis.yml +11 -23
- data/Gemfile +9 -9
- data/README.md +8 -7
- data/Rakefile +3 -3
- data/clowne.gemspec +14 -8
- data/docs/.nojekyll +0 -0
- data/docs/.rubocop.yml +8 -2
- data/docs/CNAME +1 -0
- data/docs/README.md +131 -0
- data/docs/_sidebar.md +25 -0
- data/docs/active_record.md +2 -5
- data/docs/after_clone.md +3 -6
- data/docs/after_persist.md +7 -10
- data/docs/architecture.md +2 -5
- data/docs/assets/docsify.min.js +1 -0
- data/docs/assets/prism-ruby.min.js +1 -0
- data/docs/assets/styles.css +348 -0
- data/docs/assets/vue.css +1 -0
- data/docs/clone_mapper.md +2 -5
- data/docs/customization.md +1 -4
- data/docs/exclude_association.md +1 -4
- data/docs/finalize.md +4 -8
- data/docs/from_v02_to_v1.md +2 -10
- data/docs/getting_started.md +171 -0
- data/docs/implicit_cloner.md +1 -4
- data/docs/include_association.md +3 -6
- data/docs/index.html +29 -0
- data/docs/init_as.md +4 -8
- data/docs/inline_configuration.md +1 -4
- data/docs/nullify.md +1 -5
- data/docs/operation.md +3 -6
- data/docs/parameters.md +5 -8
- data/docs/sequel.md +1 -4
- data/docs/supported_adapters.md +3 -6
- data/docs/testing.md +18 -21
- data/docs/traits.md +1 -4
- data/gemfiles/activerecord42.gemfile +5 -5
- data/gemfiles/jruby.gemfile +6 -6
- data/gemfiles/railsmaster.gemfile +6 -6
- data/lib/clowne.rb +11 -11
- data/lib/clowne/adapters/active_record.rb +3 -3
- data/lib/clowne/adapters/active_record/associations.rb +7 -7
- data/lib/clowne/adapters/active_record/associations/base.rb +1 -1
- data/lib/clowne/adapters/active_record/associations/belongs_to.rb +1 -1
- data/lib/clowne/adapters/active_record/associations/has_one.rb +1 -1
- data/lib/clowne/adapters/active_record/resolvers/association.rb +1 -1
- data/lib/clowne/adapters/base.rb +6 -6
- data/lib/clowne/adapters/sequel.rb +7 -7
- data/lib/clowne/adapters/sequel/associations.rb +6 -6
- data/lib/clowne/adapters/sequel/associations/base.rb +2 -2
- data/lib/clowne/adapters/sequel/associations/many_to_many.rb +4 -4
- data/lib/clowne/adapters/sequel/associations/one_to_many.rb +1 -1
- data/lib/clowne/adapters/sequel/associations/one_to_one.rb +1 -1
- data/lib/clowne/adapters/sequel/operation.rb +3 -2
- data/lib/clowne/adapters/sequel/resolvers/after_persist.rb +1 -1
- data/lib/clowne/adapters/sequel/resolvers/association.rb +1 -1
- data/lib/clowne/adapters/sequel/specifications/after_persist_does_not_support.rb +1 -1
- data/lib/clowne/cloner.rb +8 -8
- data/lib/clowne/declarations.rb +15 -15
- data/lib/clowne/declarations/after_clone.rb +1 -1
- data/lib/clowne/declarations/after_persist.rb +1 -1
- data/lib/clowne/declarations/finalize.rb +1 -1
- data/lib/clowne/declarations/include_association.rb +1 -1
- data/lib/clowne/declarations/init_as.rb +1 -1
- data/lib/clowne/declarations/nullify.rb +1 -1
- data/lib/clowne/ext/orm_ext.rb +1 -1
- data/lib/clowne/ext/record_key.rb +1 -1
- data/lib/clowne/ext/string_constantize.rb +1 -1
- data/lib/clowne/ext/yield_self_then.rb +1 -1
- data/lib/clowne/planner.rb +1 -1
- data/lib/clowne/rspec.rb +3 -3
- data/lib/clowne/rspec/clone_association.rb +3 -3
- data/lib/clowne/rspec/clone_associations.rb +2 -2
- data/lib/clowne/rspec/helpers.rb +1 -1
- data/lib/clowne/utils/clone_mapper.rb +1 -1
- data/lib/clowne/utils/operation.rb +3 -3
- data/lib/clowne/utils/params.rb +1 -1
- data/lib/clowne/version.rb +1 -1
- metadata +50 -35
- data/docs/alternatives.md +0 -26
- data/docs/basic_example.md +0 -83
- data/docs/installation.md +0 -46
- data/docs/overview.md +0 -25
- data/docs/web/.gitignore +0 -11
- data/docs/web/README.md +0 -6
- data/docs/web/core/Footer.js +0 -88
- data/docs/web/i18n/en.json +0 -141
- data/docs/web/package.json +0 -14
- data/docs/web/pages/en/help.js +0 -50
- data/docs/web/pages/en/index.js +0 -231
- data/docs/web/pages/en/users.js +0 -47
- data/docs/web/sidebars.json +0 -38
- data/docs/web/siteConfig.js +0 -46
- data/docs/web/static/css/custom.css +0 -235
- data/docs/web/static/fonts/FiraCode-Medium.woff +0 -0
- data/docs/web/static/fonts/FiraCode-Regular.woff +0 -0
- data/docs/web/static/img/favicon/favicon.ico +0 -0
- data/docs/web/yarn.lock +0 -1741
data/docs/assets/vue.css
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
@import url("https://fonts.googleapis.com/css?family=Roboto+Mono|Source+Sans+Pro:300,400,600");*{-webkit-font-smoothing:antialiased;-webkit-overflow-scrolling:touch;-webkit-tap-highlight-color:rgba(0,0,0,0);-webkit-text-size-adjust:none;-webkit-touch-callout:none;box-sizing:border-box}body:not(.ready){overflow:hidden}body:not(.ready) .app-nav,body:not(.ready)>nav,body:not(.ready) [data-cloak]{display:none}div#app{font-size:30px;font-weight:lighter;margin:40vh auto;text-align:center}div#app:empty:before{content:"Loading..."}.emoji{height:1.2rem;vertical-align:middle}.progress{background-color:var(--theme-color,#42b983);height:2px;left:0;position:fixed;right:0;top:0;transition:width .2s,opacity .4s;width:0;z-index:5}.search .search-keyword,.search a:hover{color:var(--theme-color,#42b983)}.search .search-keyword{font-style:normal;font-weight:700}body,html{height:100%}body{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;color:#34495e;font-family:Source Sans Pro,Helvetica Neue,Arial,sans-serif;font-size:15px;letter-spacing:0;margin:0;overflow-x:hidden}img{max-width:100%}a[disabled]{cursor:not-allowed;opacity:.6}kbd{border:1px solid #ccc;border-radius:3px;display:inline-block;font-size:12px!important;line-height:12px;margin-bottom:3px;padding:3px 5px;vertical-align:middle}li input[type=checkbox]{margin:0 .2em .25em 0;vertical-align:middle}.app-nav{margin:25px 60px 0 0;position:absolute;right:0;text-align:right;z-index:2}.app-nav.no-badge{margin-right:25px}.app-nav p{margin:0}.app-nav>a{margin:0 1rem;padding:5px 0}.app-nav li,.app-nav ul{display:inline-block;list-style:none;margin:0}.app-nav a{color:inherit;font-size:16px;text-decoration:none;transition:color .3s}.app-nav a.active,.app-nav a:hover{color:var(--theme-color,#42b983)}.app-nav a.active{border-bottom:2px solid var(--theme-color,#42b983)}.app-nav li{display:inline-block;margin:0 1rem;padding:5px 0;position:relative}.app-nav li ul{background-color:#fff;border:1px solid #ddd;border-bottom-color:#ccc;border-radius:4px;box-sizing:border-box;display:none;max-height:calc(100vh - 61px);overflow-y:auto;padding:10px 0;position:absolute;right:-15px;text-align:left;top:100%;white-space:nowrap}.app-nav li ul li{display:block;font-size:14px;line-height:1rem;margin:0;margin:8px 14px;white-space:nowrap}.app-nav li ul a{display:block;font-size:inherit;margin:0;padding:0}.app-nav li ul a.active{border-bottom:0}.app-nav li:hover ul{display:block}.github-corner{border-bottom:0;position:fixed;right:0;text-decoration:none;top:0;z-index:1}.github-corner:hover .octo-arm{animation:a .56s ease-in-out}.github-corner svg{color:#fff;fill:var(--theme-color,#42b983);height:80px;width:80px}main{display:block;position:relative;width:100vw;height:100%;z-index:0}main.hidden{display:none}.anchor{display:inline-block;text-decoration:none;transition:all .3s}.anchor span{color:#34495e}.anchor:hover{text-decoration:underline}.sidebar{border-right:1px solid rgba(0,0,0,.07);overflow-y:auto;padding:40px 0 0;position:absolute;top:0;bottom:0;left:0;transition:transform .25s ease-out;width:300px;z-index:3}.sidebar>h1{margin:0 auto 1rem;font-size:1.5rem;font-weight:300;text-align:center}.sidebar>h1 a{color:inherit;text-decoration:none}.sidebar>h1 .app-nav{display:block;position:static}.sidebar .sidebar-nav{line-height:2em;padding-bottom:40px}.sidebar li.collapse .app-sub-sidebar{display:none}.sidebar ul{margin:0 0 0 15px;padding:0}.sidebar li>p{font-weight:700;margin:0}.sidebar ul,.sidebar ul li{list-style:none}.sidebar ul li a{border-bottom:none;display:block}.sidebar ul li ul{padding-left:20px}.sidebar::-webkit-scrollbar{width:4px}.sidebar::-webkit-scrollbar-thumb{background:transparent;border-radius:4px}.sidebar:hover::-webkit-scrollbar-thumb{background:hsla(0,0%,53%,.4)}.sidebar:hover::-webkit-scrollbar-track{background:hsla(0,0%,53%,.1)}.sidebar-toggle{background-color:transparent;background-color:hsla(0,0%,100%,.8);border:0;outline:none;padding:10px;position:absolute;bottom:0;left:0;text-align:center;transition:opacity .3s;width:284px;z-index:4}.sidebar-toggle .sidebar-toggle-button:hover{opacity:.4}.sidebar-toggle span{background-color:var(--theme-color,#42b983);display:block;margin-bottom:4px;width:16px;height:2px}body.sticky .sidebar,body.sticky .sidebar-toggle{position:fixed}.content{padding-top:60px;position:absolute;top:0;right:0;bottom:0;left:300px;transition:left .25s ease}.markdown-section{margin:0 auto;max-width:800px;padding:30px 15px 40px;position:relative}.markdown-section>*{box-sizing:border-box;font-size:inherit}.markdown-section>:first-child{margin-top:0!important}.markdown-section hr{border:none;border-bottom:1px solid #eee;margin:2em 0}.markdown-section iframe{border:1px solid #eee;width:1px;min-width:100%}.markdown-section table{border-collapse:collapse;border-spacing:0;display:block;margin-bottom:1rem;overflow:auto;width:100%}.markdown-section th{font-weight:700}.markdown-section td,.markdown-section th{border:1px solid #ddd;padding:6px 13px}.markdown-section tr{border-top:1px solid #ccc}.markdown-section p.tip,.markdown-section tr:nth-child(2n){background-color:#f8f8f8}.markdown-section p.tip{border-bottom-right-radius:2px;border-left:4px solid #f66;border-top-right-radius:2px;margin:2em 0;padding:12px 24px 12px 30px;position:relative}.markdown-section p.tip:before{background-color:#f66;border-radius:100%;color:#fff;content:"!";font-family:Dosis,Source Sans Pro,Helvetica Neue,Arial,sans-serif;font-size:14px;font-weight:700;left:-12px;line-height:20px;position:absolute;height:20px;width:20px;text-align:center;top:14px}.markdown-section p.tip code{background-color:#efefef}.markdown-section p.tip em{color:#34495e}.markdown-section p.warn{background:rgba(66,185,131,.1);border-radius:2px;padding:1rem}.markdown-section ul.task-list>li{list-style-type:none}body.close .sidebar{transform:translateX(-300px)}body.close .sidebar-toggle{width:auto}body.close .content{left:0}@media print{.app-nav,.github-corner,.sidebar,.sidebar-toggle{display:none}}@media screen and (max-width:768px){.github-corner,.sidebar,.sidebar-toggle{position:fixed}.app-nav{margin-top:16px}.app-nav li ul{top:30px}main{height:auto;overflow-x:hidden}.sidebar{left:-300px;transition:transform .25s ease-out}.content{left:0;max-width:100vw;position:static;padding-top:20px;transition:transform .25s ease}.app-nav,.github-corner{transition:transform .25s ease-out}.sidebar-toggle{background-color:transparent;width:auto;padding:30px 30px 10px 10px}body.close .sidebar{transform:translateX(300px)}body.close .sidebar-toggle{background-color:hsla(0,0%,100%,.8);transition:background-color 1s;width:284px;padding:10px}body.close .content{transform:translateX(300px)}body.close .app-nav,body.close .github-corner{display:none}.github-corner:hover .octo-arm{animation:none}.github-corner .octo-arm{animation:a .56s ease-in-out}}@keyframes a{0%,to{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}section.cover{-ms-flex-align:center;align-items:center;background-position:50%;background-repeat:no-repeat;background-size:cover;height:100vh;display:none}section.cover.show{display:-ms-flexbox;display:flex}section.cover.has-mask .mask{background-color:#fff;opacity:.8;position:absolute;top:0;height:100%;width:100%}section.cover .cover-main{-ms-flex:1;flex:1;margin:-20px 16px 0;text-align:center;z-index:1}section.cover a{color:inherit}section.cover a,section.cover a:hover{text-decoration:none}section.cover p{line-height:1.5rem;margin:1em 0}section.cover h1{color:inherit;font-size:2.5rem;font-weight:300;margin:.625rem 0 2.5rem;position:relative;text-align:center}section.cover h1 a{display:block}section.cover h1 small{bottom:-.4375rem;font-size:1rem;position:absolute}section.cover blockquote{font-size:1.5rem;text-align:center}section.cover ul{line-height:1.8;list-style-type:none;margin:1em auto;max-width:500px;padding:0}section.cover .cover-main>p:last-child a{border:1px solid var(--theme-color,#42b983);border-radius:2rem;box-sizing:border-box;color:var(--theme-color,#42b983);display:inline-block;font-size:1.05rem;letter-spacing:.1rem;margin:.5rem 1rem;padding:.75em 2rem;text-decoration:none;transition:all .15s ease}section.cover .cover-main>p:last-child a:last-child{background-color:var(--theme-color,#42b983);color:#fff}section.cover .cover-main>p:last-child a:last-child:hover{color:inherit;opacity:.8}section.cover .cover-main>p:last-child a:hover{color:inherit}section.cover blockquote>p>a{border-bottom:2px solid var(--theme-color,#42b983);transition:color .3s}section.cover blockquote>p>a:hover{color:var(--theme-color,#42b983)}.sidebar,body{background-color:#fff}.sidebar{color:#364149}.sidebar li{margin:6px 0}.sidebar ul li a{color:#505d6b;font-size:14px;font-weight:400;overflow:hidden;text-decoration:none;text-overflow:ellipsis;white-space:nowrap}.sidebar ul li a:hover{text-decoration:underline}.sidebar ul li ul{padding:0}.sidebar ul li.active>a{border-right:2px solid;color:var(--theme-color,#42b983);font-weight:600}.app-sub-sidebar li:before{content:"-";padding-right:4px;float:left}.markdown-section h1,.markdown-section h2,.markdown-section h3,.markdown-section h4,.markdown-section strong{color:#2c3e50;font-weight:600}.markdown-section a{color:var(--theme-color,#42b983);font-weight:600}.markdown-section h1{font-size:2rem;margin:0 0 1rem}.markdown-section h2{font-size:1.75rem;margin:45px 0 .8rem}.markdown-section h3{font-size:1.5rem;margin:40px 0 .6rem}.markdown-section h4{font-size:1.25rem}.markdown-section h5{font-size:1rem}.markdown-section h6{color:#777;font-size:1rem}.markdown-section figure,.markdown-section p{margin:1.2em 0}.markdown-section ol,.markdown-section p,.markdown-section ul{line-height:1.6rem;word-spacing:.05rem}.markdown-section ol,.markdown-section ul{padding-left:1.5rem}.markdown-section blockquote{border-left:4px solid var(--theme-color,#42b983);color:#858585;margin:2em 0;padding-left:20px}.markdown-section blockquote p{font-weight:600;margin-left:0}.markdown-section iframe{margin:1em 0}.markdown-section em{color:#7f8c8d}.markdown-section code{border-radius:2px;color:#e96900;font-size:.8rem;margin:0 2px;padding:3px 5px;white-space:pre-wrap}.markdown-section code,.markdown-section pre{background-color:#f8f8f8;font-family:Roboto Mono,Monaco,courier,monospace}.markdown-section pre{-moz-osx-font-smoothing:initial;-webkit-font-smoothing:initial;line-height:1.5rem;margin:1.2em 0;overflow:auto;padding:0 1.4rem;position:relative;word-wrap:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#8e908c}.token.namespace{opacity:.7}.token.boolean,.token.number{color:#c76b29}.token.punctuation{color:#525252}.token.property{color:#c08b30}.token.tag{color:#2973b7}.token.string{color:var(--theme-color,#42b983)}.token.selector{color:#6679cc}.token.attr-name{color:#2973b7}.language-css .token.string,.style .token.string,.token.entity,.token.url{color:#22a2c9}.token.attr-value,.token.control,.token.directive,.token.unit{color:var(--theme-color,#42b983)}.token.function,.token.keyword{color:#e96900}.token.atrule,.token.regex,.token.statement{color:#22a2c9}.token.placeholder,.token.variable{color:#3d8fd1}.token.deleted{text-decoration:line-through}.token.inserted{border-bottom:1px dotted #202746;text-decoration:none}.token.italic{font-style:italic}.token.bold,.token.important{font-weight:700}.token.important{color:#c94922}.token.entity{cursor:help}.markdown-section pre>code{-moz-osx-font-smoothing:initial;-webkit-font-smoothing:initial;background-color:#f8f8f8;border-radius:2px;color:#525252;display:block;font-family:Roboto Mono,Monaco,courier,monospace;font-size:.8rem;line-height:inherit;margin:0 2px;max-width:inherit;overflow:inherit;padding:2.2em 5px;white-space:inherit}.markdown-section code:after,.markdown-section code:before{letter-spacing:.05rem}code .token{-moz-osx-font-smoothing:initial;-webkit-font-smoothing:initial;min-height:1.5rem}pre:after{color:#ccc;content:attr(data-lang);font-size:.6rem;font-weight:600;height:15px;line-height:15px;padding:5px 10px 0;position:absolute;right:0;text-align:right;top:0}
|
data/docs/clone_mapper.md
CHANGED
@@ -1,9 +1,6 @@
|
|
1
|
-
|
2
|
-
id: clone_mapper
|
3
|
-
title: Clone mapper
|
4
|
-
---
|
1
|
+
# Clone mapper
|
5
2
|
|
6
|
-
|
3
|
+
*Notice: `after_persist` supported only with [`active_record`](active_record.md) adapter.*
|
7
4
|
|
8
5
|
In [`after_persist`](after_persist.md) documenation you can find interisting code:
|
9
6
|
|
data/docs/customization.md
CHANGED
data/docs/exclude_association.md
CHANGED
data/docs/finalize.md
CHANGED
@@ -1,15 +1,11 @@
|
|
1
|
-
|
2
|
-
id: finalize
|
3
|
-
title: Finalization
|
4
|
-
sidebar_label: Finalize
|
5
|
-
---
|
1
|
+
# Finalization
|
6
2
|
|
7
3
|
To apply custom transformations to the cloned record, you can use the `finalize` declaration:
|
8
4
|
|
9
5
|
```ruby
|
10
6
|
class UserCloner < Clowne::Cloner
|
11
7
|
finalize do |_source, record, _params|
|
12
|
-
record.name =
|
8
|
+
record.name = "This is copy!"
|
13
9
|
end
|
14
10
|
|
15
11
|
trait :change_email do
|
@@ -22,7 +18,7 @@ end
|
|
22
18
|
cloned = UserCloner.call(user).to_record
|
23
19
|
cloned.name
|
24
20
|
# => 'This is copy!'
|
25
|
-
cloned.email ==
|
21
|
+
cloned.email == "clone@example.com"
|
26
22
|
# => false
|
27
23
|
|
28
24
|
cloned2 = UserCloner.call(user, traits: :change_email).to_record
|
@@ -32,4 +28,4 @@ cloned2.email
|
|
32
28
|
# => 'clone@example.com'
|
33
29
|
```
|
34
30
|
|
35
|
-
Finalization blocks are called at the end of the [cloning process](
|
31
|
+
Finalization blocks are called at the end of the [cloning process](getting_started?id=execution-order).
|
data/docs/from_v02_to_v1.md
CHANGED
@@ -1,7 +1,4 @@
|
|
1
|
-
|
2
|
-
id: from_v02_to_v10
|
3
|
-
title: From v0.2.x to v1.0.0
|
4
|
-
---
|
1
|
+
# From v0.2.x to v1.0.0
|
5
2
|
|
6
3
|
The breaking change of v1.0 is the return of a unified [`result object`](operation.md) for all adapters.
|
7
4
|
|
@@ -37,9 +34,8 @@ clone.persisted?
|
|
37
34
|
|
38
35
|
### Move post-processing cloning logic into [`after_persist`](after_persist.md) callback (if you have it)
|
39
36
|
|
40
|
-
|
37
|
+
*Notice: `after_persist` supported only with [`active_record`](active_record.md) adapter.*
|
41
38
|
|
42
|
-
<span style="display:none;"># rubocop:disable all</span>
|
43
39
|
```ruby
|
44
40
|
# Before
|
45
41
|
clone = UserCloner.call(user)
|
@@ -56,8 +52,6 @@ end
|
|
56
52
|
|
57
53
|
clone = UserCloner.call(user).tap(&:persist).to_record
|
58
54
|
```
|
59
|
-
<span style="display:none;"># rubocop:enable all</span>
|
60
|
-
|
61
55
|
## Sequel
|
62
56
|
|
63
57
|
### Use `to_record` instead of `to_model`
|
@@ -78,7 +72,6 @@ clone.new?
|
|
78
72
|
|
79
73
|
### Use `operation#persist` instead of converting to model and calling `#save`
|
80
74
|
|
81
|
-
<span style="display:none;"># rubocop:disable all</span>
|
82
75
|
```ruby
|
83
76
|
# Before
|
84
77
|
record_wrapper = UserCloner.call(user)
|
@@ -88,4 +81,3 @@ clone.save
|
|
88
81
|
# After
|
89
82
|
clone = UserCloner.call(user).tap(&:persist).to_record
|
90
83
|
```
|
91
|
-
<span style="display:none;"># rubocop:enable all</span>
|
@@ -0,0 +1,171 @@
|
|
1
|
+
# Getting Started
|
2
|
+
|
3
|
+
## Installation
|
4
|
+
|
5
|
+
To install Clowne with RubyGems:
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
gem install clowne
|
9
|
+
```
|
10
|
+
|
11
|
+
Or add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
gem "clowne"
|
15
|
+
```
|
16
|
+
|
17
|
+
## Configuration
|
18
|
+
|
19
|
+
Basic cloner implementation looks like:
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
class SomeCloner < Clowne::Cloner
|
23
|
+
adapter :active_record # or adapter Clowne::Adapters::ActiveRecord
|
24
|
+
# some implementation ...
|
25
|
+
end
|
26
|
+
```
|
27
|
+
|
28
|
+
You can configure the default adapter for cloners:
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
# put to initializer
|
32
|
+
# e.g. config/initializers/clowne.rb
|
33
|
+
Clowne.default_adapter = :active_record
|
34
|
+
```
|
35
|
+
|
36
|
+
and skip explicit adapter declaration
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
class SomeCloner < Clowne::Cloner
|
40
|
+
# some implementation ...
|
41
|
+
end
|
42
|
+
```
|
43
|
+
See the list of [available adapters](supported_adapters.md).
|
44
|
+
|
45
|
+
## Basic Example
|
46
|
+
|
47
|
+
Assume that you have the following model:
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
class User < ActiveRecord::Base
|
51
|
+
# create_table :users do |t|
|
52
|
+
# t.string :login
|
53
|
+
# t.string :email
|
54
|
+
# t.timestamps null: false
|
55
|
+
# end
|
56
|
+
|
57
|
+
has_one :profile
|
58
|
+
has_many :posts
|
59
|
+
end
|
60
|
+
|
61
|
+
class Profile < ActiveRecord::Base
|
62
|
+
# create_table :profiles do |t|
|
63
|
+
# t.string :name
|
64
|
+
# end
|
65
|
+
end
|
66
|
+
|
67
|
+
class Post < ActiveRecord::Base
|
68
|
+
# create_table :posts
|
69
|
+
end
|
70
|
+
```
|
71
|
+
|
72
|
+
Let's declare our cloners first:
|
73
|
+
|
74
|
+
```ruby
|
75
|
+
class UserCloner < Clowne::Cloner
|
76
|
+
adapter :active_record
|
77
|
+
|
78
|
+
include_association :profile, clone_with: SpecialProfileCloner
|
79
|
+
include_association :posts
|
80
|
+
|
81
|
+
nullify :login
|
82
|
+
|
83
|
+
# params here is an arbitrary Hash passed into cloner
|
84
|
+
finalize do |_source, record, params|
|
85
|
+
record.email = params[:email]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
class SpecialProfileCloner < Clowne::Cloner
|
90
|
+
adapter :active_record
|
91
|
+
|
92
|
+
nullify :name
|
93
|
+
end
|
94
|
+
```
|
95
|
+
|
96
|
+
Now you can use `UserCloner` to clone existing records:
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
user = User.last
|
100
|
+
# => <#User id: 1, login: 'clown', email: 'clown@circus.example.com'>
|
101
|
+
|
102
|
+
operation = UserCloner.call(user, email: "fake@example.com")
|
103
|
+
# => <#Clowne::Utils::Operation...>
|
104
|
+
|
105
|
+
operation.to_record
|
106
|
+
# => <#User id: nil, login: nil, email: 'fake@example.com'>
|
107
|
+
|
108
|
+
operation.persist!
|
109
|
+
# => true
|
110
|
+
|
111
|
+
cloned = operation.to_record
|
112
|
+
# => <#User id: 2, login: nil, email: 'fake@example.com'>
|
113
|
+
|
114
|
+
cloned.login
|
115
|
+
# => nil
|
116
|
+
cloned.email
|
117
|
+
# => "fake@example.com"
|
118
|
+
|
119
|
+
# associations:
|
120
|
+
cloned.posts.count == user.posts.count
|
121
|
+
# => true
|
122
|
+
cloned.profile.name
|
123
|
+
# => nil
|
124
|
+
```
|
125
|
+
|
126
|
+
## Overview
|
127
|
+
|
128
|
+
In [the basic example](#basic-example), you can see that Clowne consists of flexible DSL which is used in a class inherited of `Clowne::Cloner`.
|
129
|
+
|
130
|
+
You can combinate this DSL via [`traits`](traits.md) and make a cloning plan which exactly you want.
|
131
|
+
|
132
|
+
**We strongly recommend [`write tests`](testing.md) to cover resulting cloner logic**
|
133
|
+
|
134
|
+
Cloner class returns [`Operation`](operation.md) instance as a result of cloning. The operation provides methods to save cloned record. You can wrap this call to a transaction if it is necessary.
|
135
|
+
|
136
|
+
### Execution Order
|
137
|
+
|
138
|
+
The order of cloning actions depends on the adapter (i.e., could be customized).
|
139
|
+
|
140
|
+
All built-in adapters have the same order and what happens when you call `Operation#persist`:
|
141
|
+
- init clone (see [`init_as`](init_as.md)) (empty by default)
|
142
|
+
- [`clone associations`](include_association.md)
|
143
|
+
- [`nullify`](nullify.md) attributes
|
144
|
+
- run [`finalize`](finalize.md) blocks. _The order of [`finalize`](finalize.md) blocks is the order they've been written._
|
145
|
+
- run [`after_clone`](after_clone.md) callbacks
|
146
|
+
- __SAVE CLONED RECORD__
|
147
|
+
- run [`after_persist`](after_persist.md) callbacks
|
148
|
+
|
149
|
+
## Motivation & Alternatives
|
150
|
+
|
151
|
+
### Why did we decide to build our own cloning gem instead of using the existing solutions?
|
152
|
+
|
153
|
+
First, the existing solutions turned out not to be stable and flexible enough for us.
|
154
|
+
|
155
|
+
Secondly, they are Rails-only (or, more precisely, ActiveRecord-only).
|
156
|
+
|
157
|
+
Nevertheless, thanks to [amoeba](https://github.com/amoeba-rb/amoeba) and [deep_cloneable](https://github.com/moiristo/deep_cloneable) for inspiration.
|
158
|
+
|
159
|
+
For ActiveRecord we support amoeba-like [in-model configuration](active_record.md) and you can add missing DSL declarations yourself [easily](customization.md).
|
160
|
+
|
161
|
+
We also provide an ability to specify cloning [configuration in-place](inline_configuration.md) like `deep_clonable` does.
|
162
|
+
|
163
|
+
So, we took the best of these too and brought to the outside-of-Rails world.
|
164
|
+
|
165
|
+
### Why build a gem to clone models at all?
|
166
|
+
|
167
|
+
That's a good question. Of course, you can write plain old Ruby services do handle the cloning logic. But for complex models hierarchies, this approach has major disadvantages: high code complexity and lack of re-usability.
|
168
|
+
|
169
|
+
The things become even worse when you deal with STI models and different cloning contexts.
|
170
|
+
|
171
|
+
That's why we decided to build a specific cloning tool.
|
data/docs/implicit_cloner.md
CHANGED
data/docs/include_association.md
CHANGED
@@ -1,7 +1,4 @@
|
|
1
|
-
|
2
|
-
id: include_association
|
3
|
-
title: Include Association
|
4
|
-
---
|
1
|
+
# Include Association
|
5
2
|
|
6
3
|
Use this declaration to clone model's associations:
|
7
4
|
|
@@ -27,8 +24,8 @@ include_association name, scope, options
|
|
27
24
|
|
28
25
|
Adapter |1:1 |*:1 | 1:M | M:M |
|
29
26
|
------------------------------------------|------------|------------|-------------|-------------------------|
|
30
|
-
[Active Record](
|
31
|
-
[Sequel](
|
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 |
|
32
29
|
|
33
30
|
## Scope
|
34
31
|
|
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,12 +23,12 @@ class UserCloner < Clowne::Cloner
|
|
27
23
|
end
|
28
24
|
end
|
29
25
|
|
30
|
-
jack = User.find_by(email:
|
26
|
+
jack = User.find_by(email: "jack@evl.ms")
|
31
27
|
# => <#User id: 1, ...>
|
32
|
-
jack.create_profile(name:
|
28
|
+
jack.create_profile(name: "Jack")
|
33
29
|
# => <#Profile id: 1, name: 'Jack', ...>
|
34
30
|
|
35
|
-
john = User.find_by(email:
|
31
|
+
john = User.find_by(email: "john@evl.ms")
|
36
32
|
# => <#User id: 2, ...>
|
37
33
|
|
38
34
|
# we want to clone Jack's profile to John's user,
|
data/docs/nullify.md
CHANGED
data/docs/operation.md
CHANGED
@@ -1,7 +1,4 @@
|
|
1
|
-
|
2
|
-
id: operation
|
3
|
-
title: Operation
|
4
|
-
---
|
1
|
+
# Operation
|
5
2
|
|
6
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`.
|
7
4
|
|
@@ -18,7 +15,7 @@ class UserCloner < Clowne::Cloner
|
|
18
15
|
end
|
19
16
|
end
|
20
17
|
|
21
|
-
user = User.create(email:
|
18
|
+
user = User.create(email: "evl.ms")
|
22
19
|
# => <#User id: 1, email: 'evl.ms', ...>
|
23
20
|
|
24
21
|
operation = UserCloner.call(user)
|
@@ -37,7 +34,7 @@ operation.to_record
|
|
37
34
|
# Call only after_persist callbacks:
|
38
35
|
user2 = operation.to_record
|
39
36
|
# => <#User id: 2, email: 'evl-2.ms', ...>
|
40
|
-
user2.update_attributes(email:
|
37
|
+
user2.update_attributes(email: "admin@example.com")
|
41
38
|
# => <#User id: 2, email: 'admin@example.com' ...>
|
42
39
|
operation.run_after_persist
|
43
40
|
# => <#User id: 2, email: 'evl-2.ms', ...>
|
data/docs/parameters.md
CHANGED
@@ -1,7 +1,4 @@
|
|
1
|
-
|
2
|
-
id: parameters
|
3
|
-
title: Parameters
|
4
|
-
---
|
1
|
+
# Parameters
|
5
2
|
|
6
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.
|
7
4
|
|
@@ -16,7 +13,7 @@ class UserCloner < Clowne::Cloner
|
|
16
13
|
end
|
17
14
|
end
|
18
15
|
|
19
|
-
operation = UserCloner.call(user, state: :draft, email:
|
16
|
+
operation = UserCloner.call(user, state: :draft, email: "cloned@example.com")
|
20
17
|
cloned = operation.to_record
|
21
18
|
cloned.email
|
22
19
|
# => 'cloned@example.com'
|
@@ -42,7 +39,7 @@ end
|
|
42
39
|
|
43
40
|
## Nested Parameters
|
44
41
|
|
45
|
-
Also we implemented control over the parameters for cloning associations (you can read more [here](https://github.com/
|
42
|
+
Also we implemented control over the parameters for cloning associations (you can read more [here](https://github.com/clowne-rb/clowne/issues/15)).
|
46
43
|
|
47
44
|
Let's explain what the difference:
|
48
45
|
|
@@ -78,7 +75,7 @@ class UserCloner < Clowne::Cloner
|
|
78
75
|
include_association :profile, params: Proc.new do |params, user|
|
79
76
|
{
|
80
77
|
name: params[:profile][:name],
|
81
|
-
email: user.email
|
78
|
+
email: user.email,
|
82
79
|
}
|
83
80
|
end
|
84
81
|
end
|
@@ -93,7 +90,7 @@ end
|
|
93
90
|
# Execute:
|
94
91
|
|
95
92
|
def get_profile_jsonb(user, trait)
|
96
|
-
params = {
|
93
|
+
params = {profile: {name: "John", surname: "Cena"}}
|
97
94
|
cloned = UserCloner.call(user, traits: trait, **params).to_record
|
98
95
|
cloned.profile.jsonb_field
|
99
96
|
end
|