clowne 0.1.0 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.rubocop.yml +11 -45
- data/.travis.yml +14 -21
- data/CHANGELOG.md +30 -0
- data/Gemfile +11 -7
- data/README.md +38 -18
- data/Rakefile +3 -3
- data/clowne.gemspec +17 -10
- 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 +4 -7
- data/docs/after_clone.md +53 -0
- data/docs/after_persist.md +77 -0
- 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 +59 -0
- data/docs/customization.md +6 -7
- data/docs/exclude_association.md +6 -8
- data/docs/finalize.md +11 -15
- data/docs/from_v02_to_v1.md +83 -0
- data/docs/getting_started.md +171 -0
- data/docs/implicit_cloner.md +2 -5
- data/docs/include_association.md +24 -10
- data/docs/index.html +29 -0
- data/docs/init_as.md +13 -9
- data/docs/inline_configuration.md +5 -6
- data/docs/nullify.md +3 -7
- data/docs/operation.md +55 -0
- data/docs/parameters.md +112 -0
- data/docs/sequel.md +16 -22
- data/docs/supported_adapters.md +3 -6
- data/docs/testing.md +194 -0
- data/docs/traits.md +1 -4
- data/gemfiles/activerecord42.gemfile +7 -5
- data/gemfiles/jruby.gemfile +8 -6
- data/gemfiles/railsmaster.gemfile +8 -6
- data/lib/clowne.rb +12 -9
- data/lib/clowne/adapters/active_record.rb +4 -5
- data/lib/clowne/adapters/active_record/associations.rb +8 -6
- data/lib/clowne/adapters/active_record/associations/base.rb +1 -5
- data/lib/clowne/adapters/active_record/associations/belongs_to.rb +29 -0
- data/lib/clowne/adapters/active_record/associations/has_one.rb +3 -2
- data/lib/clowne/adapters/active_record/dsl.rb +2 -2
- data/lib/clowne/adapters/active_record/resolvers/association.rb +38 -0
- data/lib/clowne/adapters/base.rb +49 -44
- data/lib/clowne/adapters/base/association.rb +24 -15
- data/lib/clowne/adapters/registry.rb +49 -0
- data/lib/clowne/adapters/sequel.rb +14 -10
- data/lib/clowne/adapters/sequel/associations.rb +6 -6
- data/lib/clowne/adapters/sequel/associations/base.rb +9 -5
- data/lib/clowne/adapters/sequel/associations/many_to_many.rb +6 -2
- data/lib/clowne/adapters/sequel/associations/one_to_many.rb +7 -2
- data/lib/clowne/adapters/sequel/associations/one_to_one.rb +7 -2
- data/lib/clowne/adapters/sequel/operation.rb +35 -0
- data/lib/clowne/adapters/sequel/record_wrapper.rb +0 -16
- 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 +49 -21
- data/lib/clowne/declarations.rb +15 -12
- 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 +2 -2
- data/lib/clowne/declarations/finalize.rb +5 -4
- data/lib/clowne/declarations/include_association.rb +16 -2
- data/lib/clowne/declarations/init_as.rb +5 -4
- data/lib/clowne/declarations/nullify.rb +4 -3
- data/lib/clowne/declarations/trait.rb +1 -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 +1 -1
- data/lib/clowne/ext/record_key.rb +12 -0
- data/lib/clowne/ext/string_constantize.rb +9 -3
- data/lib/clowne/ext/yield_self_then.rb +25 -0
- data/lib/clowne/planner.rb +16 -3
- 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 +99 -45
- data/docs/alternatives.md +0 -26
- data/docs/basic_example.md +0 -66
- data/docs/configuration.md +0 -29
- data/docs/execution_order.md +0 -14
- data/docs/installation.md +0 -16
- data/docs/web/.gitignore +0 -11
- data/docs/web/core/Footer.js +0 -92
- data/docs/web/i18n/en.json +0 -134
- 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 -30
- data/docs/web/siteConfig.js +0 -44
- data/docs/web/static/css/custom.css +0 -229
- data/docs/web/static/fonts/FiraCode-Medium.woff +0 -0
- data/docs/web/static/fonts/FiraCode-Regular.woff +0 -0
- data/docs/web/static/fonts/StemText.woff +0 -0
- data/docs/web/static/fonts/StemTextBold.woff +0 -0
- data/docs/web/static/img/favicon/favicon.ico +0 -0
- data/docs/web/yarn.lock +0 -1741
- data/lib/clowne/adapters/active_record/association.rb +0 -34
- data/lib/clowne/adapters/base/finalize.rb +0 -19
- data/lib/clowne/adapters/base/init_as.rb +0 -21
- data/lib/clowne/adapters/base/nullify.rb +0 -19
- data/lib/clowne/adapters/sequel/association.rb +0 -47
- data/lib/clowne/plan.rb +0 -81
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}
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# Clone mapper
|
2
|
+
|
3
|
+
*Notice: `after_persist` supported only with [`active_record`](active_record.md) adapter.*
|
4
|
+
|
5
|
+
In [`after_persist`](after_persist.md) documenation you can find interisting code:
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
class UserCloner < Clowne::Cloner
|
9
|
+
# ...
|
10
|
+
after_persist do |origin, clone, mapper:, **|
|
11
|
+
cloned_bio = mapper.clone_of(origin.bio)
|
12
|
+
clone.update(bio_id: cloned_bio.id)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
```
|
16
|
+
|
17
|
+
What is it `mapper:` and how it works?
|
18
|
+
|
19
|
+
`mapper:` is an instance of `Clowne::Utils::CloneMapper`. Active Record pattern gives us an opportunity to build our cloning tool on objects, so while cloning, we remember origin records and their clones and can use these relations after saving with `Clowne::Utils::CloneMapper`.
|
20
|
+
|
21
|
+
It perfectly works, and we can use the mapper to fix broken associations.
|
22
|
+
There is only one small nuance - we can get `#clone_of` only for the record that participated in a cloning process, and this limits the functionality of the `after_persist` callbacks.
|
23
|
+
|
24
|
+
But `Clowne is built with extensibility in mind™` and Clowne provides the ability to use your custom mapper:
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
class CustomMapper < Clowne::Utils::CloneMapper
|
28
|
+
def initialize(dependencies)
|
29
|
+
super()
|
30
|
+
# inject dependencies if need
|
31
|
+
# or fetch it from other place (e.g. from DB)
|
32
|
+
@default_bios = dependencies.index_by(&:title)
|
33
|
+
end
|
34
|
+
|
35
|
+
def clone_of(record)
|
36
|
+
super(record) || fallback(record)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def fallback(record)
|
42
|
+
# put some mapping logic here
|
43
|
+
return if record.is_a?(Post)
|
44
|
+
|
45
|
+
@default_bios[record.title]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# now we can use it:
|
50
|
+
|
51
|
+
class Post < ActiveRecord::Base
|
52
|
+
# add simple scope
|
53
|
+
# scope :default_bios, -> {...}
|
54
|
+
end
|
55
|
+
|
56
|
+
user = User.last
|
57
|
+
operation = UserCloner.call(user, mapper: CustomMapper.new(Post.default_bios))
|
58
|
+
operation.persist
|
59
|
+
```
|
data/docs/customization.md
CHANGED
@@ -1,7 +1,4 @@
|
|
1
|
-
|
2
|
-
id: customization
|
3
|
-
title: Customization
|
4
|
-
---
|
1
|
+
# Customization
|
5
2
|
|
6
3
|
Clowne is built with extensibility in mind. You can create your own DSL commands and resolvers.
|
7
4
|
|
@@ -12,15 +9,16 @@ Suppose that you want to add the `include_all` declaration to automagically incl
|
|
12
9
|
First, you should add a custom declaration:
|
13
10
|
|
14
11
|
```ruby
|
15
|
-
|
12
|
+
# Extend from Base declaration
|
13
|
+
class IncludeAll < Clowne::Declarations::Base # :nodoc: all
|
16
14
|
def compile(plan)
|
17
|
-
# Just add all_associations
|
15
|
+
# Just add all_associations declaration (self) to plan
|
18
16
|
plan.set(:all_associations, self)
|
19
17
|
end
|
20
18
|
end
|
21
19
|
|
22
20
|
# Register our declrations, i.e. extend DSL
|
23
|
-
Clowne::Declarations.add :include_all,
|
21
|
+
Clowne::Declarations.add :include_all, IncludeAll
|
24
22
|
```
|
25
23
|
|
26
24
|
See more on `plan` in [architecture overview](architecture.md).
|
@@ -39,6 +37,7 @@ class AllAssociations
|
|
39
37
|
source.class.reflections.each_value do |_name, reflection|
|
40
38
|
# Exclude belongs_to associations
|
41
39
|
next if reflection.macro == :belongs_to
|
40
|
+
|
42
41
|
# Resolve and apply association cloner
|
43
42
|
cloner_class = Clowne::Adapters::ActiveRecord::Associations.cloner_for(reflection)
|
44
43
|
cloner_class.new(reflection, source, declaration, params).call(record)
|
data/docs/exclude_association.md
CHANGED
@@ -1,7 +1,4 @@
|
|
1
|
-
|
2
|
-
id: exclude_association
|
3
|
-
title: Exclude Association
|
4
|
-
---
|
1
|
+
# Exclude Association
|
5
2
|
|
6
3
|
Clowne doesn't include any association by default and doesn't provide _magic_ `include_all` declaration (although you can [add one by yourself](customization.md)).
|
7
4
|
|
@@ -19,12 +16,12 @@ class UserCloner < Clowne::Cloner
|
|
19
16
|
end
|
20
17
|
|
21
18
|
# copy user and posts
|
22
|
-
clone = UserCloner.call(user)
|
19
|
+
clone = UserCloner.call(user).to_record
|
23
20
|
clone.posts.count == user.posts.count
|
24
21
|
# => true
|
25
22
|
|
26
23
|
# copy only user
|
27
|
-
clone2 = UserCloner.call(user, traits: :without_posts)
|
24
|
+
clone2 = UserCloner.call(user, traits: :without_posts).to_record
|
28
25
|
clone2.posts
|
29
26
|
# => []
|
30
27
|
```
|
@@ -41,8 +38,9 @@ class UserCloner < Clowne::Cloner
|
|
41
38
|
end
|
42
39
|
end
|
43
40
|
|
44
|
-
clone = UserCloner.call(user, traits: :with_comments)
|
45
|
-
clone.comments.empty?
|
41
|
+
clone = UserCloner.call(user, traits: :with_comments).to_record
|
42
|
+
clone.comments.empty?
|
43
|
+
# => true
|
46
44
|
```
|
47
45
|
|
48
46
|
Why so? That allows us to have a deterministic cloning plan when combining multiple traits
|
data/docs/finalize.md
CHANGED
@@ -1,35 +1,31 @@
|
|
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
|
-
finalize do |_source, record, _params|
|
12
|
-
record.name =
|
7
|
+
finalize do |_source, record, **_params|
|
8
|
+
record.name = "This is copy!"
|
13
9
|
end
|
14
10
|
|
15
11
|
trait :change_email do
|
16
|
-
finalize do |_source, record, params|
|
12
|
+
finalize do |_source, record, **params|
|
17
13
|
record.email = params[:email]
|
18
14
|
end
|
19
15
|
end
|
20
16
|
end
|
21
17
|
|
22
|
-
|
23
|
-
|
18
|
+
cloned = UserCloner.call(user).to_record
|
19
|
+
cloned.name
|
24
20
|
# => 'This is copy!'
|
25
|
-
|
21
|
+
cloned.email == "clone@example.com"
|
26
22
|
# => false
|
27
23
|
|
28
|
-
|
29
|
-
|
24
|
+
cloned2 = UserCloner.call(user, traits: :change_email).to_record
|
25
|
+
cloned2.name
|
30
26
|
# => 'This is copy!'
|
31
|
-
|
27
|
+
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).
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# From v0.2.x to v1.0.0
|
2
|
+
|
3
|
+
The breaking change of v1.0 is the return of a unified [`result object`](operation.md) for all adapters.
|
4
|
+
|
5
|
+
## ActiveRecord
|
6
|
+
|
7
|
+
### Update code to work with [`Operation`](operation.md)
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
# Before
|
11
|
+
clone = UserCloner.call(user)
|
12
|
+
# => <#User id: nil, ...>
|
13
|
+
clone.save!
|
14
|
+
# => true
|
15
|
+
|
16
|
+
# After
|
17
|
+
clone = UserCloner.call(user)
|
18
|
+
# => <#Clowne::Utils::Operation ...>
|
19
|
+
clone = clone.to_record
|
20
|
+
# => <#User id: 2, ...>
|
21
|
+
clone.save!
|
22
|
+
# => true
|
23
|
+
|
24
|
+
# After (even better because of using full functionality)
|
25
|
+
operation = UserCloner.call(user)
|
26
|
+
# => <#Clowne::Utils::Operation ...>
|
27
|
+
operation.persist!
|
28
|
+
# => true
|
29
|
+
clone = operation.to_record
|
30
|
+
# => <#User id: 2, ...>
|
31
|
+
clone.persisted?
|
32
|
+
# => true
|
33
|
+
```
|
34
|
+
|
35
|
+
### Move post-processing cloning logic into [`after_persist`](after_persist.md) callback (if you have it)
|
36
|
+
|
37
|
+
*Notice: `after_persist` supported only with [`active_record`](active_record.md) adapter.*
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
# Before
|
41
|
+
clone = UserCloner.call(user)
|
42
|
+
clone.save!
|
43
|
+
# do something with persisted clone
|
44
|
+
|
45
|
+
# After
|
46
|
+
class UserCloner < Clowne::Cloner
|
47
|
+
# ...
|
48
|
+
after_persist do |origin, clone, **|
|
49
|
+
# do something with persisted clone
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
clone = UserCloner.call(user).tap(&:persist).to_record
|
54
|
+
```
|
55
|
+
## Sequel
|
56
|
+
|
57
|
+
### Use `to_record` instead of `to_model`
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
# Before
|
61
|
+
record_wrapper = UserCloner.call(user)
|
62
|
+
clone = record_wrapper.to_model
|
63
|
+
clone.new?
|
64
|
+
# => true
|
65
|
+
|
66
|
+
# After
|
67
|
+
operation = UserCloner.call(user)
|
68
|
+
clone = operation.to_record
|
69
|
+
clone.new?
|
70
|
+
# => true
|
71
|
+
```
|
72
|
+
|
73
|
+
### Use `operation#persist` instead of converting to model and calling `#save`
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
# Before
|
77
|
+
record_wrapper = UserCloner.call(user)
|
78
|
+
clone = record_wrapper.to_model
|
79
|
+
clone.save
|
80
|
+
|
81
|
+
# After
|
82
|
+
clone = UserCloner.call(user).tap(&:persist).to_record
|
83
|
+
```
|
@@ -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
@@ -1,7 +1,4 @@
|
|
1
|
-
|
2
|
-
id: implicit_cloner
|
3
|
-
title: Implicit Cloner
|
4
|
-
---
|
1
|
+
# Implicit Cloner
|
5
2
|
|
6
3
|
When [cloning associations](include_association.md) Clowne tries to infer an appropriate cloner class for the records (unless `clone_with` specified).
|
7
4
|
|
@@ -28,7 +25,7 @@ user = User.last
|
|
28
25
|
user.profile.name
|
29
26
|
#=> "Bimbo"
|
30
27
|
|
31
|
-
cloned = UserCloner.call(user)
|
28
|
+
cloned = UserCloner.call(user).to_record
|
32
29
|
cloned.profile.name
|
33
30
|
# => "Clone of Bimbo"
|
34
31
|
```
|