clowne 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.travis.yml +2 -2
- data/CHANGELOG.md +8 -0
- data/Gemfile +6 -2
- data/README.md +0 -2
- data/docs/customization.md +3 -2
- data/docs/finalize.md +6 -6
- data/docs/include_association.md +7 -3
- data/docs/parameters.md +114 -0
- data/docs/testing.md +195 -0
- data/docs/web/i18n/en.json +2 -0
- data/docs/web/sidebars.json +1 -0
- data/lib/clowne/adapters/base/association.rb +1 -1
- data/lib/clowne/adapters/base/init_as.rb +1 -1
- data/lib/clowne/cloner.rb +21 -0
- data/lib/clowne/declarations.rb +1 -0
- data/lib/clowne/declarations/base.rb +13 -0
- data/lib/clowne/declarations/exclude_association.rb +1 -1
- data/lib/clowne/declarations/finalize.rb +1 -1
- data/lib/clowne/declarations/include_association.rb +14 -1
- data/lib/clowne/declarations/init_as.rb +1 -1
- data/lib/clowne/declarations/nullify.rb +1 -1
- data/lib/clowne/ext/lambda_as_proc.rb +16 -0
- data/lib/clowne/ext/string_constantize.rb +8 -2
- data/lib/clowne/params.rb +62 -0
- data/lib/clowne/plan.rb +10 -8
- data/lib/clowne/planner.rb +10 -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 +34 -0
- data/lib/clowne/version.rb +1 -1
- metadata +12 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 616d875656dc9fa7ef815190a4281d5cac5ebd8d6ac471bd6b7292a139f78d8e
|
4
|
+
data.tar.gz: 35fd2f7681c3e13152ad98562737e6bac498c79e569f330a9986eba2e45302a2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 81734963c899011b5f518d817dddeb4fe761df3326ec2165b99b877212ec8de19100d9cb2cd5beccebab276c42f0c6e806d6385ce2f929982c3509641e6c58b9
|
7
|
+
data.tar.gz: e6fa4181b54efcd3a33e9be9dc5a9f569f324954d9d72d4cb95a253bc8c5b1386e30060c52f9f6434ee3c2f03dd2e9f86c0e21f254fab9725efd927c2d07dda9
|
data/.travis.yml
CHANGED
@@ -22,7 +22,7 @@ matrix:
|
|
22
22
|
include:
|
23
23
|
- rvm: ruby-head
|
24
24
|
gemfile: gemfiles/railsmaster.gemfile
|
25
|
-
- rvm: jruby-9.1.
|
25
|
+
- rvm: jruby-9.1.15.0
|
26
26
|
gemfile: gemfiles/jruby.gemfile
|
27
27
|
- rvm: 2.5.0
|
28
28
|
gemfile: Gemfile
|
@@ -40,7 +40,7 @@ matrix:
|
|
40
40
|
allow_failures:
|
41
41
|
- rvm: ruby-head
|
42
42
|
gemfile: gemfiles/railsmaster.gemfile
|
43
|
-
- rvm: jruby-9.1.
|
43
|
+
- rvm: jruby-9.1.15.0
|
44
44
|
gemfile: gemfiles/jruby.gemfile
|
45
45
|
deploy:
|
46
46
|
provider: pages
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,13 @@
|
|
1
1
|
# Change log
|
2
2
|
|
3
|
+
## 0.2.0 (2018-02-21)
|
4
|
+
|
5
|
+
- Add `Cloner#partial_apply` method. ([@palkan][])
|
6
|
+
|
7
|
+
- Add RSpec matchers `clone_association` / `clone_associations`. ([@palkan][])
|
8
|
+
|
9
|
+
- [[#15](https://github.com/palkan/clowne/issues/15)] Add control over nested params. ([@ssnickolay][])
|
10
|
+
|
3
11
|
## 0.1.0 (2018-02-01)
|
4
12
|
|
5
13
|
- Add `init_as` declaration. ([@palkan][])
|
data/Gemfile
CHANGED
@@ -3,8 +3,12 @@ source 'https://rubygems.org'
|
|
3
3
|
# Specify your gem's dependencies in clowne.gemspec
|
4
4
|
gemspec
|
5
5
|
|
6
|
-
gem 'pry-byebug'
|
7
|
-
|
6
|
+
gem 'pry-byebug', platform: :mri
|
7
|
+
|
8
|
+
gem 'sqlite3', platform: :mri
|
9
|
+
gem 'activerecord-jdbcsqlite3-adapter', '~> 50.0', platform: :jruby
|
10
|
+
gem 'jdbc-sqlite3', platform: :jruby
|
11
|
+
|
8
12
|
gem 'activerecord', '>= 5.0'
|
9
13
|
gem 'sequel', '>= 5.0'
|
10
14
|
gem 'simplecov'
|
data/README.md
CHANGED
@@ -5,8 +5,6 @@
|
|
5
5
|
|
6
6
|
# Clowne
|
7
7
|
|
8
|
-
**NOTE**: this is the documentation for pre-release version **0.1.0.beta1**.
|
9
|
-
|
10
8
|
A flexible gem for cloning your models. Clowne focuses on ease of use and provides the ability to connect various ORM adapters.
|
11
9
|
|
12
10
|
<a href="https://evilmartians.com/">
|
data/docs/customization.md
CHANGED
@@ -12,9 +12,10 @@ Suppose that you want to add the `include_all` declaration to automagically incl
|
|
12
12
|
First, you should add a custom declaration:
|
13
13
|
|
14
14
|
```ruby
|
15
|
-
|
15
|
+
# Extend from Base declaration
|
16
|
+
class IncludeAll < Clowne::Declarations::Base # :nodoc: all
|
16
17
|
def compile(plan)
|
17
|
-
# Just add all_associations
|
18
|
+
# Just add all_associations declaration (self) to plan
|
18
19
|
plan.set(:all_associations, self)
|
19
20
|
end
|
20
21
|
end
|
data/docs/finalize.md
CHANGED
@@ -19,16 +19,16 @@ class UserCloner < Clowne::Cloner
|
|
19
19
|
end
|
20
20
|
end
|
21
21
|
|
22
|
-
|
23
|
-
|
22
|
+
cloned = UserCloner.call(user)
|
23
|
+
cloned.name
|
24
24
|
# => 'This is copy!'
|
25
|
-
|
25
|
+
cloned.email == 'clone@example.com'
|
26
26
|
# => false
|
27
27
|
|
28
|
-
|
29
|
-
|
28
|
+
cloned2 = UserCloner.call(user, traits: :change_email)
|
29
|
+
cloned2.name
|
30
30
|
# => 'This is copy!'
|
31
|
-
|
31
|
+
cloned2.email
|
32
32
|
# => 'clone@example.com'
|
33
33
|
```
|
34
34
|
|
data/docs/include_association.md
CHANGED
@@ -26,7 +26,7 @@ include_association name, scope, options
|
|
26
26
|
## Scope
|
27
27
|
|
28
28
|
Scope can be a:
|
29
|
-
- `Symbol` - named scope
|
29
|
+
- `Symbol` - named scope.
|
30
30
|
- `Proc` - custom scope (supports parameters).
|
31
31
|
|
32
32
|
Example:
|
@@ -47,11 +47,11 @@ end
|
|
47
47
|
|
48
48
|
class UserCloner < Clowne::Cloner
|
49
49
|
include_association :accounts, :active
|
50
|
-
include_association :posts, ->(params) { where(state: params[:
|
50
|
+
include_association :posts, ->(params) { where(state: params[:state]) }
|
51
51
|
end
|
52
52
|
|
53
53
|
# Clone only draft posts
|
54
|
-
UserCloner.call(user,
|
54
|
+
UserCloner.call(user, state: :draft)
|
55
55
|
# => <#User...
|
56
56
|
```
|
57
57
|
|
@@ -99,6 +99,10 @@ UserCloner.call(user)
|
|
99
99
|
|
100
100
|
**NOTE**: if custom cloner is not defined, Clowne tries to infer the [implicit cloner](implicit_cloner.md).
|
101
101
|
|
102
|
+
## Nested parameters
|
103
|
+
|
104
|
+
Follow to [documentation page](parameters.md).
|
105
|
+
|
102
106
|
## Include multiple associations
|
103
107
|
|
104
108
|
You can include multiple associations at once too:
|
data/docs/parameters.md
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
---
|
2
|
+
id: parameters
|
3
|
+
title: Parameters
|
4
|
+
---
|
5
|
+
|
6
|
+
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
|
+
|
8
|
+
Example:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
class UserCloner < Clowne::Cloner
|
12
|
+
include_association :posts, ->(params) { where(state: params[:state]) }
|
13
|
+
|
14
|
+
finalize do |_source, record, params|
|
15
|
+
record.email = params[:email]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
cloned = UserCloner.call(user, state: :draft, email: 'cloned@example.com')
|
20
|
+
cloned.email
|
21
|
+
# => 'cloned@example.com'
|
22
|
+
```
|
23
|
+
|
24
|
+
## Potential Problems
|
25
|
+
|
26
|
+
Clowne is born as a part of our big project and we use it for cloning really deep object relations. When we started to use params and forwarding them between parent-child cloners we got a nasty bugs.
|
27
|
+
|
28
|
+
As result we strongly recommend to use ruby keyword arguments instead of params hash:
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
# Bad
|
32
|
+
finalize do |_source, record, params|
|
33
|
+
record.email = params[:email]
|
34
|
+
end
|
35
|
+
|
36
|
+
# Good
|
37
|
+
finalize do |_source, record, email:, **|
|
38
|
+
record.email = email
|
39
|
+
end
|
40
|
+
```
|
41
|
+
|
42
|
+
## Nested Parameters
|
43
|
+
|
44
|
+
Also we implemented control over the parameters for cloning associations (you can read more [here](https://github.com/palkan/clowne/issues/15)).
|
45
|
+
|
46
|
+
Let's explain what the difference:
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
class UserCloner < Clowne::Cloner
|
50
|
+
# Don't pass parameters to associations
|
51
|
+
trait :default do
|
52
|
+
include_association :profile
|
53
|
+
# equal to include_association :profile, params: false
|
54
|
+
end
|
55
|
+
|
56
|
+
# Pass all parameters to associations
|
57
|
+
trait :params_true do
|
58
|
+
include_association :profile, params: true
|
59
|
+
end
|
60
|
+
|
61
|
+
# Filter parameters by key.
|
62
|
+
# Notice: value by key must be a Hash.
|
63
|
+
|
64
|
+
trait :by_key do
|
65
|
+
include_association :profile, params: :profile
|
66
|
+
end
|
67
|
+
|
68
|
+
# Execute custom block with params as argument
|
69
|
+
trait :by_block do
|
70
|
+
include_association :profile, params: Proc.new do |params|
|
71
|
+
params[:profile].map { |k, v| [k, v.upcase] }.to_h
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Execute custom block with params and parent record as arguments
|
76
|
+
trait :by_block_with_parent do
|
77
|
+
include_association :profile, params: Proc.new do |params, user|
|
78
|
+
{
|
79
|
+
name: params[:profile][:name],
|
80
|
+
email: user.email
|
81
|
+
}
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
class ProfileCloner < Clowne::Cloner
|
87
|
+
finalize do |_source, record, params|
|
88
|
+
record.jsonb_field = params
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Execute:
|
93
|
+
|
94
|
+
def get_profile_jsonb(user, trait)
|
95
|
+
params = { profile: { name: 'John', surname: 'Cena' } }
|
96
|
+
cloned = UserCloner.call(user, traits: trait, **params)
|
97
|
+
cloned.profile.jsonb_field
|
98
|
+
end
|
99
|
+
|
100
|
+
get_profile_jsonb(user, :default)
|
101
|
+
# => {}
|
102
|
+
|
103
|
+
get_profile_jsonb(user, :params_true)
|
104
|
+
# => { profile: { name: 'John', surname: 'Cena' } }
|
105
|
+
|
106
|
+
get_profile_jsonb(user, :by_key)
|
107
|
+
# => { name: 'John', surname: 'Cena' }
|
108
|
+
|
109
|
+
get_profile_jsonb(user, :by_block)
|
110
|
+
# => { name: 'JOHN', surname: 'CENA' }
|
111
|
+
|
112
|
+
get_profile_jsonb(user, :by_block_with_parent)
|
113
|
+
# => { name: 'JOHN', email: user.email }
|
114
|
+
```
|
data/docs/testing.md
ADDED
@@ -0,0 +1,195 @@
|
|
1
|
+
---
|
2
|
+
id: testing
|
3
|
+
title: Testing
|
4
|
+
---
|
5
|
+
|
6
|
+
Clowne provides specific tools to help you test your cloners.
|
7
|
+
|
8
|
+
The main goal is to make it possible to test different cloning phases separately and avoid _heavy_ tests setup phases.
|
9
|
+
|
10
|
+
Let's consider the following models and cloners:
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
# app/models/user.rb
|
14
|
+
class User < ApplicationRecord
|
15
|
+
has_one :profile
|
16
|
+
has_many :posts
|
17
|
+
end
|
18
|
+
|
19
|
+
# app/models/post.rb
|
20
|
+
class Post < ApplicationRecord
|
21
|
+
has_many :comments
|
22
|
+
has_many :votes
|
23
|
+
|
24
|
+
scope :draft, -> { where(draft: true) }
|
25
|
+
end
|
26
|
+
|
27
|
+
# app/cloners/user_cloner.rb
|
28
|
+
class UserCloner < Clowne::Cloner
|
29
|
+
class ProfileCloner
|
30
|
+
nullify :rating
|
31
|
+
end
|
32
|
+
|
33
|
+
include_association :profile, clone_with: ProfileCloner
|
34
|
+
|
35
|
+
nullify :email
|
36
|
+
|
37
|
+
finalize do |_, record, name: nil, **|
|
38
|
+
record.name = name unless name.nil?
|
39
|
+
end
|
40
|
+
|
41
|
+
trait :copy do
|
42
|
+
init_as do |user, target:, **|
|
43
|
+
# copy name
|
44
|
+
target.name = user.name
|
45
|
+
target
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
trait :with_posts do
|
50
|
+
include_association :posts, :draft, traits: :mark_as_copy
|
51
|
+
end
|
52
|
+
|
53
|
+
trait :with_popular_posts do
|
54
|
+
include_association :posts, (lambda do |params|
|
55
|
+
where('rating > ?', params[:min_rating])
|
56
|
+
end)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# app/cloners/post_cloner.rb
|
61
|
+
class PostCloner < Clowne::Cloner
|
62
|
+
include_association :comments
|
63
|
+
|
64
|
+
trait :mark_as_copy do |_, record|
|
65
|
+
record.title += ' (copy)'
|
66
|
+
end
|
67
|
+
end
|
68
|
+
```
|
69
|
+
|
70
|
+
## Getting started
|
71
|
+
|
72
|
+
Currently, only [RSpec](http://rspec.info/) is supported.
|
73
|
+
|
74
|
+
Add this line to your `spec_helper.rb` (or `rails_helper.rb`):
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
require 'clowne/rspec'
|
78
|
+
```
|
79
|
+
|
80
|
+
## Configuration matchers
|
81
|
+
|
82
|
+
There are several matchers that allow you to verify the cloner configuration.
|
83
|
+
|
84
|
+
### `clone_associations`
|
85
|
+
|
86
|
+
This matcher vefifies that your cloner includes the specified associations:
|
87
|
+
|
88
|
+
```ruby
|
89
|
+
# spec/cloners/user_cloner_spec.rb
|
90
|
+
RSpec.describe UserCloner, type: :cloner do
|
91
|
+
subject { described_class }
|
92
|
+
|
93
|
+
specify do
|
94
|
+
# checks that only the specified associations is included
|
95
|
+
is_expected.to clone_associations(:profile)
|
96
|
+
|
97
|
+
# with traits
|
98
|
+
is_expected.to clone_associations(:profile, :posts)
|
99
|
+
.with_traits(:with_posts)
|
100
|
+
|
101
|
+
# raises when there are some unspecified associations
|
102
|
+
is_expected.to clone_associations(:profile)
|
103
|
+
.with_traits(:with_posts)
|
104
|
+
#=> raises RSpec::Expectations::ExpectationNotMetError
|
105
|
+
end
|
106
|
+
end
|
107
|
+
```
|
108
|
+
|
109
|
+
### `clone_association`
|
110
|
+
|
111
|
+
This matcher allows to verify the specified association options:
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
# spec/cloners/user_cloner_spec.rb
|
115
|
+
RSpec.describe UserCloner, type: :cloner do
|
116
|
+
subject { described_class }
|
117
|
+
|
118
|
+
specify do
|
119
|
+
# simply check that association is included
|
120
|
+
is_expected.to clone_association(:profile)
|
121
|
+
|
122
|
+
# check options
|
123
|
+
is_expected.to clone_association(
|
124
|
+
:profile,
|
125
|
+
clone_with: described_class::ProfileCloner
|
126
|
+
)
|
127
|
+
|
128
|
+
# with traits, scope and activated trait
|
129
|
+
is_expected.to clone_association(
|
130
|
+
:posts,
|
131
|
+
traits: :mark_as_copy,
|
132
|
+
scope: :draft
|
133
|
+
).with_traits(:with_posts)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
```
|
137
|
+
|
138
|
+
**NOTE:** `clone_associations`/`clone_association` matchers are only available in groups marked with `type: :cloner` tag.
|
139
|
+
|
140
|
+
Clowne automaticaly marks all specs in `spec/cloners` folder with `type: :cloner`. Otherwise you have to add this tag you.
|
141
|
+
|
142
|
+
|
143
|
+
## Clone actions matchers
|
144
|
+
|
145
|
+
Under the hood, Clowne builds a [compilation plan](architecture.md) which is used to clone the record.
|
146
|
+
|
147
|
+
Plan is a set of _actions_ (such as `nullify`, `finalize`, `association`, `init_as`) which are applied to the record.
|
148
|
+
|
149
|
+
Most of the time these actions don't depend on each other, thus we can test them separately:
|
150
|
+
|
151
|
+
```ruby
|
152
|
+
# spec/cloners/user_cloner_spec.rb
|
153
|
+
RSpec.describe UserCloner, type: :cloner do
|
154
|
+
subject(:user) { create :user, name: 'Bombon' }
|
155
|
+
|
156
|
+
specify 'simple case' do
|
157
|
+
# apply only the specified part of the plan
|
158
|
+
cloned_user = described_class.partial_apply(:nullify, user)
|
159
|
+
expect(cloned_user.email).to be_nil
|
160
|
+
# finalize wasn't applied
|
161
|
+
expect(cloned_user.name).to eq 'Bombon'
|
162
|
+
end
|
163
|
+
|
164
|
+
specify 'with params' do
|
165
|
+
cloned_user = described_class.partial_apply(:finalize, user, name: 'new name')
|
166
|
+
# nullify actions were not applied!
|
167
|
+
expect(cloned_user.email).to eq user.email
|
168
|
+
# finalize was applied
|
169
|
+
expect(cloned_user.name).to eq 'new name'
|
170
|
+
end
|
171
|
+
|
172
|
+
specify 'with traits' do
|
173
|
+
a_user = create(:user, name: 'Dindon')
|
174
|
+
cloned_user = described_class.partial_apply(:init_as, user, traits: :copy, target: a_user)
|
175
|
+
# returned user is the same as target
|
176
|
+
expect(cloned_user).to be_eql(a_user)
|
177
|
+
expect(cloned_user.name).to eq 'Bombon'
|
178
|
+
end
|
179
|
+
|
180
|
+
specify 'associations' do
|
181
|
+
create(:post, user: user, rating: 1, text: 'Boom Boom')
|
182
|
+
create(:post, user: user, rating: 2, text: 'Flying Dumplings')
|
183
|
+
|
184
|
+
# you can specify which associations to include (you can use array)
|
185
|
+
# to apply all associations write:
|
186
|
+
# plan.apply(:association)
|
187
|
+
cloned_user = described_class.partial_apply(
|
188
|
+
'association.posts', user, traits: :with_popular_posts, min_rating: 1
|
189
|
+
)
|
190
|
+
|
191
|
+
expect(cloned_user.posts.size).to eq 1
|
192
|
+
expect(cloned_user.posts.first.text).to eq 'Flying Dumplings'
|
193
|
+
end
|
194
|
+
end
|
195
|
+
```
|
data/docs/web/i18n/en.json
CHANGED
@@ -22,8 +22,10 @@
|
|
22
22
|
"installation": "Installation",
|
23
23
|
"nullify": "Nullify Attributes",
|
24
24
|
"Nullify": "Nullify",
|
25
|
+
"parameters": "Parameters",
|
25
26
|
"sequel": "Sequel",
|
26
27
|
"supported_adapters": "Supported Adapters",
|
28
|
+
"testing": "Testing",
|
27
29
|
"traits": "Traits",
|
28
30
|
"HISTORY": "HISTORY",
|
29
31
|
"README": "README",
|
data/docs/web/sidebars.json
CHANGED
@@ -17,7 +17,7 @@ module Clowne
|
|
17
17
|
@params = params
|
18
18
|
@association_name = declaration.name.to_s
|
19
19
|
@reflection = reflection
|
20
|
-
@cloner_options = params
|
20
|
+
@cloner_options = declaration.params_proxy.permit(params: params, parent: source)
|
21
21
|
@cloner_options.merge!(traits: declaration.traits) if declaration.traits
|
22
22
|
end
|
23
23
|
|
@@ -6,7 +6,7 @@ module Clowne
|
|
6
6
|
module InitAs # :nodoc: all
|
7
7
|
# rubocop: disable Metrics/ParameterLists
|
8
8
|
def self.call(source, _record, declaration, params:, adapter:, **_options)
|
9
|
-
adapter.init_record(declaration.block.call(source, params))
|
9
|
+
adapter.init_record(declaration.block.call(source, **params))
|
10
10
|
end
|
11
11
|
# rubocop: enable Metrics/ParameterLists
|
12
12
|
end
|
data/lib/clowne/cloner.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require 'clowne/planner'
|
4
4
|
require 'clowne/dsl'
|
5
|
+
require 'clowne/params'
|
5
6
|
|
6
7
|
module Clowne # :nodoc: all
|
7
8
|
class UnprocessableSourceError < StandardError; end
|
@@ -47,6 +48,8 @@ module Clowne # :nodoc: all
|
|
47
48
|
|
48
49
|
traits = options.delete(:traits)
|
49
50
|
|
51
|
+
only = options.delete(:clowne_only_actions)
|
52
|
+
|
50
53
|
traits = Array(traits) unless traits.nil?
|
51
54
|
|
52
55
|
plan =
|
@@ -58,9 +61,15 @@ module Clowne # :nodoc: all
|
|
58
61
|
|
59
62
|
plan = Clowne::Planner.enhance(plan, Proc.new) if block_given?
|
60
63
|
|
64
|
+
plan = Clowne::Planner.filter_declarations(plan, only)
|
65
|
+
|
61
66
|
adapter.clone(object, plan, params: options)
|
62
67
|
end
|
63
68
|
|
69
|
+
def partial_apply(only, *args, **hargs)
|
70
|
+
call(*args, **hargs, clowne_only_actions: prepare_only(only))
|
71
|
+
end
|
72
|
+
|
64
73
|
# rubocop: enable Metrics/AbcSize, Metrics/MethodLength
|
65
74
|
# rubocop: enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
66
75
|
|
@@ -88,6 +97,18 @@ module Clowne # :nodoc: all
|
|
88
97
|
return @traits_plans if instance_variable_defined?(:@traits_plans)
|
89
98
|
@traits_plans = {}
|
90
99
|
end
|
100
|
+
|
101
|
+
def prepare_only(val)
|
102
|
+
val = Array.wrap(val)
|
103
|
+
val.each_with_object({}) do |type, acc|
|
104
|
+
# type is a Symbol or Hash
|
105
|
+
if type.is_a?(Hash)
|
106
|
+
acc.merge!(type)
|
107
|
+
elsif type.is_a?(Symbol)
|
108
|
+
acc[type] = nil
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
91
112
|
end
|
92
113
|
end
|
93
114
|
end
|
data/lib/clowne/declarations.rb
CHANGED
@@ -4,7 +4,7 @@ require 'clowne/ext/string_constantize'
|
|
4
4
|
|
5
5
|
module Clowne
|
6
6
|
module Declarations
|
7
|
-
class IncludeAssociation # :nodoc: all
|
7
|
+
class IncludeAssociation < Base # :nodoc: all
|
8
8
|
using Clowne::Ext::StringConstantize
|
9
9
|
|
10
10
|
attr_accessor :name, :scope, :options
|
@@ -19,6 +19,19 @@ module Clowne
|
|
19
19
|
plan.add_to(:association, name, self)
|
20
20
|
end
|
21
21
|
|
22
|
+
def matches?(names)
|
23
|
+
names = Array(names)
|
24
|
+
names.include?(name)
|
25
|
+
end
|
26
|
+
|
27
|
+
def params_proxy
|
28
|
+
@_params_proxy ||= Clowne::Params.proxy(options[:params])
|
29
|
+
end
|
30
|
+
|
31
|
+
def params
|
32
|
+
options[:params]
|
33
|
+
end
|
34
|
+
|
22
35
|
def clone_with
|
23
36
|
return @clone_with if instance_variable_defined?(:@clone_with)
|
24
37
|
@clone_with =
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Clowne
|
4
|
+
module Ext
|
5
|
+
# Add to_proc method for lambda
|
6
|
+
module LambdaAsProc
|
7
|
+
refine Proc do
|
8
|
+
def to_proc
|
9
|
+
return self unless lambda?
|
10
|
+
this = self
|
11
|
+
proc { |*args| this.call(*args.take(this.arity)) }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -13,8 +13,14 @@ module Clowne
|
|
13
13
|
# Remove the first blank element in case of '::ClassName' notation.
|
14
14
|
names.shift if names.size > 1 && names.first.empty?
|
15
15
|
|
16
|
-
|
17
|
-
|
16
|
+
begin
|
17
|
+
names.inject(Object) do |constant, name|
|
18
|
+
constant.const_get(name)
|
19
|
+
end
|
20
|
+
# rescue instead of const_defined? allow us to use
|
21
|
+
# Rails const autoloading (aka patched const_get)
|
22
|
+
rescue NameError
|
23
|
+
nil
|
18
24
|
end
|
19
25
|
end
|
20
26
|
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'clowne/ext/lambda_as_proc'
|
4
|
+
|
5
|
+
module Clowne
|
6
|
+
class Params # :nodoc: all
|
7
|
+
class BaseProxy
|
8
|
+
attr_reader :value
|
9
|
+
|
10
|
+
def initialize(value)
|
11
|
+
@value = value
|
12
|
+
end
|
13
|
+
|
14
|
+
def permit(_params)
|
15
|
+
raise NotImplementedError
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class PassProxy < BaseProxy
|
20
|
+
def permit(params:, **)
|
21
|
+
params
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class NullProxy < BaseProxy
|
26
|
+
def permit(_params)
|
27
|
+
{}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class BlockProxy < BaseProxy
|
32
|
+
using Clowne::Ext::LambdaAsProc
|
33
|
+
|
34
|
+
def permit(params:, parent:)
|
35
|
+
value.to_proc.call(params, parent)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class KeyProxy < BaseProxy
|
40
|
+
def permit(params:, **)
|
41
|
+
nested_params = params.fetch(value)
|
42
|
+
return nested_params if nested_params.is_a?(Hash)
|
43
|
+
|
44
|
+
raise KeyError, "value by key '#{value}' must be a Hash"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class << self
|
49
|
+
def proxy(value)
|
50
|
+
if value == true
|
51
|
+
PassProxy
|
52
|
+
elsif value.nil? || value == false
|
53
|
+
NullProxy
|
54
|
+
elsif value.is_a?(Proc)
|
55
|
+
BlockProxy
|
56
|
+
else
|
57
|
+
KeyProxy
|
58
|
+
end.new(value)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
data/lib/clowne/plan.rb
CHANGED
@@ -56,14 +56,16 @@ module Clowne
|
|
56
56
|
data[type].delete(id)
|
57
57
|
end
|
58
58
|
|
59
|
-
def declarations
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
59
|
+
def declarations(reload = false)
|
60
|
+
return @declarations if !reload && instance_variable_defined?(:@declarations)
|
61
|
+
@declarations =
|
62
|
+
registry.actions.flat_map do |type|
|
63
|
+
value = data[type]
|
64
|
+
next if value.nil?
|
65
|
+
value = value.values if value.is_a?(TwoPhaseSet)
|
66
|
+
value = Array(value)
|
67
|
+
value.map { |v| [type, v] }
|
68
|
+
end.compact
|
67
69
|
end
|
68
70
|
|
69
71
|
def dup
|
data/lib/clowne/planner.rb
CHANGED
@@ -26,6 +26,16 @@ module Clowne
|
|
26
26
|
end
|
27
27
|
end
|
28
28
|
|
29
|
+
def filter_declarations(plan, only)
|
30
|
+
return plan if only.nil?
|
31
|
+
|
32
|
+
plan.dup.tap do |new_plan|
|
33
|
+
new_plan.declarations.reject! do |(type, declaration)|
|
34
|
+
!only.key?(type) || !declaration.matches?(only[type])
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
29
39
|
private
|
30
40
|
|
31
41
|
def compile_traits(cloner, traits)
|
data/lib/clowne/rspec.rb
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Clowne
|
4
|
+
module RSpec
|
5
|
+
module Matchers # :nodoc: all
|
6
|
+
class CloneAssociation < ::RSpec::Matchers::BuiltIn::BaseMatcher
|
7
|
+
include Clowne::RSpec::Helpers
|
8
|
+
|
9
|
+
AVAILABLE_PARAMS = %i[
|
10
|
+
traits
|
11
|
+
clone_with
|
12
|
+
params
|
13
|
+
scope
|
14
|
+
].freeze
|
15
|
+
|
16
|
+
attr_reader :expected_params
|
17
|
+
|
18
|
+
def initialize(name, options)
|
19
|
+
@expected = name
|
20
|
+
extract_options! options
|
21
|
+
end
|
22
|
+
|
23
|
+
# rubocop: disable Metrics/AbcSize
|
24
|
+
def match(expected, _actual)
|
25
|
+
@actual = plan.declarations
|
26
|
+
.find { |key, decl| key == :association && decl.name == expected }
|
27
|
+
|
28
|
+
return false if @actual.nil?
|
29
|
+
|
30
|
+
@actual = actual.last
|
31
|
+
|
32
|
+
AVAILABLE_PARAMS.each do |param|
|
33
|
+
if expected_params[param] != UNDEFINED
|
34
|
+
return false if expected_params[param] != actual.send(param)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
true
|
39
|
+
end
|
40
|
+
# rubocop: enable Metrics/AbcSize
|
41
|
+
|
42
|
+
def does_not_match?(*)
|
43
|
+
raise "This matcher doesn't support negation"
|
44
|
+
end
|
45
|
+
|
46
|
+
def failure_message
|
47
|
+
if @actual.nil?
|
48
|
+
"expected to include association #{expected}, but none found"
|
49
|
+
else
|
50
|
+
params_failure_message
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def extract_options!(options)
|
57
|
+
@expected_params = {}
|
58
|
+
|
59
|
+
AVAILABLE_PARAMS.each do |param|
|
60
|
+
expected_params[param] = options.fetch(param, UNDEFINED)
|
61
|
+
end
|
62
|
+
|
63
|
+
raise ArgumentError, 'Lambda scope is not supported' if
|
64
|
+
expected_params[:scope].is_a?(Proc)
|
65
|
+
|
66
|
+
raise ArgumentError, 'Lambda params is not supported' if
|
67
|
+
expected_params[:params].is_a?(Proc)
|
68
|
+
end
|
69
|
+
|
70
|
+
def params_failure_message
|
71
|
+
"expected :#{@expected} association " \
|
72
|
+
"to have options #{formatted_expected_params}, " \
|
73
|
+
"but got #{formatted_actual_params}"
|
74
|
+
end
|
75
|
+
|
76
|
+
def formatted_expected_params
|
77
|
+
::RSpec::Support::ObjectFormatter.format(
|
78
|
+
expected_params.reject { |_, v| v == UNDEFINED }
|
79
|
+
)
|
80
|
+
end
|
81
|
+
|
82
|
+
def formatted_actual_params
|
83
|
+
actual_params = AVAILABLE_PARAMS.each_with_object({}) do |key, acc|
|
84
|
+
acc[key] = actual.send(key)
|
85
|
+
end
|
86
|
+
::RSpec::Support::ObjectFormatter.format(actual_params)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
RSpec.configure do |config|
|
94
|
+
config.include(Module.new do
|
95
|
+
def clone_association(expected, **options)
|
96
|
+
Clowne::RSpec::Matchers::CloneAssociation.new(expected, options)
|
97
|
+
end
|
98
|
+
end, type: :cloner)
|
99
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Clowne
|
4
|
+
module RSpec
|
5
|
+
module Matchers # :nodoc: all
|
6
|
+
# `clone_associations` matcher is just an extension of `contain_exactly` matcher
|
7
|
+
class CloneAssociations < ::RSpec::Matchers::BuiltIn::ContainExactly
|
8
|
+
include Clowne::RSpec::Helpers
|
9
|
+
|
10
|
+
def convert_actual_to_an_array
|
11
|
+
@actual = plan.declarations
|
12
|
+
.select { |key, _| key == :association }
|
13
|
+
.map { |_, decl| decl.name }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
RSpec.configure do |config|
|
21
|
+
config.include(Module.new do
|
22
|
+
def clone_associations(*expected)
|
23
|
+
Clowne::RSpec::Matchers::CloneAssociations.new(expected)
|
24
|
+
end
|
25
|
+
end, type: :cloner)
|
26
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Clowne
|
4
|
+
module RSpec
|
5
|
+
module Helpers # :nodoc: all
|
6
|
+
attr_reader :cloner
|
7
|
+
|
8
|
+
def with_traits(*traits)
|
9
|
+
@traits = traits
|
10
|
+
self
|
11
|
+
end
|
12
|
+
|
13
|
+
def matches?(actual)
|
14
|
+
raise ArgumentError, non_cloner_message unless actual <= ::Clowne::Cloner
|
15
|
+
@cloner = actual
|
16
|
+
super
|
17
|
+
end
|
18
|
+
|
19
|
+
def plan
|
20
|
+
@plan ||=
|
21
|
+
if @traits.nil?
|
22
|
+
cloner.default_plan
|
23
|
+
else
|
24
|
+
cloner.plan_with_traits(@traits)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def non_cloner_message
|
29
|
+
'expected a cloner to be passed to `expect(...)`, ' \
|
30
|
+
"but got #{actual_formatted}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/clowne/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: clowne
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Vladimir Dementyev
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: exe
|
11
11
|
cert_chain: []
|
12
|
-
date: 2018-02-
|
12
|
+
date: 2018-02-21 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|
@@ -134,8 +134,10 @@ files:
|
|
134
134
|
- docs/inline_configuration.md
|
135
135
|
- docs/installation.md
|
136
136
|
- docs/nullify.md
|
137
|
+
- docs/parameters.md
|
137
138
|
- docs/sequel.md
|
138
139
|
- docs/supported_adapters.md
|
140
|
+
- docs/testing.md
|
139
141
|
- docs/traits.md
|
140
142
|
- docs/web/.gitignore
|
141
143
|
- docs/web/core/Footer.js
|
@@ -184,6 +186,7 @@ files:
|
|
184
186
|
- lib/clowne/adapters/sequel/record_wrapper.rb
|
185
187
|
- lib/clowne/cloner.rb
|
186
188
|
- lib/clowne/declarations.rb
|
189
|
+
- lib/clowne/declarations/base.rb
|
187
190
|
- lib/clowne/declarations/exclude_association.rb
|
188
191
|
- lib/clowne/declarations/finalize.rb
|
189
192
|
- lib/clowne/declarations/include_association.rb
|
@@ -191,10 +194,16 @@ files:
|
|
191
194
|
- lib/clowne/declarations/nullify.rb
|
192
195
|
- lib/clowne/declarations/trait.rb
|
193
196
|
- lib/clowne/dsl.rb
|
197
|
+
- lib/clowne/ext/lambda_as_proc.rb
|
194
198
|
- lib/clowne/ext/orm_ext.rb
|
195
199
|
- lib/clowne/ext/string_constantize.rb
|
200
|
+
- lib/clowne/params.rb
|
196
201
|
- lib/clowne/plan.rb
|
197
202
|
- lib/clowne/planner.rb
|
203
|
+
- lib/clowne/rspec.rb
|
204
|
+
- lib/clowne/rspec/clone_association.rb
|
205
|
+
- lib/clowne/rspec/clone_associations.rb
|
206
|
+
- lib/clowne/rspec/helpers.rb
|
198
207
|
- lib/clowne/version.rb
|
199
208
|
homepage: https://github.com/palkan/clowne
|
200
209
|
licenses:
|
@@ -216,7 +225,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
216
225
|
version: '0'
|
217
226
|
requirements: []
|
218
227
|
rubyforge_project:
|
219
|
-
rubygems_version: 2.
|
228
|
+
rubygems_version: 2.7.4
|
220
229
|
signing_key:
|
221
230
|
specification_version: 4
|
222
231
|
summary: A flexible gem for cloning your models.
|