apiwork-rspec 0.1.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.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/Rakefile +10 -0
  4. data/lib/apiwork/rspec/matchers/base_matcher.rb +44 -0
  5. data/lib/apiwork/rspec/matchers/define_contact_matcher.rb +80 -0
  6. data/lib/apiwork/rspec/matchers/define_enum_matcher.rb +139 -0
  7. data/lib/apiwork/rspec/matchers/define_license_matcher.rb +53 -0
  8. data/lib/apiwork/rspec/matchers/define_server_matcher.rb +50 -0
  9. data/lib/apiwork/rspec/matchers/have_association_matcher.rb +247 -0
  10. data/lib/apiwork/rspec/matchers/have_attribute_matcher.rb +283 -0
  11. data/lib/apiwork/rspec/matchers/have_description_matcher.rb +30 -0
  12. data/lib/apiwork/rspec/matchers/have_discriminator_matcher.rb +30 -0
  13. data/lib/apiwork/rspec/matchers/have_example_matcher.rb +30 -0
  14. data/lib/apiwork/rspec/matchers/have_export_matcher.rb +29 -0
  15. data/lib/apiwork/rspec/matchers/have_identifier_matcher.rb +30 -0
  16. data/lib/apiwork/rspec/matchers/have_import_matcher.rb +34 -0
  17. data/lib/apiwork/rspec/matchers/have_key_format_matcher.rb +30 -0
  18. data/lib/apiwork/rspec/matchers/have_model_matcher.rb +30 -0
  19. data/lib/apiwork/rspec/matchers/have_operation_id_matcher.rb +30 -0
  20. data/lib/apiwork/rspec/matchers/have_param_matcher.rb +222 -0
  21. data/lib/apiwork/rspec/matchers/have_path_format_matcher.rb +30 -0
  22. data/lib/apiwork/rspec/matchers/have_raises_matcher.rb +30 -0
  23. data/lib/apiwork/rspec/matchers/have_representation_matcher.rb +30 -0
  24. data/lib/apiwork/rspec/matchers/have_resource_matcher.rb +142 -0
  25. data/lib/apiwork/rspec/matchers/have_root_matcher.rb +35 -0
  26. data/lib/apiwork/rspec/matchers/have_summary_matcher.rb +30 -0
  27. data/lib/apiwork/rspec/matchers/have_tags_matcher.rb +30 -0
  28. data/lib/apiwork/rspec/matchers/have_terms_of_service_matcher.rb +30 -0
  29. data/lib/apiwork/rspec/matchers/have_title_matcher.rb +30 -0
  30. data/lib/apiwork/rspec/matchers/have_type_name_matcher.rb +30 -0
  31. data/lib/apiwork/rspec/matchers/have_variant_matcher.rb +127 -0
  32. data/lib/apiwork/rspec/matchers/have_version_matcher.rb +30 -0
  33. data/lib/apiwork/rspec/matchers/no_content_matcher.rb +22 -0
  34. data/lib/apiwork/rspec/matchers/param_wrapper.rb +17 -0
  35. data/lib/apiwork/rspec/matchers.rb +388 -0
  36. data/lib/apiwork/rspec/version.rb +7 -0
  37. data/lib/apiwork/rspec.rb +25 -0
  38. data/lib/apiwork-rspec.rb +3 -0
  39. metadata +154 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 83aafbeb0b15f739a2b538f43f386322d94631de471addc08f1b69196a38e254
4
+ data.tar.gz: 34000a080944fe635de1e5bd6d4b07021e8f6c9f8d1d7feb86c496373e3f05e3
5
+ SHA512:
6
+ metadata.gz: 186e6a9a8e19a7f5507dd9152e9e3d22284faecf4b97fbcf58d47b03d52e2ce3d6d143084d141786f444eb7b8ce2cfb0a9e75f122805b54052c9096c2761760e
7
+ data.tar.gz: e38d9d193b0b5ddf9b3df0282502402fd4da58caaddde442cef8227642330053ad9edc9140bb1aefe7f8f73ec7f726c2a14893ec5c2c5a3c1c80e6cf6f686789
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2026 skiftle
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rubocop/rake_task'
5
+ require 'rspec/core/rake_task'
6
+
7
+ RuboCop::RakeTask.new
8
+ RSpec::Core::RakeTask.new(:spec)
9
+
10
+ task default: %i[rubocop spec]
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module RSpec
5
+ module Matchers
6
+ # @api public
7
+ # Base class for matchers.
8
+ class BaseMatcher
9
+ include ::RSpec::Matchers::Composable
10
+
11
+ attr_reader :failure_message
12
+
13
+ def initialize
14
+ @failure_message = nil
15
+ end
16
+
17
+ def does_not_match?(subject)
18
+ !matches?(subject)
19
+ end
20
+
21
+ def failure_message_when_negated
22
+ "expected #{format_subject} not to #{description}"
23
+ end
24
+
25
+ private
26
+
27
+ def format_subject
28
+ @subject.respond_to?(:name) ? @subject.name : @subject.to_s
29
+ end
30
+
31
+ def fail_with(message)
32
+ @failure_message = message
33
+ false
34
+ end
35
+
36
+ def join_sentence(words)
37
+ return words.join(' and ') if words.size <= 2
38
+
39
+ "#{words[0..-2].join(', ')}, and #{words[-1]}"
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module RSpec
5
+ module Matchers
6
+ # @api public
7
+ # Matcher for verifying an API info defines a contact.
8
+ class DefineContactMatcher < BaseMatcher
9
+ VALUE_CHECKS = {
10
+ email: :email,
11
+ url: :url,
12
+ }.freeze
13
+
14
+ def initialize(name)
15
+ super()
16
+ @name = name
17
+ @checks = {}
18
+ end
19
+
20
+ # @api public
21
+ # Requires the contact to have the expected email.
22
+ #
23
+ # @param email [String]
24
+ # The email.
25
+ # @return [self]
26
+ def with_email(email)
27
+ @checks[:email] = email
28
+ self
29
+ end
30
+
31
+ # @api public
32
+ # Requires the contact to have the expected URL.
33
+ #
34
+ # @param url [String]
35
+ # The URL.
36
+ # @return [self]
37
+ def with_url(url)
38
+ @checks[:url] = url
39
+ self
40
+ end
41
+
42
+ def matches?(subject)
43
+ @subject = subject
44
+ contact = subject.contact
45
+ return fail_with("expected #{format_subject} to define contact #{@name.inspect}") unless contact
46
+ unless contact.name == @name
47
+ return fail_with("expected #{format_subject} to define contact #{@name.inspect}, but got #{contact.name.inspect}")
48
+ end
49
+
50
+ verify_value_checks(contact)
51
+ end
52
+
53
+ def description
54
+ parts = ["define contact #{@name.inspect}"]
55
+ VALUE_CHECKS.each_key do |key|
56
+ parts << "with #{key} #{@checks[key].inspect}" if @checks.key?(key)
57
+ end
58
+ parts.join(' ')
59
+ end
60
+
61
+ private
62
+
63
+ def verify_value_checks(contact)
64
+ VALUE_CHECKS.each do |key, method|
65
+ next unless @checks.key?(key)
66
+
67
+ actual = contact.public_send(method)
68
+ next if actual == @checks[key]
69
+
70
+ return fail_with(
71
+ "expected #{format_subject} to define contact #{@name.inspect} " \
72
+ "with #{key} #{@checks[key].inspect}, but got #{actual.inspect}",
73
+ )
74
+ end
75
+ true
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module RSpec
5
+ module Matchers
6
+ # @api public
7
+ # Matcher for enum definitions.
8
+ class DefineEnumMatcher < BaseMatcher
9
+ VALUE_CHECKS = {
10
+ description: :description,
11
+ example: :example,
12
+ }.freeze
13
+
14
+ def initialize(name)
15
+ super()
16
+ @name = name
17
+ @expected_values = nil
18
+ @checks = {}
19
+ end
20
+
21
+ # @api public
22
+ # Requires the enum to have the expected values.
23
+ #
24
+ # @param values [Array]
25
+ # The enum values.
26
+ # @return [self]
27
+ def with_values(values)
28
+ @expected_values = values
29
+ self
30
+ end
31
+
32
+ # @api public
33
+ # Requires the enum to be deprecated.
34
+ #
35
+ # @return [self]
36
+ def deprecated
37
+ @checks[:deprecated] = true
38
+ self
39
+ end
40
+
41
+ # @api public
42
+ # Requires the enum to have the expected description.
43
+ #
44
+ # @param text [String]
45
+ # The description.
46
+ # @return [self]
47
+ def with_description(text)
48
+ @checks[:description] = text
49
+ self
50
+ end
51
+
52
+ # @api public
53
+ # Requires the enum to have the expected example.
54
+ #
55
+ # @param value [Object]
56
+ # The example.
57
+ # @return [self]
58
+ def with_example(value)
59
+ @checks[:example] = value
60
+ self
61
+ end
62
+
63
+ def matches?(subject)
64
+ @subject = subject
65
+ values = subject.enum_values(@name)
66
+ return fail_with("expected #{format_subject} to define enum #{@name.inspect}") unless values
67
+
68
+ verify_values(values) &&
69
+ verify_metadata(subject)
70
+ end
71
+
72
+ def description
73
+ parts = ["define enum #{@name.inspect}"]
74
+ parts << 'that is deprecated' if @checks.key?(:deprecated)
75
+ parts << "with values #{@expected_values.inspect}" if @expected_values
76
+ VALUE_CHECKS.each_key do |key|
77
+ parts << "with #{key} #{@checks[key].inspect}" if @checks.key?(key)
78
+ end
79
+ parts.join(' ')
80
+ end
81
+
82
+ private
83
+
84
+ def verify_values(values)
85
+ return true unless @expected_values
86
+ return true if values == @expected_values
87
+
88
+ fail_with(
89
+ "expected #{format_subject} to define enum #{@name.inspect} " \
90
+ "with values #{@expected_values.inspect}, but got #{values.inspect}",
91
+ )
92
+ end
93
+
94
+ def verify_metadata(subject)
95
+ definition = find_enum_definition(subject)
96
+ return true unless definition_required?
97
+ return fail_with("expected #{format_subject} to support enum metadata (use API class)") unless definition
98
+
99
+ verify_deprecated(definition) &&
100
+ verify_value_checks(definition)
101
+ end
102
+
103
+ def find_enum_definition(subject)
104
+ return unless subject.respond_to?(:enum_registry)
105
+
106
+ subject.enum_registry[@name]
107
+ end
108
+
109
+ def definition_required?
110
+ @checks.key?(:deprecated) || @checks.key?(:description) || @checks.key?(:example)
111
+ end
112
+
113
+ def verify_deprecated(definition)
114
+ return true unless @checks.key?(:deprecated)
115
+ return true if definition.deprecated?
116
+
117
+ fail_with(
118
+ "expected #{format_subject} to define enum #{@name.inspect} that is deprecated, but it is not",
119
+ )
120
+ end
121
+
122
+ def verify_value_checks(definition)
123
+ VALUE_CHECKS.each do |key, method|
124
+ next unless @checks.key?(key)
125
+
126
+ actual = definition.public_send(method)
127
+ next if actual == @checks[key]
128
+
129
+ return fail_with(
130
+ "expected #{format_subject} to define enum #{@name.inspect} " \
131
+ "with #{key} #{@checks[key].inspect}, but got #{actual.inspect}",
132
+ )
133
+ end
134
+ true
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module RSpec
5
+ module Matchers
6
+ # @api public
7
+ # Matcher for verifying an API info defines a license.
8
+ class DefineLicenseMatcher < BaseMatcher
9
+ def initialize(name)
10
+ super()
11
+ @name = name
12
+ @checks = {}
13
+ end
14
+
15
+ # @api public
16
+ # Requires the license to have the expected URL.
17
+ #
18
+ # @param url [String]
19
+ # The URL.
20
+ # @return [self]
21
+ def with_url(url)
22
+ @checks[:url] = url
23
+ self
24
+ end
25
+
26
+ def matches?(subject)
27
+ @subject = subject
28
+ license = subject.license
29
+ return fail_with("expected #{format_subject} to define license #{@name.inspect}") unless license
30
+ unless license.name == @name
31
+ return fail_with("expected #{format_subject} to define license #{@name.inspect}, but got #{license.name.inspect}")
32
+ end
33
+
34
+ return true unless @checks.key?(:url)
35
+
36
+ actual = license.url
37
+ return true if actual == @checks[:url]
38
+
39
+ fail_with(
40
+ "expected #{format_subject} to define license #{@name.inspect} " \
41
+ "with url #{@checks[:url].inspect}, but got #{actual.inspect}",
42
+ )
43
+ end
44
+
45
+ def description
46
+ parts = ["define license #{@name.inspect}"]
47
+ parts << "with url #{@checks[:url].inspect}" if @checks.key?(:url)
48
+ parts.join(' ')
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module RSpec
5
+ module Matchers
6
+ # @api public
7
+ # Matcher for verifying an API info defines a server.
8
+ class DefineServerMatcher < BaseMatcher
9
+ def initialize(url)
10
+ super()
11
+ @url = url
12
+ @checks = {}
13
+ end
14
+
15
+ # @api public
16
+ # Requires the server to have the expected description.
17
+ #
18
+ # @param text [String]
19
+ # The description.
20
+ # @return [self]
21
+ def with_description(text)
22
+ @checks[:description] = text
23
+ self
24
+ end
25
+
26
+ def matches?(subject)
27
+ @subject = subject
28
+ server = subject.servers.find { |s| s.url == @url }
29
+ return fail_with("expected #{format_subject} to define server #{@url.inspect}") unless server
30
+
31
+ return true unless @checks.key?(:description)
32
+
33
+ actual = server.description
34
+ return true if actual == @checks[:description]
35
+
36
+ fail_with(
37
+ "expected #{format_subject} to define server #{@url.inspect} " \
38
+ "with description #{@checks[:description].inspect}, but got #{actual.inspect}",
39
+ )
40
+ end
41
+
42
+ def description
43
+ parts = ["define server #{@url.inspect}"]
44
+ parts << "with description #{@checks[:description].inspect}" if @checks.key?(:description)
45
+ parts.join(' ')
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,247 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module RSpec
5
+ module Matchers
6
+ # @api public
7
+ # Matcher for representation associations.
8
+ class HaveAssociationMatcher < BaseMatcher
9
+ BOOLEAN_CHECKS = {
10
+ allow_destroy: :allow_destroy,
11
+ deprecated: :deprecated?,
12
+ filterable: :filterable?,
13
+ nullable: :nullable?,
14
+ polymorphic: :polymorphic?,
15
+ sortable: :sortable?,
16
+ }.freeze
17
+
18
+ VALUE_CHECKS = {
19
+ description: :description,
20
+ example: :example,
21
+ include: :include,
22
+ representation: :representation_class,
23
+ }.freeze
24
+
25
+ def initialize(name)
26
+ super()
27
+ @name = name
28
+ @checks = {}
29
+ end
30
+
31
+ # @api public
32
+ # Requires the association to have the expected type.
33
+ #
34
+ # @param type [Symbol] [:belongs_to, :has_many, :has_one]
35
+ # The type.
36
+ # @return [self]
37
+ def of_type(type)
38
+ @checks[:type] = type
39
+ self
40
+ end
41
+
42
+ # @api public
43
+ # Requires the association to be writable.
44
+ #
45
+ # @param action [Symbol, Boolean] (true) [Symbol: :create, :update]
46
+ # The action to check writability for, or `true` for any action.
47
+ # @return [self]
48
+ def writable(action = true)
49
+ @checks[:writable] = action
50
+ self
51
+ end
52
+
53
+ # @api public
54
+ # Requires the association to allow destroy.
55
+ #
56
+ # @return [self]
57
+ def allow_destroy
58
+ @checks[:allow_destroy] = true
59
+ self
60
+ end
61
+
62
+ # @api public
63
+ # Requires the association to be deprecated.
64
+ #
65
+ # @return [self]
66
+ def deprecated
67
+ @checks[:deprecated] = true
68
+ self
69
+ end
70
+
71
+ # @api public
72
+ # Requires the association to be filterable.
73
+ #
74
+ # @return [self]
75
+ def filterable
76
+ @checks[:filterable] = true
77
+ self
78
+ end
79
+
80
+ # @api public
81
+ # Requires the association to be nullable.
82
+ #
83
+ # @return [self]
84
+ def nullable
85
+ @checks[:nullable] = true
86
+ self
87
+ end
88
+
89
+ # @api public
90
+ # Requires the association to be polymorphic.
91
+ #
92
+ # @return [self]
93
+ def polymorphic
94
+ @checks[:polymorphic] = true
95
+ self
96
+ end
97
+
98
+ # @api public
99
+ # Requires the association to be sortable.
100
+ #
101
+ # @return [self]
102
+ def sortable
103
+ @checks[:sortable] = true
104
+ self
105
+ end
106
+
107
+ # @api public
108
+ # Requires the association to have the expected inclusion strategy.
109
+ #
110
+ # @param value [Symbol] [:always, :optional]
111
+ # The inclusion strategy.
112
+ # @return [self]
113
+ def with_include(value)
114
+ @checks[:include] = value
115
+ self
116
+ end
117
+
118
+ # @api public
119
+ # Requires the association to have the expected representation class.
120
+ #
121
+ # @param klass [Class]
122
+ # The representation class.
123
+ # @return [self]
124
+ def with_representation(klass)
125
+ @checks[:representation] = klass
126
+ self
127
+ end
128
+
129
+ # @api public
130
+ # Requires the association to have the expected description.
131
+ #
132
+ # @param text [String]
133
+ # The description.
134
+ # @return [self]
135
+ def with_description(text)
136
+ @checks[:description] = text
137
+ self
138
+ end
139
+
140
+ # @api public
141
+ # Requires the association to have the expected example.
142
+ #
143
+ # @param value [Object]
144
+ # The example.
145
+ # @return [self]
146
+ def with_example(value)
147
+ @checks[:example] = value
148
+ self
149
+ end
150
+
151
+ def matches?(subject)
152
+ @subject = subject
153
+ association = subject.associations[@name]
154
+ return fail_with("expected #{format_subject} to have association #{@name.inspect}") unless association
155
+
156
+ verify_all(association)
157
+ end
158
+
159
+ def description
160
+ parts = ["have association #{@name.inspect}"]
161
+ parts << "of type #{@checks[:type].inspect}" if @checks.key?(:type)
162
+ boolean_parts = boolean_description_parts
163
+ parts << "that is #{join_sentence(boolean_parts)}" if boolean_parts.any?
164
+ VALUE_CHECKS.each_key do |key|
165
+ parts << "with #{key} #{@checks[key].inspect}" if @checks.key?(key)
166
+ end
167
+ parts.join(' ')
168
+ end
169
+
170
+ private
171
+
172
+ def verify_all(association)
173
+ verify_type(association) &&
174
+ verify_boolean_checks(association) &&
175
+ verify_writable(association) &&
176
+ verify_value_checks(association)
177
+ end
178
+
179
+ def verify_type(association)
180
+ return true unless @checks.key?(:type)
181
+ return true if association.type == @checks[:type]
182
+
183
+ fail_with(
184
+ "expected #{format_subject} to have association #{@name.inspect} " \
185
+ "of type #{@checks[:type].inspect}, but got #{association.type.inspect}",
186
+ )
187
+ end
188
+
189
+ def verify_boolean_checks(association)
190
+ BOOLEAN_CHECKS.each do |key, method|
191
+ next unless @checks.key?(key)
192
+ next if association.public_send(method)
193
+
194
+ return fail_with(
195
+ "expected #{format_subject} to have association #{@name.inspect} that is #{key}, but it is not",
196
+ )
197
+ end
198
+ true
199
+ end
200
+
201
+ def verify_writable(association)
202
+ return true unless @checks.key?(:writable)
203
+
204
+ expected = @checks[:writable]
205
+ if expected == true
206
+ return true if association.writable?
207
+
208
+ return fail_with(
209
+ "expected #{format_subject} to have association #{@name.inspect} that is writable, but it is not",
210
+ )
211
+ end
212
+
213
+ return true if association.writable_for?(expected)
214
+
215
+ fail_with(
216
+ "expected #{format_subject} to have association #{@name.inspect} " \
217
+ "that is writable for #{expected.inspect}, but it is not",
218
+ )
219
+ end
220
+
221
+ def verify_value_checks(association)
222
+ VALUE_CHECKS.each do |key, method|
223
+ next unless @checks.key?(key)
224
+
225
+ actual = association.public_send(method)
226
+ next if actual == @checks[key]
227
+
228
+ return fail_with(
229
+ "expected #{format_subject} to have association #{@name.inspect} " \
230
+ "with #{key} #{@checks[key].inspect}, but got #{actual.inspect}",
231
+ )
232
+ end
233
+ true
234
+ end
235
+
236
+ def boolean_description_parts
237
+ parts = []
238
+ BOOLEAN_CHECKS.each_key { |key| parts << key.to_s if @checks.key?(key) }
239
+ if @checks.key?(:writable)
240
+ parts << (@checks[:writable] == true ? 'writable' : "writable for #{@checks[:writable].inspect}")
241
+ end
242
+ parts
243
+ end
244
+ end
245
+ end
246
+ end
247
+ end