clowne 0.1.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (125) hide show
  1. checksums.yaml +5 -5
  2. data/.rubocop.yml +11 -45
  3. data/.travis.yml +14 -21
  4. data/CHANGELOG.md +30 -0
  5. data/Gemfile +11 -7
  6. data/README.md +38 -18
  7. data/Rakefile +3 -3
  8. data/clowne.gemspec +17 -10
  9. data/docs/.nojekyll +0 -0
  10. data/docs/.rubocop.yml +8 -2
  11. data/docs/CNAME +1 -0
  12. data/docs/README.md +131 -0
  13. data/docs/_sidebar.md +25 -0
  14. data/docs/active_record.md +4 -7
  15. data/docs/after_clone.md +53 -0
  16. data/docs/after_persist.md +77 -0
  17. data/docs/architecture.md +2 -5
  18. data/docs/assets/docsify.min.js +1 -0
  19. data/docs/assets/prism-ruby.min.js +1 -0
  20. data/docs/assets/styles.css +348 -0
  21. data/docs/assets/vue.css +1 -0
  22. data/docs/clone_mapper.md +59 -0
  23. data/docs/customization.md +6 -7
  24. data/docs/exclude_association.md +6 -8
  25. data/docs/finalize.md +11 -15
  26. data/docs/from_v02_to_v1.md +83 -0
  27. data/docs/getting_started.md +171 -0
  28. data/docs/implicit_cloner.md +2 -5
  29. data/docs/include_association.md +24 -10
  30. data/docs/index.html +29 -0
  31. data/docs/init_as.md +13 -9
  32. data/docs/inline_configuration.md +5 -6
  33. data/docs/nullify.md +3 -7
  34. data/docs/operation.md +55 -0
  35. data/docs/parameters.md +112 -0
  36. data/docs/sequel.md +16 -22
  37. data/docs/supported_adapters.md +3 -6
  38. data/docs/testing.md +194 -0
  39. data/docs/traits.md +1 -4
  40. data/gemfiles/activerecord42.gemfile +7 -5
  41. data/gemfiles/jruby.gemfile +8 -6
  42. data/gemfiles/railsmaster.gemfile +8 -6
  43. data/lib/clowne.rb +12 -9
  44. data/lib/clowne/adapters/active_record.rb +4 -5
  45. data/lib/clowne/adapters/active_record/associations.rb +8 -6
  46. data/lib/clowne/adapters/active_record/associations/base.rb +1 -5
  47. data/lib/clowne/adapters/active_record/associations/belongs_to.rb +29 -0
  48. data/lib/clowne/adapters/active_record/associations/has_one.rb +3 -2
  49. data/lib/clowne/adapters/active_record/dsl.rb +2 -2
  50. data/lib/clowne/adapters/active_record/resolvers/association.rb +38 -0
  51. data/lib/clowne/adapters/base.rb +49 -44
  52. data/lib/clowne/adapters/base/association.rb +24 -15
  53. data/lib/clowne/adapters/registry.rb +49 -0
  54. data/lib/clowne/adapters/sequel.rb +14 -10
  55. data/lib/clowne/adapters/sequel/associations.rb +6 -6
  56. data/lib/clowne/adapters/sequel/associations/base.rb +9 -5
  57. data/lib/clowne/adapters/sequel/associations/many_to_many.rb +6 -2
  58. data/lib/clowne/adapters/sequel/associations/one_to_many.rb +7 -2
  59. data/lib/clowne/adapters/sequel/associations/one_to_one.rb +7 -2
  60. data/lib/clowne/adapters/sequel/operation.rb +35 -0
  61. data/lib/clowne/adapters/sequel/record_wrapper.rb +0 -16
  62. data/lib/clowne/adapters/sequel/resolvers/after_persist.rb +22 -0
  63. data/lib/clowne/adapters/sequel/resolvers/association.rb +51 -0
  64. data/lib/clowne/adapters/sequel/specifications/after_persist_does_not_support.rb +15 -0
  65. data/lib/clowne/cloner.rb +49 -21
  66. data/lib/clowne/declarations.rb +15 -12
  67. data/lib/clowne/declarations/after_clone.rb +21 -0
  68. data/lib/clowne/declarations/after_persist.rb +21 -0
  69. data/lib/clowne/declarations/base.rb +13 -0
  70. data/lib/clowne/declarations/exclude_association.rb +2 -2
  71. data/lib/clowne/declarations/finalize.rb +5 -4
  72. data/lib/clowne/declarations/include_association.rb +16 -2
  73. data/lib/clowne/declarations/init_as.rb +5 -4
  74. data/lib/clowne/declarations/nullify.rb +4 -3
  75. data/lib/clowne/declarations/trait.rb +1 -0
  76. data/lib/clowne/dsl.rb +9 -0
  77. data/lib/clowne/ext/lambda_as_proc.rb +17 -0
  78. data/lib/clowne/ext/orm_ext.rb +1 -1
  79. data/lib/clowne/ext/record_key.rb +12 -0
  80. data/lib/clowne/ext/string_constantize.rb +9 -3
  81. data/lib/clowne/ext/yield_self_then.rb +25 -0
  82. data/lib/clowne/planner.rb +16 -3
  83. data/lib/clowne/resolvers/after_clone.rb +17 -0
  84. data/lib/clowne/resolvers/after_persist.rb +18 -0
  85. data/lib/clowne/resolvers/finalize.rb +12 -0
  86. data/lib/clowne/resolvers/init_as.rb +13 -0
  87. data/lib/clowne/resolvers/nullify.rb +15 -0
  88. data/lib/clowne/rspec.rb +5 -0
  89. data/lib/clowne/rspec/clone_association.rb +99 -0
  90. data/lib/clowne/rspec/clone_associations.rb +26 -0
  91. data/lib/clowne/rspec/helpers.rb +35 -0
  92. data/lib/clowne/utils/clone_mapper.rb +26 -0
  93. data/lib/clowne/utils/operation.rb +95 -0
  94. data/lib/clowne/utils/options.rb +39 -0
  95. data/lib/clowne/utils/params.rb +64 -0
  96. data/lib/clowne/utils/plan.rb +90 -0
  97. data/lib/clowne/version.rb +1 -1
  98. metadata +99 -45
  99. data/docs/alternatives.md +0 -26
  100. data/docs/basic_example.md +0 -66
  101. data/docs/configuration.md +0 -29
  102. data/docs/execution_order.md +0 -14
  103. data/docs/installation.md +0 -16
  104. data/docs/web/.gitignore +0 -11
  105. data/docs/web/core/Footer.js +0 -92
  106. data/docs/web/i18n/en.json +0 -134
  107. data/docs/web/package.json +0 -14
  108. data/docs/web/pages/en/help.js +0 -50
  109. data/docs/web/pages/en/index.js +0 -231
  110. data/docs/web/pages/en/users.js +0 -47
  111. data/docs/web/sidebars.json +0 -30
  112. data/docs/web/siteConfig.js +0 -44
  113. data/docs/web/static/css/custom.css +0 -229
  114. data/docs/web/static/fonts/FiraCode-Medium.woff +0 -0
  115. data/docs/web/static/fonts/FiraCode-Regular.woff +0 -0
  116. data/docs/web/static/fonts/StemText.woff +0 -0
  117. data/docs/web/static/fonts/StemTextBold.woff +0 -0
  118. data/docs/web/static/img/favicon/favicon.ico +0 -0
  119. data/docs/web/yarn.lock +0 -1741
  120. data/lib/clowne/adapters/active_record/association.rb +0 -34
  121. data/lib/clowne/adapters/base/finalize.rb +0 -19
  122. data/lib/clowne/adapters/base/init_as.rb +0 -21
  123. data/lib/clowne/adapters/base/nullify.rb +0 -19
  124. data/lib/clowne/adapters/sequel/association.rb +0 -47
  125. data/lib/clowne/plan.rb +0 -81
@@ -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
+ ```
@@ -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
- class IncludeAll # :nodoc: all
12
+ # Extend from Base declaration
13
+ class IncludeAll < Clowne::Declarations::Base # :nodoc: all
16
14
  def compile(plan)
17
- # Just add all_associations object to plan
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, Clowne::Declarations::IncludeAll
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)
@@ -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? #=> true
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 = 'This is copy!'
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
- clone = UserCloner.call(user)
23
- clone.name
18
+ cloned = UserCloner.call(user).to_record
19
+ cloned.name
24
20
  # => 'This is copy!'
25
- clone.email == 'clone@example.com'
21
+ cloned.email == "clone@example.com"
26
22
  # => false
27
23
 
28
- clone2 = UserCloner.call(user, traits: :change_email)
29
- clone2.name
24
+ cloned2 = UserCloner.call(user, traits: :change_email).to_record
25
+ cloned2.name
30
26
  # => 'This is copy!'
31
- clone2.email
27
+ cloned2.email
32
28
  # => 'clone@example.com'
33
29
  ```
34
30
 
35
- Finalization blocks are called at the end of the [cloning process](execution_order.md).
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.
@@ -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
  ```