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
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module RSpec
5
+ module Matchers
6
+ # @api public
7
+ # Matcher for variants in union type definitions.
8
+ class HaveVariantMatcher < BaseMatcher
9
+ BOOLEAN_CHECKS = {
10
+ deprecated: :deprecated,
11
+ partial: :partial,
12
+ }.freeze
13
+
14
+ VALUE_CHECKS = {
15
+ description: :description,
16
+ type: :type,
17
+ }.freeze
18
+
19
+ def initialize(name)
20
+ super()
21
+ @name = name
22
+ @checks = {}
23
+ end
24
+
25
+ # @api public
26
+ # Requires the variant to reference the expected type.
27
+ #
28
+ # @param type [Symbol]
29
+ # The type name.
30
+ # @return [self]
31
+ def of_type(type)
32
+ @checks[:type] = type
33
+ self
34
+ end
35
+
36
+ # @api public
37
+ # Requires the variant to be deprecated.
38
+ #
39
+ # @return [self]
40
+ def deprecated
41
+ @checks[:deprecated] = true
42
+ self
43
+ end
44
+
45
+ # @api public
46
+ # Requires the variant to be partial.
47
+ #
48
+ # @return [self]
49
+ def partial
50
+ @checks[:partial] = true
51
+ self
52
+ end
53
+
54
+ # @api public
55
+ # Requires the variant to have the expected description.
56
+ #
57
+ # @param text [String]
58
+ # The description.
59
+ # @return [self]
60
+ def with_description(text)
61
+ @checks[:description] = text
62
+ self
63
+ end
64
+
65
+ def matches?(subject)
66
+ @subject = subject
67
+ variant = find_variant(subject)
68
+ return fail_with("expected #{format_subject} to have variant #{@name.inspect}") unless variant
69
+
70
+ verify_value_checks(variant) &&
71
+ verify_boolean_checks(variant)
72
+ end
73
+
74
+ def description
75
+ parts = ["have variant #{@name.inspect}"]
76
+ parts << "of type #{@checks[:type].inspect}" if @checks.key?(:type)
77
+ boolean_parts = BOOLEAN_CHECKS.each_key.select { |key| @checks.key?(key) }.map(&:to_s)
78
+ parts << "that is #{join_sentence(boolean_parts)}" if boolean_parts.any?
79
+ parts << "with description #{@checks[:description].inspect}" if @checks.key?(:description)
80
+ parts.join(' ')
81
+ end
82
+
83
+ private
84
+
85
+ def find_variant(subject)
86
+ subject.variants.find { |variant| variant[:tag] == @name.to_s }
87
+ end
88
+
89
+ def verify_value_checks(variant)
90
+ VALUE_CHECKS.each do |key, hash_key|
91
+ next unless @checks.key?(key)
92
+
93
+ actual = variant[hash_key]
94
+ next if actual == @checks[key]
95
+
96
+ return fail_with(
97
+ "expected #{format_subject} to have variant #{@name.inspect} " \
98
+ "#{format_value_check(key, @checks[key])}, but got #{actual.inspect}",
99
+ )
100
+ end
101
+ true
102
+ end
103
+
104
+ def verify_boolean_checks(variant)
105
+ BOOLEAN_CHECKS.each do |key, hash_key|
106
+ next unless @checks.key?(key)
107
+ next if variant[hash_key]
108
+
109
+ return fail_with(
110
+ "expected #{format_subject} to have variant #{@name.inspect} that is #{key}, but it is not",
111
+ )
112
+ end
113
+ true
114
+ end
115
+
116
+ def format_value_check(key, value)
117
+ case key
118
+ when :type
119
+ "of type #{value.inspect}"
120
+ else
121
+ "with #{key} #{value.inspect}"
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,30 @@
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 has the expected version.
8
+ class HaveVersionMatcher < BaseMatcher
9
+ def initialize(text)
10
+ super()
11
+ @text = text
12
+ end
13
+
14
+ def matches?(subject)
15
+ @subject = subject
16
+ actual = subject.version
17
+ return true if actual == @text
18
+
19
+ fail_with(
20
+ "expected #{format_subject} to have version #{@text.inspect}, but got #{actual.inspect}",
21
+ )
22
+ end
23
+
24
+ def description
25
+ "have version #{@text.inspect}"
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module RSpec
5
+ module Matchers
6
+ # @api public
7
+ # Matcher for verifying an action has no response content.
8
+ class NoContentMatcher < BaseMatcher
9
+ def matches?(subject)
10
+ @subject = subject
11
+ return true if subject.response.no_content?
12
+
13
+ fail_with("expected #{format_subject} to be no content, but it has content")
14
+ end
15
+
16
+ def description
17
+ 'be no content'
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module RSpec
5
+ module Matchers
6
+ class ParamWrapper
7
+ attr_reader :name,
8
+ :params
9
+
10
+ def initialize(name, param)
11
+ @name = name
12
+ @params = param[:params] || {}
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,388 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module RSpec
5
+ # @api public
6
+ module Matchers
7
+ class << self
8
+ def included(base)
9
+ base.extend(ClassMethods)
10
+ end
11
+ end
12
+
13
+ module ClassMethods
14
+ def describe_action(action_name, &block)
15
+ context "action #{action_name.inspect}" do
16
+ subject { super().actions[action_name] }
17
+ class_exec(&block)
18
+ end
19
+ end
20
+
21
+ def describe_body(&block)
22
+ context 'body' do
23
+ subject { super().body }
24
+ class_exec(&block)
25
+ end
26
+ end
27
+
28
+ def describe_query(&block)
29
+ context 'query' do
30
+ subject { super().query }
31
+ class_exec(&block)
32
+ end
33
+ end
34
+
35
+ def describe_response(&block)
36
+ context 'response' do
37
+ subject { super().response }
38
+ class_exec(&block)
39
+ end
40
+ end
41
+
42
+ def describe_request(&block)
43
+ context 'request' do
44
+ subject { super().request }
45
+ class_exec(&block)
46
+ end
47
+ end
48
+
49
+ def describe_param(param_name, &block)
50
+ context "param #{param_name.inspect}" do
51
+ subject do
52
+ param = super().params[param_name]
53
+ ParamWrapper.new(param_name, param)
54
+ end
55
+ class_exec(&block)
56
+ end
57
+ end
58
+
59
+ def describe_info(&block)
60
+ context 'info' do
61
+ subject { super().info }
62
+ class_exec(&block)
63
+ end
64
+ end
65
+
66
+ def describe_object(type_name, &block)
67
+ context "object #{type_name.inspect}" do
68
+ subject do
69
+ parent = super()
70
+ if parent.respond_to?(:type_registry)
71
+ parent.type_registry[type_name]
72
+ elsif parent.respond_to?(:resolve_custom_type)
73
+ parent.resolve_custom_type(type_name)
74
+ end
75
+ end
76
+ class_exec(&block)
77
+ end
78
+ end
79
+
80
+ def describe_union(type_name, &block)
81
+ context "union #{type_name.inspect}" do
82
+ subject do
83
+ parent = super()
84
+ if parent.respond_to?(:type_registry)
85
+ parent.type_registry[type_name]
86
+ elsif parent.respond_to?(:resolve_custom_type)
87
+ parent.resolve_custom_type(type_name)
88
+ end
89
+ end
90
+ class_exec(&block)
91
+ end
92
+ end
93
+ end
94
+
95
+ # @api public
96
+ # Verifies a representation has a specific attribute.
97
+ #
98
+ # @param name [Symbol]
99
+ # The attribute name.
100
+ # @return [HaveAttributeMatcher]
101
+ def have_attribute(name)
102
+ HaveAttributeMatcher.new(name)
103
+ end
104
+
105
+ # @api public
106
+ # Verifies a representation has a specific association.
107
+ #
108
+ # @param name [Symbol]
109
+ # The association name.
110
+ # @return [HaveAssociationMatcher]
111
+ def have_association(name)
112
+ HaveAssociationMatcher.new(name)
113
+ end
114
+
115
+ # @api public
116
+ # Verifies an API defines a specific enum.
117
+ #
118
+ # @param name [Symbol]
119
+ # The enum name.
120
+ # @return [DefineEnumMatcher]
121
+ def define_enum(name)
122
+ DefineEnumMatcher.new(name)
123
+ end
124
+
125
+ # @api public
126
+ # Verifies a param exists with the expected properties.
127
+ #
128
+ # @param name [Symbol]
129
+ # The param name.
130
+ # @return [HaveParamMatcher]
131
+ def have_param(name)
132
+ HaveParamMatcher.new(name)
133
+ end
134
+
135
+ # @api public
136
+ # Verifies a union has a specific variant.
137
+ #
138
+ # @param name [Symbol]
139
+ # The variant name.
140
+ # @return [HaveVariantMatcher]
141
+ def have_variant(name)
142
+ HaveVariantMatcher.new(name)
143
+ end
144
+
145
+ # @api public
146
+ # Verifies an action has no response content.
147
+ #
148
+ # @return [NoContentMatcher]
149
+ def be_no_content
150
+ NoContentMatcher.new
151
+ end
152
+
153
+ # @api public
154
+ # Verifies an action has the expected summary.
155
+ #
156
+ # @param text [String]
157
+ # The summary.
158
+ # @return [HaveSummaryMatcher]
159
+ def have_summary(text)
160
+ HaveSummaryMatcher.new(text)
161
+ end
162
+
163
+ # @api public
164
+ # Verifies a subject has the expected description.
165
+ #
166
+ # @param text [String]
167
+ # The description.
168
+ # @return [HaveDescriptionMatcher]
169
+ def have_description(text)
170
+ HaveDescriptionMatcher.new(text)
171
+ end
172
+
173
+ # @api public
174
+ # Verifies a union has the expected discriminator.
175
+ #
176
+ # @param field [Symbol]
177
+ # The discriminator field.
178
+ # @return [HaveDiscriminatorMatcher]
179
+ def have_discriminator(field)
180
+ HaveDiscriminatorMatcher.new(field)
181
+ end
182
+
183
+ # @api public
184
+ # Verifies a subject has the expected example.
185
+ #
186
+ # @param value [Object]
187
+ # The example value.
188
+ # @return [HaveExampleMatcher]
189
+ def have_example(value)
190
+ HaveExampleMatcher.new(value)
191
+ end
192
+
193
+ # @api public
194
+ # Verifies an action has the expected tags.
195
+ #
196
+ # @param tags [Array<Symbol>]
197
+ # The tags.
198
+ # @return [HaveTagsMatcher]
199
+ def have_tags(*tags)
200
+ HaveTagsMatcher.new(tags.flatten)
201
+ end
202
+
203
+ # @api public
204
+ # Verifies an action has the expected raises.
205
+ #
206
+ # @param codes [Array<Symbol>]
207
+ # The error codes.
208
+ # @return [HaveRaisesMatcher]
209
+ def have_raises(*codes)
210
+ HaveRaisesMatcher.new(codes.flatten)
211
+ end
212
+
213
+ # @api public
214
+ # Verifies an action has the expected operation ID.
215
+ #
216
+ # @param id [String]
217
+ # The operation ID.
218
+ # @return [HaveOperationIdMatcher]
219
+ def have_operation_id(id)
220
+ HaveOperationIdMatcher.new(id)
221
+ end
222
+
223
+ # @api public
224
+ # Verifies an API has a specific resource.
225
+ #
226
+ # @param name [Symbol]
227
+ # The resource name.
228
+ # @return [HaveResourceMatcher]
229
+ def have_resource(name)
230
+ HaveResourceMatcher.new(name)
231
+ end
232
+
233
+ # @api public
234
+ # Verifies a contract has the expected representation.
235
+ #
236
+ # @param klass [Class]
237
+ # The representation class.
238
+ # @return [HaveRepresentationMatcher]
239
+ def have_representation(klass)
240
+ HaveRepresentationMatcher.new(klass)
241
+ end
242
+
243
+ # @api public
244
+ # Verifies a contract has the expected identifier.
245
+ #
246
+ # @param value [Symbol]
247
+ # The identifier.
248
+ # @return [HaveIdentifierMatcher]
249
+ def have_identifier(value)
250
+ HaveIdentifierMatcher.new(value)
251
+ end
252
+
253
+ # @api public
254
+ # Verifies a representation has the expected model.
255
+ #
256
+ # @param klass [Class]
257
+ # The model class.
258
+ # @return [HaveModelMatcher]
259
+ def have_model(klass)
260
+ HaveModelMatcher.new(klass)
261
+ end
262
+
263
+ # @api public
264
+ # Verifies a representation has the expected root key.
265
+ #
266
+ # @param singular [Symbol]
267
+ # The singular root key.
268
+ # @param plural [Symbol]
269
+ # The plural root key.
270
+ # @return [HaveRootMatcher]
271
+ def have_root(singular, plural)
272
+ HaveRootMatcher.new(singular, plural)
273
+ end
274
+
275
+ # @api public
276
+ # Verifies a representation has the expected type name.
277
+ #
278
+ # @param value [String]
279
+ # The type name.
280
+ # @return [HaveTypeNameMatcher]
281
+ def have_type_name(value)
282
+ HaveTypeNameMatcher.new(value)
283
+ end
284
+
285
+ # @api public
286
+ # Verifies an API has the expected key format.
287
+ #
288
+ # @param format [Symbol] [:camel, :kebab, :keep, :pascal, :underscore]
289
+ # The key format.
290
+ # @return [HaveKeyFormatMatcher]
291
+ def have_key_format(format)
292
+ HaveKeyFormatMatcher.new(format)
293
+ end
294
+
295
+ # @api public
296
+ # Verifies an API has the expected path format.
297
+ #
298
+ # @param format [Symbol] [:camel, :kebab, :keep, :pascal, :underscore]
299
+ # The path format.
300
+ # @return [HavePathFormatMatcher]
301
+ def have_path_format(format)
302
+ HavePathFormatMatcher.new(format)
303
+ end
304
+
305
+ # @api public
306
+ # Verifies an API info has the expected title.
307
+ #
308
+ # @param text [String]
309
+ # The title.
310
+ # @return [HaveTitleMatcher]
311
+ def have_title(text)
312
+ HaveTitleMatcher.new(text)
313
+ end
314
+
315
+ # @api public
316
+ # Verifies an API info has the expected version.
317
+ #
318
+ # @param text [String]
319
+ # The version.
320
+ # @return [HaveVersionMatcher]
321
+ def have_version(text)
322
+ HaveVersionMatcher.new(text)
323
+ end
324
+
325
+ # @api public
326
+ # Verifies an API info has the expected terms of service.
327
+ #
328
+ # @param url [String]
329
+ # The terms of service URL.
330
+ # @return [HaveTermsOfServiceMatcher]
331
+ def have_terms_of_service(url)
332
+ HaveTermsOfServiceMatcher.new(url)
333
+ end
334
+
335
+ # @api public
336
+ # Verifies an API info defines a contact.
337
+ #
338
+ # @param name [String]
339
+ # The contact name.
340
+ # @return [DefineContactMatcher]
341
+ def define_contact(name)
342
+ DefineContactMatcher.new(name)
343
+ end
344
+
345
+ # @api public
346
+ # Verifies an API info defines a license.
347
+ #
348
+ # @param name [String]
349
+ # The license name.
350
+ # @return [DefineLicenseMatcher]
351
+ def define_license(name)
352
+ DefineLicenseMatcher.new(name)
353
+ end
354
+
355
+ # @api public
356
+ # Verifies an API info defines a server.
357
+ #
358
+ # @param url [String]
359
+ # The server URL.
360
+ # @return [DefineServerMatcher]
361
+ def define_server(url)
362
+ DefineServerMatcher.new(url)
363
+ end
364
+
365
+ # @api public
366
+ # Verifies an API has a specific export enabled.
367
+ #
368
+ # @param name [Symbol] [:openapi, :sorbus, :typescript, :zod]
369
+ # The export name.
370
+ # @return [HaveExportMatcher]
371
+ def have_export(name)
372
+ HaveExportMatcher.new(name)
373
+ end
374
+
375
+ # @api public
376
+ # Verifies a contract imports another contract.
377
+ #
378
+ # @param klass [Class]
379
+ # The imported contract class.
380
+ # @param as [Symbol]
381
+ # The import alias.
382
+ # @return [HaveImportMatcher]
383
+ def have_import(klass, as:)
384
+ HaveImportMatcher.new(klass, as:)
385
+ end
386
+ end
387
+ end
388
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module RSpec
5
+ VERSION = '0.1.1'
6
+ end
7
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zeitwerk'
4
+ require_relative 'rspec/version'
5
+
6
+ # @api public
7
+ module Apiwork
8
+ # @api public
9
+ module RSpec
10
+ end
11
+ end
12
+
13
+ loader = Zeitwerk::Loader.new
14
+ loader.tag = 'apiwork-rspec'
15
+ loader.push_dir("#{__dir__}/..", namespace: Object)
16
+
17
+ loader.inflector.inflect(
18
+ 'rspec' => 'RSpec',
19
+ )
20
+
21
+ loader.ignore(__FILE__)
22
+ loader.ignore("#{__dir__}/../apiwork-rspec.rb")
23
+ loader.ignore("#{__dir__}/rspec/version.rb")
24
+
25
+ loader.setup
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'apiwork/rspec'