phillumeny 0.2.0 → 0.2.1

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
2
  SHA256:
3
- metadata.gz: 8403edeb95537840758d0411c34a4cb4eac959f3ae89498309a1c3faf62c7b59
4
- data.tar.gz: cdddf19e194bdf21d13f8beaaf58f5e8185986cd443b5867381033c4c872edcb
3
+ metadata.gz: cc29628456bbe7ab061f91f3a0519fee437a3351151c66b83b37a93bd5c40573
4
+ data.tar.gz: 3c1e8512e573d04b100ba7b37fa2e96fb310f29aa47041c442a7ab9d04720d8c
5
5
  SHA512:
6
- metadata.gz: 31e35ad66e92c1618c210c2a3a26a5d5a6f14b136c31179314e0b990d132c40163943ba0a1f213fc9e5989d9d5eabf8f6a7a8d8d3b8f7834a168d418d73f803e
7
- data.tar.gz: 37a789b49915fb23ce58f7cf4ee1863226eada76cbef064b173440bac967e9cca55864519aef919345144dd5b78ae6296a93e27de0b860c65c281795e750a78c
6
+ metadata.gz: f7a5bb55690d759ebdfb51b7d324621347cfbac9bdd08d58db15b3a5d318d9b87a7d2fba8477d6d6fd97c9a138762fe84361aea8a7b15472c0c2d758cf021de9
7
+ data.tar.gz: 65cb57ad917ce2a1132335d966d1b88b1a624c34fc610e051230af77d42e595e499b221d7b8606ae0063f377c5eb2156b12e3dd6983a4f0a8697c44964c89615
@@ -3,6 +3,7 @@
3
3
  require 'phillumeny/version'
4
4
 
5
5
  require 'phillumeny/active_model'
6
+ require 'phillumeny/active_record'
6
7
  require 'phillumeny/factory_bot'
7
8
 
8
9
  # Phillumeny is a collection of RSpec matchers for verbose testers that I use to fills some of the gaps
@@ -1,203 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Phillumeny
4
-
5
- # Matchers testing ActiveModel functionality
6
- module ActiveModel
7
-
8
- def validate_presence_of_any(*args)
9
- ValidatePresenceOfAny.new(args)
10
- end
11
-
12
- # Used for testing conditional validation of presence where you need one of the other
13
- #
14
- # @example
15
- #
16
- # class Webpage
17
- #
18
- # include ActiveModel::Validations
19
- #
20
- # attr_accessor :body
21
- # attr_accessor :headline
22
- # attr_accessor :title
23
- #
24
- # validates :body,
25
- # presence: true
26
- #
27
- # validates :headline,
28
- # presence: true,
29
- # unless: -> { title.present? }
30
- #
31
- # validates :title,
32
- # presence: true,
33
- # inclusion: { allow_blank: true, in: ['this'] },
34
- # unless: -> { headline.present? && headline != 'needs title' }
35
- #
36
- # end
37
- #
38
- # RSpec.describe Webpage, type: :model do
39
- #
40
- # it { should validate_presence_of_any(:headline, :title).valid_values(title: 'this') }
41
- # it { should_not validate_presence_of_any(:headline, :title) }
42
- # it { should_not validate_presence_of_any(:headline, :title).valid_value(:title, 'that') }
43
- #
44
- # end
45
- class ValidatePresenceOfAny
46
-
47
- attr_reader :attributes
48
- attr_reader :subject
49
-
50
- # Description used when matcher is used
51
- #
52
- # @api private
53
- #
54
- # @return [String]
55
- def description
56
- "validate the presence of any of these attributes: #{attributes.join(', ')}"
57
- end
58
-
59
- # Compiles the error message for display
60
- #
61
- # @api private
62
- #
63
- # @return [String]
64
- def failure_message
65
- messages = [subject.inspect]
66
- attributes.each do |attribute|
67
- messages << subject.errors.full_messages_for(attribute)
68
- end
69
- messages.compact.join("\n")
70
- end
71
-
72
- # Sets up arguments for the matcher
73
- #
74
- # @api private
75
- #
76
- # @return [void]
77
- def initialize(args)
78
- @attributes = args
79
- end
80
-
81
- # Runs the logic to determine if expectations are being met
82
- #
83
- # @api private
84
- #
85
- # @return [Boolean]
86
- def matches?(subject)
87
- @subject = subject
88
- store_initial_values
89
- invalid_when_none_present? && attributes.all? do |attribute|
90
- clear_attributes_and_errors
91
- initialize_value(attribute)
92
- free_of_errors_on_attribute?(attribute) ? other_attributes_valid?(attribute) : false
93
- end
94
- end
95
-
96
- # Explicitly set the value to test for an attribute if 'X' is not acceptable
97
- #
98
- # @api public
99
- #
100
- # @example
101
- # it { should validate_presence_of_any(:headline, :title).valid_value(:title, 'A valid title') }
102
- #
103
- # @return [self]
104
- def valid_value(attribute, value)
105
- attribute_values[attribute] = value
106
- self
107
- end
108
-
109
- # Explicitly set the value to test for an attribute if 'X' is not acceptable
110
- #
111
- # @api public
112
- #
113
- # @example
114
- # it do
115
- # should validate_presence_of_any(:headline, :title).valid_values(title: 'A valid title')
116
- # end
117
- #
118
- # @return [self]
119
- def valid_values(**values)
120
- attribute_values.merge!(values)
121
- self
122
- end
123
-
124
- private
125
-
126
- # Storage for our initial and valid values for the attributes we are testing against
127
- #
128
- # @api private
129
- #
130
- # @return [Hash]
131
- def attribute_values
132
- @attribute_values ||= {}
133
- end
134
-
135
- # Clears all of the attributes that we are testing against
136
- #
137
- # @api private
138
- #
139
- # @return [void]
140
- def clear_attributes_and_errors
141
- attributes.each do |attribute|
142
- subject.send("#{attribute}=", nil)
143
- end
144
- subject.errors.clear
145
- end
146
-
147
- # Checks that the current attribute we are checking against passes its other validations
148
- #
149
- # @api private
150
- #
151
- # @return [Boolean]
152
- def free_of_errors_on_attribute?(attribute)
153
- subject.class.validators_on(attribute).each do |validator|
154
- # Can/do we use self[attribute] here or would that be better used in the
155
- # conditional Proc on the validation itself?
156
- validator.validate_each(subject, attribute, subject.send(attribute))
157
- end
158
- subject.errors[attribute].empty?
159
- end
160
-
161
- # Sets the value of the attribute that we currently testing against
162
- #
163
- # @api private
164
- #
165
- # @return [void]
166
- def initialize_value(attribute)
167
- subject.send("#{attribute}=", attribute_values[attribute] || 'X')
168
- end
169
-
170
- def invalid_when_none_present?
171
- clear_attributes_and_errors
172
- !subject.valid? && attributes.all? { |attribute| subject.errors[attribute].present? }
173
- end
174
-
175
- # Checks that the other attributes that we are checking against are valid as promised
176
- #
177
- # @api private
178
- #
179
- # @return [Boolean]
180
- def other_attributes_valid?(exclude_attribute)
181
- subject.valid?
182
- (attributes - [exclude_attribute]).all? do |attribute|
183
- subject.errors[attribute].empty?
184
- end
185
- end
186
-
187
- # Store the initial values of our subject before we start messing around with it
188
- #
189
- # @api private
190
- #
191
- # @return [void]
192
- def store_initial_values
193
- attributes.each do |attribute|
194
- next if attribute_values.key?(attribute)
195
- attribute_values[attribute] = subject.send(attribute)
196
- end
197
- end
198
-
199
- end
200
-
4
+ module ActiveModel # :nodoc:
201
5
  end
202
-
203
6
  end
7
+
8
+ require 'phillumeny/active_model/validate_presence_of_any'
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phillumeny
4
+
5
+ module ActiveModel # :nodoc:
6
+
7
+ def validate_presence_of_any(*args)
8
+ ValidatePresenceOfAny.new(args)
9
+ end
10
+
11
+ # Used for testing conditional validation of presence where you need one of the other
12
+ #
13
+ # @example
14
+ #
15
+ # class Webpage
16
+ #
17
+ # include ActiveModel::Validations
18
+ #
19
+ # attr_accessor :body
20
+ # attr_accessor :headline
21
+ # attr_accessor :title
22
+ #
23
+ # validates :body,
24
+ # presence: true
25
+ #
26
+ # validates :headline,
27
+ # presence: true,
28
+ # unless: -> { title.present? }
29
+ #
30
+ # validates :title,
31
+ # presence: true,
32
+ # inclusion: { allow_blank: true, in: ['this'] },
33
+ # unless: -> { headline.present? && headline != 'needs title' }
34
+ #
35
+ # end
36
+ #
37
+ # RSpec.describe Webpage, type: :model do
38
+ #
39
+ # it { should validate_presence_of_any(:headline, :title).valid_values(title: 'this') }
40
+ # it { should_not validate_presence_of_any(:headline, :title) }
41
+ # it { should_not validate_presence_of_any(:headline, :title).valid_value(:title, 'that') }
42
+ #
43
+ # end
44
+ class ValidatePresenceOfAny
45
+
46
+ attr_reader :attributes
47
+ attr_reader :subject
48
+
49
+ # Description used when matcher is used
50
+ #
51
+ # @api private
52
+ #
53
+ # @return [String]
54
+ def description
55
+ "validate the presence of any of these attributes: #{attributes.join(', ')}"
56
+ end
57
+
58
+ # Compiles the error message for display
59
+ #
60
+ # @api private
61
+ #
62
+ # @return [String]
63
+ def failure_message
64
+ messages = [subject.inspect]
65
+ attributes.each do |attribute|
66
+ messages << subject.errors.full_messages_for(attribute)
67
+ end
68
+ messages.compact.join("\n")
69
+ end
70
+
71
+ # Sets up arguments for the matcher
72
+ #
73
+ # @api private
74
+ #
75
+ # @return [void]
76
+ def initialize(args)
77
+ @attributes = args
78
+ end
79
+
80
+ # Runs the logic to determine if expectations are being met
81
+ #
82
+ # @api private
83
+ #
84
+ # @return [Boolean]
85
+ def matches?(subject)
86
+ @subject = subject
87
+ store_initial_values
88
+ invalid_when_none_present? && attributes.all? do |attribute|
89
+ clear_attributes_and_errors
90
+ initialize_value(attribute)
91
+ free_of_errors_on_attribute?(attribute) ? other_attributes_valid?(attribute) : false
92
+ end
93
+ end
94
+
95
+ # Explicitly set the value to test for an attribute if 'X' is not acceptable
96
+ #
97
+ # @api public
98
+ #
99
+ # @example
100
+ # it { should validate_presence_of_any(:headline, :title).valid_value(:title, 'A valid title') }
101
+ #
102
+ # @return [self]
103
+ def valid_value(attribute, value)
104
+ attribute_values[attribute] = value
105
+ self
106
+ end
107
+
108
+ # Explicitly set the value to test for an attribute if 'X' is not acceptable
109
+ #
110
+ # @api public
111
+ #
112
+ # @example
113
+ # it do
114
+ # should validate_presence_of_any(:headline, :title).valid_values(title: 'A valid title')
115
+ # end
116
+ #
117
+ # @return [self]
118
+ def valid_values(**values)
119
+ attribute_values.merge!(values)
120
+ self
121
+ end
122
+
123
+ private
124
+
125
+ # Storage for our initial and valid values for the attributes we are testing against
126
+ #
127
+ # @api private
128
+ #
129
+ # @return [Hash]
130
+ def attribute_values
131
+ @attribute_values ||= {}
132
+ end
133
+
134
+ # Clears all of the attributes that we are testing against
135
+ #
136
+ # @api private
137
+ #
138
+ # @return [void]
139
+ def clear_attributes_and_errors
140
+ attributes.each do |attribute|
141
+ subject.send("#{attribute}=", nil)
142
+ end
143
+ subject.errors.clear
144
+ end
145
+
146
+ # Checks that the current attribute we are checking against passes its other validations
147
+ #
148
+ # @api private
149
+ #
150
+ # @return [Boolean]
151
+ def free_of_errors_on_attribute?(attribute)
152
+ subject.class.validators_on(attribute).each do |validator|
153
+ # Can/do we use self[attribute] here or would that be better used in the
154
+ # conditional Proc on the validation itself?
155
+ validator.validate_each(subject, attribute, subject.send(attribute))
156
+ end
157
+ subject.errors[attribute].empty?
158
+ end
159
+
160
+ # Sets the value of the attribute that we currently testing against
161
+ #
162
+ # @api private
163
+ #
164
+ # @return [void]
165
+ def initialize_value(attribute)
166
+ subject.send("#{attribute}=", attribute_values[attribute] || 'X')
167
+ end
168
+
169
+ def invalid_when_none_present?
170
+ clear_attributes_and_errors
171
+ !subject.valid? && attributes.all? { |attribute| subject.errors[attribute].present? }
172
+ end
173
+
174
+ # Checks that the other attributes that we are checking against are valid as promised
175
+ #
176
+ # @api private
177
+ #
178
+ # @return [Boolean]
179
+ def other_attributes_valid?(exclude_attribute)
180
+ subject.valid?
181
+ (attributes - [exclude_attribute]).all? do |attribute|
182
+ subject.errors[attribute].empty?
183
+ end
184
+ end
185
+
186
+ # Store the initial values of our subject before we start messing around with it
187
+ #
188
+ # @api private
189
+ #
190
+ # @return [void]
191
+ def store_initial_values
192
+ attributes.each do |attribute|
193
+ next if attribute_values.key?(attribute)
194
+ attribute_values[attribute] = subject.send(attribute)
195
+ end
196
+ end
197
+
198
+ end
199
+
200
+ end
201
+
202
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phillumeny
4
+ module ActiveRecord # :nodoc:
5
+
6
+ # Helper methods for working with the database
7
+ #
8
+ # All the methods depend on the @subject instance variable being set
9
+ module TableInformation
10
+
11
+ protected
12
+
13
+ def model_class
14
+ @model_class ||= @subject.class
15
+ end
16
+
17
+ # Retrieve all the table
18
+ def table_columns
19
+ @table_columns ||= ::ActiveRecord::Base.connection.columns(table_name)
20
+ end
21
+
22
+ def table_indexes
23
+ @table_indexes ||= ::ActiveRecord::Base.connection.indexes(table_name)
24
+ end
25
+
26
+ def table_name
27
+ @table_name ||= model_class.table_name
28
+ end
29
+
30
+ end
31
+ end
32
+ end
33
+
34
+ require 'phillumeny/active_record/cover_query_with_indexes'
35
+ require 'phillumeny/active_record/have_default_value_of'
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phillumeny
4
+
5
+ module ActiveRecord # :nodoc:
6
+
7
+ # Comb through indexes to ensure the columns are covered
8
+ #
9
+ # @api public
10
+ #
11
+ # @example
12
+ #
13
+ # [:col1, [:col1, :col2], :col2].each do |columns|
14
+ # it { should cover_query_with_indexes columns }
15
+ # end
16
+ #
17
+ # @return [Phillumeny::ActiveRecord::CoverQueryWithIndexes]
18
+ def cover_query_with_indexes(columns)
19
+ CoverQueryWithIndexes.new(columns)
20
+ end
21
+
22
+ # There is often a misunderstanding on how database indexes
23
+ # work when you are indexing across multiple columns. An index
24
+ # created for the columns [:col1, :col2] will *usually* cover
25
+ # where clauses against :col1 and [:col2, :col1] also but does not
26
+ # guarantee it will be even used if a where clause only has :col2.
27
+ class CoverQueryWithIndexes
28
+
29
+ include Phillumeny::ActiveRecord::TableInformation
30
+
31
+ def initialize(columns)
32
+ self.columns = Array(columns).map(&:to_s)
33
+ end
34
+
35
+ def description
36
+ "have database indexes that would cover #{columns}"
37
+ end
38
+
39
+ def failure_message
40
+ return "No database column(s) found for #{invalid_columns.inspect}" unless valid_columns?
41
+ "The table #{table_name} did not have index to cover queries for #{columns}"
42
+ end
43
+
44
+ def matches?(subject)
45
+ @subject = subject
46
+ valid_columns? && matching_index?
47
+ end
48
+
49
+ private
50
+
51
+ attr_accessor :columns
52
+
53
+ def invalid_columns
54
+ @invalid_columns ||= columns.reject do |col_name|
55
+ ::ActiveRecord::Base.connection.column_exists?(table_name, col_name)
56
+ end
57
+ end
58
+
59
+ def matching_index?
60
+ sized_table_indexs_columns.any? do |relevent_columns|
61
+ (columns - relevent_columns).empty?
62
+ end
63
+ end
64
+
65
+ def sized_table_indexs_columns
66
+ table_indexes.map { |index| index.columns.first(columns.size) }
67
+ end
68
+
69
+ def valid_columns?
70
+ invalid_columns.empty?
71
+ end
72
+
73
+ end
74
+
75
+ end
76
+
77
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phillumeny
4
+
5
+ module ActiveRecord # :nodoc:
6
+
7
+ # Confirm a default value is getting returned as expected
8
+ #
9
+ # @api public
10
+ #
11
+ # @example
12
+ #
13
+ # { user_id: nil, timezone: 'PDT' }.each do |attribute, value|
14
+ # # Just checks the subject that the right value is present
15
+ # it { should have_default_value_of(value).for(attribute) }
16
+ # end
17
+ #
18
+ # # Checks both the subject and confirms at the database column level
19
+ # it { should have_default_value_of('PDT').for(:timezone).in_the_database }
20
+ #
21
+ # @return [Phillumeny::ActiveRecord::HaveADefaultValueOf]
22
+ def have_default_value_of(default_value)
23
+ HaveDefaultValueOf.new(default_value)
24
+ end
25
+
26
+ class HaveDefaultValueOf # :nodoc:
27
+ # @note
28
+ # Part of this could live outside of the ActiveRecord module but we have
29
+ # an option here to confirm and check the database also so leaving
30
+ # it in here for now
31
+
32
+ include Phillumeny::ActiveRecord::TableInformation
33
+
34
+ def description
35
+ "have a default value of '#{default_value}':#{default_value.class} for #{attribute_name}"
36
+ end
37
+
38
+ def failure_message
39
+ # rubocop:disable LineLength
40
+ "The value of '#{value_for_attribute}':#{value_for_attribute.class} was found not '#{default_value}':#{default_value.class}"
41
+ # rubocop:enable LineLength
42
+ end
43
+
44
+ def for(attribute_name)
45
+ self.attribute_name = attribute_name
46
+ self
47
+ end
48
+
49
+ def in_the_database
50
+ self.check_column_default = true
51
+ self
52
+ end
53
+
54
+ def initialize(default_value)
55
+ self.default_value = default_value
56
+ end
57
+
58
+ def matches?(subject)
59
+ @subject = subject
60
+ default_value_found? && column_configured_for_default_value?
61
+ end
62
+
63
+ protected
64
+
65
+ attr_accessor :attribute_name, :check_column_default, :default_value
66
+
67
+ private
68
+
69
+ def column
70
+ @column ||= table_columns.find { |column| column.name == attribute_name.to_s }
71
+ end
72
+
73
+ def column_configured_for_default_value?
74
+ return true unless @check_column_default
75
+ default_column_value == default_value
76
+ end
77
+
78
+ def column_type
79
+ @column_type ||= ::ActiveModel::Type.lookup(column.type)
80
+ end
81
+
82
+ def default_column_value
83
+ column_type.deserialize(column.default)
84
+ end
85
+
86
+ def default_value_found?
87
+ value_for_attribute == default_value
88
+ end
89
+
90
+ def value_for_attribute
91
+ @subject.send(attribute_name)
92
+ end
93
+
94
+ end
95
+
96
+ end
97
+
98
+ end
@@ -8,7 +8,7 @@ module Phillumeny
8
8
 
9
9
  MAJOR = 0
10
10
  MINOR = 2
11
- PATCH = 0
11
+ PATCH = 1
12
12
  PRERELEASE = nil
13
13
  # PRERELEASE = 'alpha'.freeze
14
14
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: phillumeny
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Deering
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-02-25 00:00:00.000000000 Z
11
+ date: 2018-04-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -47,6 +47,10 @@ extra_rdoc_files: []
47
47
  files:
48
48
  - lib/phillumeny.rb
49
49
  - lib/phillumeny/active_model.rb
50
+ - lib/phillumeny/active_model/validate_presence_of_any.rb
51
+ - lib/phillumeny/active_record.rb
52
+ - lib/phillumeny/active_record/cover_query_with_indexes.rb
53
+ - lib/phillumeny/active_record/have_default_value_of.rb
50
54
  - lib/phillumeny/factory_bot.rb
51
55
  - lib/phillumeny/version.rb
52
56
  homepage: https://github.com/mdeering/phillumeny
@@ -68,7 +72,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
68
72
  version: '0'
69
73
  requirements: []
70
74
  rubyforge_project:
71
- rubygems_version: 2.7.5
75
+ rubygems_version: 2.7.6
72
76
  signing_key:
73
77
  specification_version: 4
74
78
  summary: Collection of RSpec matchers for verbose testers.