kokishin 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/kokishin.gemspec ADDED
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'kokishin/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.authors = %w[ bm5k ]
9
+ spec.bindir = 'exe'
10
+ spec.description = 'A single home for various custom RSpec matchers.'
11
+ spec.email = %w[ 226882-BM5k@users.noreply.gitlab.com ]
12
+ spec.homepage = 'https://gitlab.com/devfu/kokishin'
13
+ spec.license = 'MIT'
14
+ spec.name = 'kokishin'
15
+ spec.require_paths = %w[ lib ]
16
+ spec.summary = 'RSpec matchers'
17
+ spec.version = Kokishin::VERSION
18
+
19
+ spec.metadata['homepage_uri'] = spec.homepage
20
+ spec.metadata['rubygems_mfa_required'] = 'true'
21
+ spec.metadata['source_code_uri'] = spec.homepage
22
+
23
+ # Specify which files should be added to the gem when it is released.
24
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
25
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
26
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
27
+ end
28
+
29
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
30
+
31
+ spec.add_dependency 'rspec-expectations'
32
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kokishin
4
+ module HashDiff
5
+
6
+ refine Hash do
7
+ # Stolen from Rails 4.0.5 since this has been removed!
8
+ #
9
+ # Returns a hash that represents the difference between two hashes.
10
+ #
11
+ # {1 => 2}.diff(1 => 2) # => {}
12
+ # {1 => 2}.diff(1 => 3) # => {1 => 2}
13
+ # {}.diff(1 => 2) # => {1 => 2}
14
+ # {1 => 2, 3 => 4}.diff(1 => 2) # => {3 => 4}
15
+ def diff other
16
+ dup.delete_if { |k, v| other[k] == v }.merge!(other.dup.delete_if { |k, _v| key?(k) })
17
+ end
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kokishin/extensions/hash_diff'
4
+
5
+ using Kokishin::HashDiff
6
+
7
+ RSpec::Matchers.define :belong_to do |kind|
8
+ description do
9
+ "belong_to #{ kind.inspect }"
10
+ end
11
+
12
+ failure_message do
13
+ msg = "Expected #{ described_class } to belong_to #{ kind.inspect }"
14
+ msg << "\n column #{ key.inspect } does not exist in #{ described_class }" if associated? && !has_column?
15
+ msg
16
+ end
17
+
18
+ failure_message_when_negated do
19
+ "Expected #{ described_class } not to belong_to #{ kind.inspect }"
20
+ end
21
+
22
+ def association
23
+ gem_version = Gem::Version.new ActiveRecord::VERSION::STRING
24
+
25
+ key = if gem_version >= Gem::Version.new('4.2.0')
26
+ expected.to_s
27
+ else
28
+ expected
29
+ end
30
+
31
+ actual.class.reflections[key]
32
+ end
33
+
34
+ def associated?
35
+ association && association.macro == :belongs_to
36
+ end
37
+
38
+ def key
39
+ @key ||= association.foreign_key
40
+ end
41
+
42
+ def has_column?
43
+ actual.class.column_names.include? key
44
+ end
45
+
46
+ match do |_actual|
47
+ associated? && has_column?
48
+ end
49
+ end
50
+
51
+ RSpec::Matchers.define :have_many do |kind, expected_options|
52
+ description do
53
+ msg = "have_many #{ kind.inspect }"
54
+ msg << ", #{ @expected_options.map { |k, v| ":#{ k } => #{ v }" }.join ',' }" if @expected_options.present?
55
+
56
+ @expected_options = nil # for some reason this appears to be cached between runs?
57
+
58
+ msg
59
+ end
60
+
61
+ failure_message do
62
+ "Expected #{ described_class } to have_many #{ kind.inspect } #{ diff }"
63
+ end
64
+
65
+ failure_message_when_negated do
66
+ "Expected #{ described_class } not to have_many #{ kind.inspect } #{ diff }"
67
+ end
68
+
69
+ def expected_options
70
+ @expected_options || {}
71
+ end
72
+
73
+ def association
74
+ @association ||= actual.class.reflect_on_association [ *expected ].first
75
+ end
76
+
77
+ def association_options
78
+ @association_options ||= association.options.reject { |k, v| k == :extend and v == [] } rescue {}
79
+ end
80
+
81
+ def diff?
82
+ # if association_options is empty and @expected_options is not, there is a problem
83
+ # if @expected_options is empty and association_options is not, there is a problem
84
+ # if neither is empty diff them, if there's a diff, there is a problem
85
+
86
+ return true if expected_options.present? ^ association_options.present?
87
+ return true if association_options.diff(expected_options).present?
88
+
89
+ false
90
+ end
91
+
92
+ def diff
93
+ return '' unless diff?
94
+
95
+ <<-MSG
96
+ \n expected options: #{ expected_options.inspect }
97
+ \n actual options: #{ association_options.inspect }
98
+ MSG
99
+ end
100
+
101
+ match do |_actual|
102
+ @expected_options = expected_options
103
+ association.try(:macro) == :has_many and not diff?
104
+ end
105
+ end
106
+
107
+ RSpec::Matchers.define :have_one do |kind, expected_options|
108
+ description do
109
+ msg = "have_many #{ kind.inspect }"
110
+ msg << ", #{ @expected_options.map { |k, v| ":#{ k } => #{ v }" }.join ',' }" if @expected_options.present?
111
+
112
+ @expected_options = nil # for some reason this appears to be cached between runs?
113
+
114
+ msg
115
+ end
116
+
117
+ failure_message do
118
+ "Expected #{ described_class } to have_many #{ kind.inspect } #{ diff }"
119
+ end
120
+
121
+ failure_message_when_negated do
122
+ "Expected #{ described_class } not to have_many #{ kind.inspect } #{ diff }"
123
+ end
124
+
125
+ def expected_options
126
+ @expected_options || {}
127
+ end
128
+
129
+ def association
130
+ @association ||= actual.class.reflect_on_association [ *expected ].first
131
+ end
132
+
133
+ def association_options
134
+ @association_options ||= association.options.reject { |k, v| k == :extend and v == [] } rescue {}
135
+ end
136
+
137
+ def diff?
138
+ # if association_options is empty and @expected_options is not, there is a problem
139
+ # if @expected_options is empty and association_options is not, there is a problem
140
+ # if neither is empty diff them, if there's a diff, there is a problem
141
+ return true if expected_options.present? ^ association_options.present?
142
+ return true if association_options.diff(expected_options).present?
143
+
144
+ false
145
+ end
146
+
147
+ def diff
148
+ return '' unless diff?
149
+
150
+ <<-MSG
151
+ \n expected options: #{ expected_options.inspect }
152
+ \n actual options: #{ association_options.inspect }
153
+ MSG
154
+ end
155
+
156
+ match do |_actual|
157
+ @expected_options = expected_options
158
+ association.try(:macro) == :has_one and not diff?
159
+ end
160
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec/expectations'
4
+
5
+ RSpec::Matchers.define :have_method do |method_name, include_all = false|
6
+ match do |actual|
7
+ @actual = actual
8
+ @include_all = include_all
9
+ @method_name = method_name
10
+
11
+ if @alias_name
12
+ responds_to_method? && responds_to_alias? && source_locations_match?
13
+ else
14
+ responds_to_method?
15
+ end
16
+ end
17
+
18
+ chain :with_alias do |alias_name|
19
+ @alias_name = alias_name
20
+ end
21
+
22
+ description do
23
+ if @alias_name
24
+ "have method ##{ @method_name } with alias ##{ @alias_name }"
25
+ else
26
+ "have method ##{ @method_name }"
27
+ end
28
+ end
29
+
30
+ failure_message do
31
+ "should have method ##{ @method_name }, but does not."
32
+ end
33
+
34
+ failure_message_when_negated do
35
+ "should not have method ##{ @method_name }, but does."
36
+ end
37
+
38
+ def responds_to_alias?
39
+ subject.respond_to? @alias_name, @include_all
40
+ end
41
+
42
+ def responds_to_method?
43
+ subject.respond_to? @method_name, @include_all
44
+ end
45
+
46
+ def source_locations_match?
47
+ location_of(@method_name) == location_of(@alias_name)
48
+ end
49
+
50
+ def location_of name
51
+ @actual.method(name).source_location
52
+ end
53
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec/expectations'
4
+
5
+ RSpec::Matchers.define :validate do |_expected|
6
+ chain(:of) { |attr| @attribute = attr }
7
+
8
+ chain(:with) do |*opts|
9
+ @options = opts.extract_options!
10
+ @options = opts if @options.empty?
11
+ end
12
+
13
+ match do |_actual|
14
+ if @attribute
15
+ validator.present? && (@options ? validator.options.should =~ @options : true)
16
+ else
17
+ callback.present? && (@options ? check_callback_options : true)
18
+ end
19
+ end
20
+
21
+ match_when_negated do |_actual|
22
+ if @attribute
23
+ raise "Negating a matcher with options doesn't really make sense!" if @options
24
+
25
+ validator.should be_nil
26
+ else
27
+ callback.should be_nil
28
+ end
29
+ end
30
+
31
+ def callback
32
+ actual.class._validate_callbacks.detect { |c| c.filter == expected }
33
+ end
34
+
35
+ def check_callback_options
36
+ opts = parse_callback_options
37
+
38
+ if @options.is_a? Hash
39
+ raise 'Cannot expect custom procs' if @options.values.any? { |v| v.is_a? Proc }
40
+
41
+ @options.each { |k, _| opts[k].should =~ Array(@options[k]) }
42
+ else
43
+ opts.keys.should =~ @options
44
+ end
45
+ end
46
+
47
+ def parse_callback_options
48
+ gem_version = Gem::Version.new ActiveModel::VERSION::STRING
49
+
50
+ options = if (Gem::Version.new('4.0.0')...Gem::Version.new('4.1.0')).cover?(gem_version)
51
+ parse_callback_options4
52
+ else
53
+ parse_callback_options_default
54
+ end
55
+
56
+ options.reject { |_, v| v.empty? }
57
+ end
58
+
59
+ def parse_callback_options_default
60
+ ifs = callback.instance_variable_get '@if'
61
+ unlesses = callback.instance_variable_get '@unless'
62
+
63
+ if ifs.first.is_a?(Proc)
64
+ proc_binding = ifs.first.binding
65
+ if proc_binding.local_variables.include? :context # rails 7.1.0+
66
+ ons = proc_binding.local_variable_get :context
67
+ ifs = ifs[1..-1]
68
+ elsif proc_binding.local_variables.include? :options
69
+ ons = proc_binding.local_variable_get(:options)[:on]
70
+ ifs = ifs[1..-1]
71
+ end
72
+ end
73
+
74
+ { if: ifs, on: Array(ons), unless: Array(unlesses) }
75
+ end
76
+
77
+ def parse_callback_options4
78
+ ifs = [ *callback.options[:if] ]
79
+ ons = [ *callback.options[:on] ]
80
+ unlesses = [ *callback.options[:unless] ]
81
+
82
+ ifs = ifs[1..-1] if ons.any?
83
+
84
+ { if: ifs, on: ons, unless: unlesses }
85
+ end
86
+
87
+ def validator
88
+ actual.class.validators_on(@attribute).detect { |v| v.kind == expected }
89
+ end
90
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Should syntax is the best syntax.
4
+ require 'rspec/matchers'
5
+
6
+ module RSpec
7
+
8
+ configure do |config|
9
+ config.expect_with(:rspec) { |spec| spec.syntax = :should }
10
+ config.mock_with(:rspec) { |spec| spec.syntax = :should }
11
+ end
12
+
13
+ module Matchers
14
+
15
+ # Force generated description to use should syntax
16
+ def self.generated_description
17
+ return nil if last_expectation_handler.nil?
18
+
19
+ "should #{ last_description }"
20
+ end
21
+
22
+ end
23
+
24
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kokishin
4
+
5
+ VERSION = '0.1.0'
6
+
7
+ end
data/lib/kokishin.rb ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'kokishin/version'
4
+
5
+ module Kokishin
6
+ end
metadata ADDED
@@ -0,0 +1,98 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kokishin
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - bm5k
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2023-10-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec-expectations
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description: A single home for various custom RSpec matchers.
28
+ email:
29
+ - 226882-BM5k@users.noreply.gitlab.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - ".ci/Gemfile.rails_4.0.0"
35
+ - ".ci/Gemfile.rails_4.0.0.lock"
36
+ - ".ci/Gemfile.rails_4.1.0"
37
+ - ".ci/Gemfile.rails_4.1.0.lock"
38
+ - ".ci/Gemfile.rails_4.2.0"
39
+ - ".ci/Gemfile.rails_4.2.0.lock"
40
+ - ".ci/Gemfile.rails_5.0.0"
41
+ - ".ci/Gemfile.rails_5.0.0.lock"
42
+ - ".ci/Gemfile.rails_5.1.0"
43
+ - ".ci/Gemfile.rails_5.1.0.lock"
44
+ - ".ci/Gemfile.rails_5.2.0"
45
+ - ".ci/Gemfile.rails_5.2.0.lock"
46
+ - ".ci/Gemfile.rails_6.0.0"
47
+ - ".ci/Gemfile.rails_6.0.0.lock"
48
+ - ".ci/Gemfile.rails_6.1.0"
49
+ - ".ci/Gemfile.rails_6.1.0.lock"
50
+ - ".ci/Gemfile.rails_7.0.0"
51
+ - ".ci/Gemfile.rails_7.0.0.lock"
52
+ - ".gitignore"
53
+ - ".gitlab-ci.yml"
54
+ - ".rspec"
55
+ - ".rubocop.yml"
56
+ - ".rubocop_todo.yml"
57
+ - Gemfile
58
+ - Gemfile.lock
59
+ - LICENSE.txt
60
+ - README.md
61
+ - Rakefile
62
+ - bin/console
63
+ - bin/setup
64
+ - kokishin.gemspec
65
+ - lib/kokishin.rb
66
+ - lib/kokishin/extensions/hash_diff.rb
67
+ - lib/kokishin/matchers/association_matcher.rb
68
+ - lib/kokishin/matchers/method_matcher.rb
69
+ - lib/kokishin/matchers/validate_matcher.rb
70
+ - lib/kokishin/should.rb
71
+ - lib/kokishin/version.rb
72
+ homepage: https://gitlab.com/devfu/kokishin
73
+ licenses:
74
+ - MIT
75
+ metadata:
76
+ homepage_uri: https://gitlab.com/devfu/kokishin
77
+ rubygems_mfa_required: 'true'
78
+ source_code_uri: https://gitlab.com/devfu/kokishin
79
+ post_install_message:
80
+ rdoc_options: []
81
+ require_paths:
82
+ - lib
83
+ required_ruby_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ required_rubygems_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ requirements: []
94
+ rubygems_version: 3.4.10
95
+ signing_key:
96
+ specification_version: 4
97
+ summary: RSpec matchers
98
+ test_files: []