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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: cb07a988be474d61f3bbf9360cfc4ab25de047be
4
- data.tar.gz: 1a4763ae4bd1537ab0a254d4a0ebcb5ddd56930d
2
+ SHA256:
3
+ metadata.gz: 616d875656dc9fa7ef815190a4281d5cac5ebd8d6ac471bd6b7292a139f78d8e
4
+ data.tar.gz: 35fd2f7681c3e13152ad98562737e6bac498c79e569f330a9986eba2e45302a2
5
5
  SHA512:
6
- metadata.gz: 1ff3270acfabe0aebebfd272b3cb6eb27c9c7faea337fb59489898e008095d71dbfb835062e6c99c3b0fbc4b63e909073dd7e9e46231a6c3a0ecf5762f7204ef
7
- data.tar.gz: 53e267bcb71f9283a64bb95df753e26b38058d5283c0b0b01276209fe6a27a07b3906cfbae881734875fca9a987e25fcbda6b3143f44956ef361102296821860
6
+ metadata.gz: 81734963c899011b5f518d817dddeb4fe761df3326ec2165b99b877212ec8de19100d9cb2cd5beccebab276c42f0c6e806d6385ce2f929982c3509641e6c58b9
7
+ data.tar.gz: e6fa4181b54efcd3a33e9be9dc5a9f569f324954d9d72d4cb95a253bc8c5b1386e30060c52f9f6434ee3c2f03dd2e9f86c0e21f254fab9725efd927c2d07dda9
@@ -22,7 +22,7 @@ matrix:
22
22
  include:
23
23
  - rvm: ruby-head
24
24
  gemfile: gemfiles/railsmaster.gemfile
25
- - rvm: jruby-9.1.0.0
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.0.0
43
+ - rvm: jruby-9.1.15.0
44
44
  gemfile: gemfiles/jruby.gemfile
45
45
  deploy:
46
46
  provider: pages
@@ -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
- gem 'sqlite3'
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/">
@@ -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
- class IncludeAll # :nodoc: all
15
+ # Extend from Base declaration
16
+ class IncludeAll < Clowne::Declarations::Base # :nodoc: all
16
17
  def compile(plan)
17
- # Just add all_associations object to plan
18
+ # Just add all_associations declaration (self) to plan
18
19
  plan.set(:all_associations, self)
19
20
  end
20
21
  end
@@ -19,16 +19,16 @@ class UserCloner < Clowne::Cloner
19
19
  end
20
20
  end
21
21
 
22
- clone = UserCloner.call(user)
23
- clone.name
22
+ cloned = UserCloner.call(user)
23
+ cloned.name
24
24
  # => 'This is copy!'
25
- clone.email == 'clone@example.com'
25
+ cloned.email == 'clone@example.com'
26
26
  # => false
27
27
 
28
- clone2 = UserCloner.call(user, traits: :change_email)
29
- clone2.name
28
+ cloned2 = UserCloner.call(user, traits: :change_email)
29
+ cloned2.name
30
30
  # => 'This is copy!'
31
- clone2.email
31
+ cloned2.email
32
32
  # => 'clone@example.com'
33
33
  ```
34
34
 
@@ -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[:status]) if params[:status] }
50
+ include_association :posts, ->(params) { where(state: params[:state]) }
51
51
  end
52
52
 
53
53
  # Clone only draft posts
54
- UserCloner.call(user, status: :draft)
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:
@@ -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
+ ```
@@ -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
+ ```
@@ -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",
@@ -13,6 +13,7 @@
13
13
  "finalize",
14
14
  "init_as",
15
15
  "traits",
16
+ "parameters",
16
17
  "execution_order"
17
18
  ],
18
19
  "Adapters": [
@@ -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
@@ -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
@@ -23,6 +23,7 @@ module Clowne
23
23
  end
24
24
  end
25
25
 
26
+ require 'clowne/declarations/base'
26
27
  require 'clowne/declarations/init_as'
27
28
  require 'clowne/declarations/exclude_association'
28
29
  require 'clowne/declarations/finalize'
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clowne
4
+ module Declarations
5
+ class Base # :nodoc: all
6
+ # Used with partial_apply.
7
+ # By default match everything
8
+ def matches?(_)
9
+ true
10
+ end
11
+ end
12
+ end
13
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Clowne
4
4
  module Declarations
5
- class ExcludeAssociation # :nodoc: all
5
+ class ExcludeAssociation < Base # :nodoc: all
6
6
  attr_accessor :name
7
7
 
8
8
  def initialize(name)
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Clowne
4
4
  module Declarations
5
- class Finalize # :nodoc: all
5
+ class Finalize < Base # :nodoc: all
6
6
  attr_reader :block
7
7
 
8
8
  def initialize
@@ -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 =
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Clowne
4
4
  module Declarations
5
- class InitAs # :nodoc: all
5
+ class InitAs < Base # :nodoc: all
6
6
  attr_reader :block
7
7
 
8
8
  def initialize
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Clowne
4
4
  module Declarations
5
- class Nullify # :nodoc: all
5
+ class Nullify < Base # :nodoc: all
6
6
  attr_reader :attributes
7
7
 
8
8
  def initialize(*attributes)
@@ -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
- names.inject(Object) do |constant, name|
17
- constant.const_get(name) if constant.const_defined?(name)
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
@@ -56,14 +56,16 @@ module Clowne
56
56
  data[type].delete(id)
57
57
  end
58
58
 
59
- def declarations
60
- registry.actions.flat_map do |type|
61
- value = data[type]
62
- next if value.nil?
63
- value = value.values if value.is_a?(TwoPhaseSet)
64
- value = Array(value)
65
- value.map { |v| [type, v] }
66
- end.compact
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
@@ -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)
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'clowne/rspec/helpers'
4
+ require 'clowne/rspec/clone_associations'
5
+ require 'clowne/rspec/clone_association'
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Clowne
4
- VERSION = '0.1.0'
4
+ VERSION = '0.2.0'
5
5
  end
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.1.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-01 00:00:00.000000000 Z
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.6.13
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.