kokishin 0.1.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.
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: []