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 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.