clowne 0.1.0 → 0.2.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 +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.
|