clowne 0.1.0.pre1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.codeclimate.yml +7 -0
- data/.gitattributes +1 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +17 -0
- data/.travis.yml +15 -2
- data/CHANGELOG.md +9 -2
- data/Gemfile +1 -0
- data/README.md +25 -381
- data/clowne.gemspec +1 -0
- data/docs/.rubocop.yml +12 -0
- data/docs/active_record.md +36 -0
- data/docs/alternatives.md +26 -0
- data/docs/architecture.md +141 -0
- data/docs/basic_example.md +66 -0
- data/docs/configuration.md +29 -0
- data/docs/customization.md +64 -0
- data/docs/exclude_association.md +63 -0
- data/docs/execution_order.md +14 -0
- data/docs/finalize.md +35 -0
- data/docs/implicit_cloner.md +36 -0
- data/docs/include_association.md +119 -0
- data/docs/init_as.md +36 -0
- data/docs/inline_configuration.md +38 -0
- data/docs/installation.md +16 -0
- data/docs/nullify.md +37 -0
- data/docs/sequel.md +56 -0
- data/docs/supported_adapters.md +13 -0
- data/docs/traits.md +28 -0
- data/docs/web/.gitignore +11 -0
- data/docs/web/core/Footer.js +92 -0
- data/docs/web/i18n/en.json +134 -0
- data/docs/web/package.json +14 -0
- data/docs/web/pages/en/help.js +50 -0
- data/docs/web/pages/en/index.js +231 -0
- data/docs/web/pages/en/users.js +47 -0
- data/docs/web/sidebars.json +30 -0
- data/docs/web/siteConfig.js +44 -0
- data/docs/web/static/css/custom.css +229 -0
- 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 +1741 -0
- data/gemfiles/activerecord42.gemfile +1 -0
- data/gemfiles/jruby.gemfile +2 -0
- data/gemfiles/railsmaster.gemfile +1 -0
- data/lib/clowne.rb +3 -1
- data/lib/clowne/adapters/active_record.rb +3 -12
- data/lib/clowne/adapters/active_record/association.rb +1 -1
- data/lib/clowne/adapters/active_record/associations/base.rb +8 -48
- data/lib/clowne/adapters/active_record/associations/has_one.rb +8 -1
- data/lib/clowne/adapters/active_record/associations/noop.rb +4 -1
- data/lib/clowne/adapters/active_record/dsl.rb +33 -0
- data/lib/clowne/adapters/base.rb +13 -6
- data/lib/clowne/adapters/base/association.rb +69 -0
- data/lib/clowne/adapters/base/finalize.rb +1 -1
- data/lib/clowne/adapters/base/init_as.rb +21 -0
- data/lib/clowne/adapters/registry.rb +5 -11
- data/lib/clowne/adapters/sequel.rb +25 -0
- data/lib/clowne/adapters/sequel/association.rb +47 -0
- data/lib/clowne/adapters/sequel/associations.rb +26 -0
- data/lib/clowne/adapters/sequel/associations/base.rb +23 -0
- data/lib/clowne/adapters/sequel/associations/many_to_many.rb +19 -0
- data/lib/clowne/adapters/sequel/associations/noop.rb +16 -0
- data/lib/clowne/adapters/sequel/associations/one_to_many.rb +23 -0
- data/lib/clowne/adapters/sequel/associations/one_to_one.rb +23 -0
- data/lib/clowne/adapters/sequel/copier.rb +23 -0
- data/lib/clowne/adapters/sequel/record_wrapper.rb +59 -0
- data/lib/clowne/cloner.rb +6 -4
- data/lib/clowne/declarations.rb +1 -0
- data/lib/clowne/declarations/exclude_association.rb +0 -5
- data/lib/clowne/declarations/include_association.rb +0 -2
- data/lib/clowne/declarations/init_as.rb +20 -0
- data/lib/clowne/declarations/trait.rb +2 -0
- data/lib/clowne/ext/orm_ext.rb +21 -0
- data/lib/clowne/ext/string_constantize.rb +2 -2
- data/lib/clowne/planner.rb +11 -4
- data/lib/clowne/version.rb +1 -1
- metadata +70 -4
data/clowne.gemspec
CHANGED
data/docs/.rubocop.yml
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
---
|
2
|
+
id: active_record
|
3
|
+
title: Active Record
|
4
|
+
---
|
5
|
+
|
6
|
+
Clowne provides an optional ActiveRecord integration which allows you to configure cloners in your models and adds a shortcut to invoke cloners (`#clowne` method).
|
7
|
+
|
8
|
+
To enable this integration, you must require `"clowne/adapters/active_record/dsl"` somewhere in your app, e.g., in the initializer:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
# config/initializers/clowne.rb
|
12
|
+
require 'clowne/adapters/active_record/dsl'
|
13
|
+
Clowne.default_adapter = :active_record
|
14
|
+
```
|
15
|
+
|
16
|
+
Now you can specify cloning configs in your AR models:
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
class User < ActiveRecord::Base
|
20
|
+
clowne_config do
|
21
|
+
include_associations :profile
|
22
|
+
|
23
|
+
nullify :email
|
24
|
+
|
25
|
+
# whatever available for your cloners,
|
26
|
+
# active_record adapter is set implicitly here
|
27
|
+
end
|
28
|
+
end
|
29
|
+
```
|
30
|
+
|
31
|
+
And then you can clone objects like this:
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
user.clowne(traits: my_traits, **params)
|
35
|
+
# => <#User...
|
36
|
+
```
|
@@ -0,0 +1,26 @@
|
|
1
|
+
---
|
2
|
+
id: alternatives
|
3
|
+
title: Motivation & Alternatives
|
4
|
+
---
|
5
|
+
|
6
|
+
### Why did we decide to build our own cloning gem instead of using the existing solutions?
|
7
|
+
|
8
|
+
First, the existing solutions turned out not to be stable and flexible enough for us.
|
9
|
+
|
10
|
+
Secondly, they are Rails-only (or, more precisely, ActiveRecord-only).
|
11
|
+
|
12
|
+
Nevertheless, thanks to [amoeba](https://github.com/amoeba-rb/amoeba) and [deep_cloneable](https://github.com/moiristo/deep_cloneable) for inspiration.
|
13
|
+
|
14
|
+
For ActiveRecord we support amoeba-like [in-model configuration](active_record.md) and you can add missing DSL declarations yourself [easily](customization.md).
|
15
|
+
|
16
|
+
We also provide an ability to specify cloning [configuration in-place](inline_configuration.md) like `deep_clonable` does.
|
17
|
+
|
18
|
+
So, we took the best of these too and brought to the outside-of-Rails world.
|
19
|
+
|
20
|
+
### Why build a gem to clone models at all?
|
21
|
+
|
22
|
+
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.
|
23
|
+
|
24
|
+
The things become even worse when you deal with STI models and different cloning contexts.
|
25
|
+
|
26
|
+
That's why we decided to build a specific cloning tool.
|
@@ -0,0 +1,141 @@
|
|
1
|
+
---
|
2
|
+
id: architecture
|
3
|
+
title: Architecture
|
4
|
+
---
|
5
|
+
|
6
|
+
This post aims to help developers and contributors to understand how Clowne works under the hood.
|
7
|
+
|
8
|
+
There are two main notions we want to talk about: **declaration set** and **cloning plan**.
|
9
|
+
|
10
|
+
## Declaration set
|
11
|
+
|
12
|
+
_Declaration set_ is a result of calling Clowne DSL methods in the cloner class. There is (almost) one-to-one correspondence between configuration calls and declarations.
|
13
|
+
|
14
|
+
Consider an example:
|
15
|
+
|
16
|
+
```ruby
|
17
|
+
class UserCloner < Clowne::Cloner
|
18
|
+
include_association :profile
|
19
|
+
include_association :posts
|
20
|
+
|
21
|
+
nullify :external_id, :slug
|
22
|
+
|
23
|
+
trait :with_social_profile do
|
24
|
+
include_association :social_profiles
|
25
|
+
end
|
26
|
+
end
|
27
|
+
```
|
28
|
+
|
29
|
+
This cloner's declaration set contains 3 declarations. You can access it through `.declarations` method:
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
UserCloner.declarations #=>
|
33
|
+
# [
|
34
|
+
# <#Clowne::Declarations::IncludeAssociation...>,
|
35
|
+
# <#Clowne::Declarations::IncludeAssociation...>,
|
36
|
+
# <#Clowne::Declarations::Nullify...>
|
37
|
+
# ]
|
38
|
+
```
|
39
|
+
|
40
|
+
**NOTE:** `include_associations` and `exclude_associations` methods are just syntactic sugar, so they produce multiple declarations.
|
41
|
+
|
42
|
+
Traits are not included in the declaration set. Moreover, each trait is just a wrapper over a declaration set. You can access traits through `.traits` method:
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
UserCloner.traits #=>
|
46
|
+
# {
|
47
|
+
# with_social_profiles: <#Clowne::Declarations::Trait...>
|
48
|
+
# }
|
49
|
+
|
50
|
+
UserCloner.traits[:with_social_profiles].declarations #=>
|
51
|
+
# [
|
52
|
+
# <#Clowne::Declarations::IncludeAssociation...>
|
53
|
+
# ]
|
54
|
+
```
|
55
|
+
|
56
|
+
## Cloning plan
|
57
|
+
|
58
|
+
Every time you use a cloner the corresponding declaration set (which is the cloner itself declarations combined with all the specified traits declaration sets) is compiled into a _cloning plan_.
|
59
|
+
|
60
|
+
The process is straightforward: starting with an empty `Plan`, we're applying every declaration to it.
|
61
|
+
Every declaration object must implement `#compile(plan)` method, which populates the plan with _actions_. For example:
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
class IncludeAssociation
|
65
|
+
def compile(plan)
|
66
|
+
# add a `name` key with the specified value (self)
|
67
|
+
# to :association set
|
68
|
+
plan.add_to(:association, name, self)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
```
|
72
|
+
|
73
|
+
`Plan` supports 3 types of registers:
|
74
|
+
|
75
|
+
1) Scalars:
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
plan.set(key, value)
|
79
|
+
plan.remove(key)
|
80
|
+
```
|
81
|
+
|
82
|
+
Used by [`init_as`](init_as.md) declaration.
|
83
|
+
|
84
|
+
2) Append-only lists:
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
plan.add(key, value)
|
88
|
+
```
|
89
|
+
|
90
|
+
Used by [`nullify`](nullify.md) and [`finalize`](finalize.md) declarations.
|
91
|
+
|
92
|
+
3) Two-phase set (2P-Set\*):
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
plan.add_to(type, key, value)
|
96
|
+
plan.remove_from(type, key)
|
97
|
+
```
|
98
|
+
|
99
|
+
Used by [`include_association`](include_association.md) and [`exclude_association`](exclude_association.md).
|
100
|
+
|
101
|
+
|
102
|
+
\* Operations over [2P-Set](https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type#2P-Set_(Two-Phase_Set)) (adding/removing) do not depend on the order of execution; we use "remove-wins" semantics, i.e., when a key has been removed, it cannot be re-added.
|
103
|
+
|
104
|
+
Thus, the resulting plan is just a key-value storage.
|
105
|
+
|
106
|
+
## Plan resolving
|
107
|
+
|
108
|
+
The final step in the cloning process is to apply the compiled plan to a record.
|
109
|
+
|
110
|
+
Every adapter registers its own resolvers for each plan _key_ (or _action_). The [order of execution](execution_order.md) is also specified by the adapter.
|
111
|
+
|
112
|
+
For example, you can override `:nullify` resolver to handle associations:
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
module NullifyWithAssociation
|
116
|
+
def self.call(source, record, declaration, _params)
|
117
|
+
reflection = source.class.reflections[declaration.name.to_s]
|
118
|
+
|
119
|
+
# fallback to built-in nullify
|
120
|
+
if reflection.nil?
|
121
|
+
Clowne::Adapters::Base::Nullify.call(source, record, declaration)
|
122
|
+
else
|
123
|
+
association = record.__send__(declaration.name)
|
124
|
+
next record if association.nil?
|
125
|
+
|
126
|
+
association.is_a?(ActiveRecord::Relation) ? association.destroy_all : association.destroy
|
127
|
+
record.association(declaration.name).reset
|
128
|
+
|
129
|
+
# resolver must return cloned record
|
130
|
+
record
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
Clowne::Adapters::ActiveRecord.register_resolver(
|
136
|
+
:nullify,
|
137
|
+
NullifyWithAssociation,
|
138
|
+
# specify the order of execution (available options are `prepend`, `before`, `after`)
|
139
|
+
before: :finalize
|
140
|
+
)
|
141
|
+
```
|
@@ -0,0 +1,66 @@
|
|
1
|
+
---
|
2
|
+
id: basic_example
|
3
|
+
title: Basic Example
|
4
|
+
---
|
5
|
+
|
6
|
+
Assume that you have the following model:
|
7
|
+
|
8
|
+
```ruby
|
9
|
+
class User < ActiveRecord::Base
|
10
|
+
# create_table :users do |t|
|
11
|
+
# t.string :login
|
12
|
+
# t.string :email
|
13
|
+
# t.timestamps null: false
|
14
|
+
# end
|
15
|
+
|
16
|
+
has_one :profile
|
17
|
+
has_many :posts
|
18
|
+
end
|
19
|
+
```
|
20
|
+
|
21
|
+
Let's declare our cloners first:
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
class UserCloner < Clowne::Cloner
|
25
|
+
adapter :active_record
|
26
|
+
|
27
|
+
include_association :profile, clone_with: SpecialProfileCloner
|
28
|
+
include_association :posts
|
29
|
+
|
30
|
+
nullify :login
|
31
|
+
|
32
|
+
# params here is an arbitrary Hash passed into cloner
|
33
|
+
finalize do |_source, record, params|
|
34
|
+
record.email = params[:email]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class SpecialProfileCloner < Clowne::Cloner
|
39
|
+
adapter :active_record
|
40
|
+
|
41
|
+
nullify :name
|
42
|
+
end
|
43
|
+
```
|
44
|
+
|
45
|
+
Now you can use `UserCloner` to clone existing records:
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
user = User.last
|
49
|
+
#=> <#User(login: 'clown', email: 'clown@circus.example.com')>
|
50
|
+
|
51
|
+
cloned = UserCloner.call(user, email: 'fake@example.com')
|
52
|
+
cloned.persisted?
|
53
|
+
# => false
|
54
|
+
|
55
|
+
cloned.save!
|
56
|
+
cloned.login
|
57
|
+
# => nil
|
58
|
+
cloned.email
|
59
|
+
# => "fake@example.com"
|
60
|
+
|
61
|
+
# associations:
|
62
|
+
cloned.posts.count == user.posts.count
|
63
|
+
# => true
|
64
|
+
cloned.profile.name
|
65
|
+
# => nil
|
66
|
+
```
|
@@ -0,0 +1,29 @@
|
|
1
|
+
---
|
2
|
+
id: configuration
|
3
|
+
title: Configuration
|
4
|
+
---
|
5
|
+
|
6
|
+
Basic cloner implementation looks like
|
7
|
+
|
8
|
+
```ruby
|
9
|
+
class SomeCloner < Clowne::Cloner
|
10
|
+
adapter :active_record # or adapter Clowne::ActiveRecord::Adapter
|
11
|
+
# some implementation ...
|
12
|
+
end
|
13
|
+
```
|
14
|
+
|
15
|
+
But you can configure the default adapter for cloners:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
# somewhere in initializers
|
19
|
+
Clowne.default_adapter = :active_record
|
20
|
+
```
|
21
|
+
|
22
|
+
and skip adapter declaration
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
class SomeCloner < Clowne::Cloner
|
26
|
+
# some implementation ...
|
27
|
+
end
|
28
|
+
```
|
29
|
+
See the list of [available adapters](supported_adapters.md).
|
@@ -0,0 +1,64 @@
|
|
1
|
+
---
|
2
|
+
id: customization
|
3
|
+
title: Customization
|
4
|
+
---
|
5
|
+
|
6
|
+
Clowne is built with extensibility in mind. You can create your own DSL commands and resolvers.
|
7
|
+
|
8
|
+
Let's consider an example.
|
9
|
+
|
10
|
+
Suppose that you want to add the `include_all` declaration to automagically include all associations (for ActiveRecord).
|
11
|
+
|
12
|
+
First, you should add a custom declaration:
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
class IncludeAll # :nodoc: all
|
16
|
+
def compile(plan)
|
17
|
+
# Just add all_associations object to plan
|
18
|
+
plan.set(:all_associations, self)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Register our declrations, i.e. extend DSL
|
23
|
+
Clowne::Declarations.add :include_all, Clowne::Declarations::IncludeAll
|
24
|
+
```
|
25
|
+
|
26
|
+
See more on `plan` in [architecture overview](architecture.md).
|
27
|
+
|
28
|
+
Secondly, register a resolver:
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
class AllAssociations
|
32
|
+
# This method is called when all_associations command is applied.
|
33
|
+
#
|
34
|
+
# source – source record
|
35
|
+
# record – target record (our clone)
|
36
|
+
# declaration – declaration object
|
37
|
+
# params – custom params passed to cloner
|
38
|
+
def call(source, record, declaration, params:)
|
39
|
+
source.class.reflections.each_value do |_name, reflection|
|
40
|
+
# Exclude belongs_to associations
|
41
|
+
next if reflection.macro == :belongs_to
|
42
|
+
# Resolve and apply association cloner
|
43
|
+
cloner_class = Clowne::Adapters::ActiveRecord::Associations.cloner_for(reflection)
|
44
|
+
cloner_class.new(reflection, source, declaration, params).call(record)
|
45
|
+
end
|
46
|
+
record
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Finally, register the resolver
|
51
|
+
Clowne::Adapters::ActiveRecord.register_resolver(
|
52
|
+
:all_associations, AllAssociations
|
53
|
+
)
|
54
|
+
```
|
55
|
+
|
56
|
+
Now you can use it likes this:
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
class UserCloner < Clowne::Cloner
|
60
|
+
adapter :active_record
|
61
|
+
|
62
|
+
include_all
|
63
|
+
end
|
64
|
+
```
|
@@ -0,0 +1,63 @@
|
|
1
|
+
---
|
2
|
+
id: exclude_association
|
3
|
+
title: Exclude Association
|
4
|
+
---
|
5
|
+
|
6
|
+
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
|
+
|
8
|
+
Nevertheless, sometimes you might want to exclude already added associations (when inheriting a cloner or using [traits](traits.md)).
|
9
|
+
|
10
|
+
Consider an example:
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
class UserCloner < Clowne::Cloner
|
14
|
+
include_association :posts
|
15
|
+
|
16
|
+
trait :without_posts do
|
17
|
+
exclude_association :posts
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# copy user and posts
|
22
|
+
clone = UserCloner.call(user)
|
23
|
+
clone.posts.count == user.posts.count
|
24
|
+
# => true
|
25
|
+
|
26
|
+
# copy only user
|
27
|
+
clone2 = UserCloner.call(user, traits: :without_posts)
|
28
|
+
clone2.posts
|
29
|
+
# => []
|
30
|
+
```
|
31
|
+
|
32
|
+
**NOTE**: once excluded association cannot be re-included, e.g. the following cloner:
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
class UserCloner < Clowne::Cloner
|
36
|
+
exclude_association :comments
|
37
|
+
|
38
|
+
trait :with_comments do
|
39
|
+
# That wouldn't work
|
40
|
+
include_association :comments
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
clone = UserCloner.call(user, traits: :with_comments)
|
45
|
+
clone.comments.empty? #=> true
|
46
|
+
```
|
47
|
+
|
48
|
+
Why so? That allows us to have a deterministic cloning plan when combining multiple traits
|
49
|
+
(or inheriting cloners).
|
50
|
+
|
51
|
+
## Exclude multiple associations
|
52
|
+
|
53
|
+
It's possible to exclude multiple associations at once the same way as [`include_associations`](include_association.md):
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
class UserCloner < Clowne::Cloner
|
57
|
+
include_associations :accounts, :posts, :comments
|
58
|
+
|
59
|
+
trait :without_posts do
|
60
|
+
exclude_associations :posts, :comments
|
61
|
+
end
|
62
|
+
end
|
63
|
+
```
|