phillumeny 0.2.0 → 0.2.1

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