zenspec 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.
@@ -0,0 +1,554 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rspec/expectations"
4
+
5
+ module Zenspec
6
+ module Matchers
7
+ module GraphQLTypeMatchers
8
+ # Matcher for checking if a GraphQL type has a specific field with type and arguments
9
+ #
10
+ # @example
11
+ # expect(UserType).to have_field(:id).of_type("ID!")
12
+ # expect(UserType).to have_field(:name).of_type("String!")
13
+ # expect(UserType).to have_field(:posts).of_type("[Post!]!")
14
+ # expect(UserType).to have_field(:posts).with_argument(:limit, "Int")
15
+ #
16
+ RSpec::Matchers.define :have_field do |field_name|
17
+ match do |type|
18
+ @field_name = field_name.to_s
19
+ @type = type
20
+
21
+ # Get the field from the type
22
+ @field = get_field(type, @field_name)
23
+ return false unless @field
24
+
25
+ # Check field type if specified
26
+ if @expected_type
27
+ actual_type = field_type_string(@field)
28
+ return false unless actual_type == @expected_type
29
+ end
30
+
31
+ # Check arguments if specified
32
+ if @expected_arguments && @expected_arguments.any?
33
+ @expected_arguments.all? do |arg_name, arg_type|
34
+ check_argument(arg_name, arg_type)
35
+ end
36
+ else
37
+ true
38
+ end
39
+ end
40
+
41
+ chain :of_type do |expected_type|
42
+ @expected_type = expected_type
43
+ end
44
+
45
+ chain :with_argument do |arg_name, arg_type|
46
+ @expected_arguments ||= {}
47
+ @expected_arguments[arg_name.to_s] = arg_type
48
+ end
49
+
50
+ failure_message do
51
+ if !@field
52
+ "expected #{type_name(@type)} to have field #{@field_name.inspect}, but it does not"
53
+ elsif @expected_type && field_type_string(@field) != @expected_type
54
+ "expected field #{@field_name.inspect} to have type #{@expected_type.inspect}, " \
55
+ "got #{field_type_string(@field).inspect}"
56
+ elsif @expected_arguments
57
+ missing_or_wrong = find_mismatched_arguments
58
+ "expected field #{@field_name.inspect} to have arguments #{@expected_arguments.inspect}, " \
59
+ "but #{missing_or_wrong.inspect} did not match"
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def get_field(type, field_name)
66
+ if type.respond_to?(:fields)
67
+ type.fields[field_name]
68
+ elsif type.respond_to?(:get_field)
69
+ type.get_field(field_name)
70
+ end
71
+ end
72
+
73
+ def field_type_string(field)
74
+ return nil unless field
75
+
76
+ type = field.type
77
+ type_to_string(type)
78
+ end
79
+
80
+ def type_to_string(type)
81
+ case type
82
+ when GraphQL::Schema::NonNull
83
+ "#{type_to_string(type.of_type)}!"
84
+ when GraphQL::Schema::List
85
+ "[#{type_to_string(type.of_type)}]"
86
+ when GraphQL::Schema::Member
87
+ type.graphql_name
88
+ else
89
+ # Handle wrapped types from graphql-ruby
90
+ if type.respond_to?(:unwrap)
91
+ unwrapped = type.unwrap
92
+ base_name = unwrapped.respond_to?(:graphql_name) ? unwrapped.graphql_name : unwrapped.to_s
93
+
94
+ # Build the type string with wrappers
95
+ result = base_name
96
+ result = "[#{result}]" if list?(type)
97
+ result = "#{result}!" if non_null?(type)
98
+ result
99
+ elsif type.respond_to?(:graphql_name)
100
+ type.graphql_name
101
+ else
102
+ type.to_s
103
+ end
104
+ end
105
+ end
106
+
107
+ def list?(type)
108
+ type.is_a?(GraphQL::Schema::List) ||
109
+ (type.respond_to?(:list?) && type.list?)
110
+ end
111
+
112
+ def non_null?(type)
113
+ type.is_a?(GraphQL::Schema::NonNull) ||
114
+ (type.respond_to?(:non_null?) && type.non_null?)
115
+ end
116
+
117
+ def check_argument(arg_name, expected_type)
118
+ return false unless @field.respond_to?(:arguments)
119
+
120
+ arg = @field.arguments[arg_name]
121
+ return false unless arg
122
+
123
+ if expected_type
124
+ actual_type = type_to_string(arg.type)
125
+ actual_type == expected_type
126
+ else
127
+ true
128
+ end
129
+ end
130
+
131
+ def find_mismatched_arguments
132
+ return {} unless @expected_arguments
133
+
134
+ @expected_arguments.select do |arg_name, expected_type|
135
+ !check_argument(arg_name, expected_type)
136
+ end
137
+ end
138
+
139
+ def type_name(type)
140
+ if type.respond_to?(:graphql_name)
141
+ type.graphql_name
142
+ elsif type.respond_to?(:name)
143
+ type.name
144
+ else
145
+ type.to_s
146
+ end
147
+ end
148
+ end
149
+
150
+ # Matcher for checking if an enum has specific values
151
+ #
152
+ # @example
153
+ # expect(StatusEnum).to have_enum_values("ACTIVE", "INACTIVE", "PENDING")
154
+ #
155
+ RSpec::Matchers.define :have_enum_values do |*expected_values|
156
+ match do |enum_type|
157
+ @enum_type = enum_type
158
+ @expected_values = expected_values.map(&:to_s)
159
+
160
+ actual_values = get_enum_values(enum_type)
161
+ @actual_values = actual_values
162
+
163
+ @expected_values.all? { |value| actual_values.include?(value) }
164
+ end
165
+
166
+ failure_message do
167
+ missing_values = @expected_values - @actual_values
168
+ "expected #{enum_name(@enum_type)} to have values #{@expected_values.inspect}, " \
169
+ "but #{missing_values.inspect} were missing. " \
170
+ "Actual values: #{@actual_values.inspect}"
171
+ end
172
+
173
+ private
174
+
175
+ def get_enum_values(enum_type)
176
+ if enum_type.respond_to?(:values)
177
+ # GraphQL::EnumType or GraphQL::Schema::Enum
178
+ if enum_type.values.is_a?(Hash)
179
+ enum_type.values.keys.map(&:to_s)
180
+ elsif enum_type.values.is_a?(Array)
181
+ enum_type.values.map { |v| v.respond_to?(:graphql_name) ? v.graphql_name : v.to_s }
182
+ else
183
+ []
184
+ end
185
+ else
186
+ []
187
+ end
188
+ end
189
+
190
+ def enum_name(enum_type)
191
+ if enum_type.respond_to?(:graphql_name)
192
+ enum_type.graphql_name
193
+ elsif enum_type.respond_to?(:name)
194
+ enum_type.name
195
+ else
196
+ enum_type.to_s
197
+ end
198
+ end
199
+ end
200
+
201
+ # Matcher for checking if a field has a specific argument with type
202
+ #
203
+ # @example
204
+ # expect(UserType.fields["posts"]).to have_argument(:limit).of_type("Int")
205
+ # expect(UserType.fields["posts"]).to have_argument(:status).of_type("Status!")
206
+ # expect(QueryType.fields["users"]).to have_argument(:filter).of_type("UserFilterInput")
207
+ #
208
+ RSpec::Matchers.define :have_argument do |arg_name|
209
+ match do |field|
210
+ @field = field
211
+ @arg_name = arg_name.to_s
212
+
213
+ # Get the argument
214
+ @argument = get_argument(field, @arg_name)
215
+ return false unless @argument
216
+
217
+ # Check argument type if specified
218
+ if @expected_type
219
+ actual_type = argument_type_string(@argument)
220
+ return false unless actual_type == @expected_type
221
+ end
222
+
223
+ true
224
+ end
225
+
226
+ chain :of_type do |expected_type|
227
+ @expected_type = expected_type
228
+ end
229
+
230
+ failure_message do
231
+ if !@argument
232
+ field_name = @field.respond_to?(:name) ? @field.name : @field.to_s
233
+ "expected field #{field_name.inspect} to have argument #{@arg_name.inspect}, but it does not"
234
+ elsif @expected_type && argument_type_string(@argument) != @expected_type
235
+ "expected argument #{@arg_name.inspect} to have type #{@expected_type.inspect}, " \
236
+ "got #{argument_type_string(@argument).inspect}"
237
+ end
238
+ end
239
+
240
+ private
241
+
242
+ def get_argument(field, arg_name)
243
+ return nil unless field.respond_to?(:arguments)
244
+
245
+ field.arguments[arg_name]
246
+ end
247
+
248
+ def argument_type_string(argument)
249
+ return nil unless argument
250
+
251
+ type = argument.type
252
+ type_to_string(type)
253
+ end
254
+
255
+ def type_to_string(type)
256
+ case type
257
+ when GraphQL::Schema::NonNull
258
+ "#{type_to_string(type.of_type)}!"
259
+ when GraphQL::Schema::List
260
+ "[#{type_to_string(type.of_type)}]"
261
+ when GraphQL::Schema::Member
262
+ type.graphql_name
263
+ else
264
+ # Handle wrapped types from graphql-ruby
265
+ if type.respond_to?(:unwrap)
266
+ unwrapped = type.unwrap
267
+ base_name = unwrapped.respond_to?(:graphql_name) ? unwrapped.graphql_name : unwrapped.to_s
268
+
269
+ # Build the type string with wrappers
270
+ result = base_name
271
+ result = "[#{result}]" if list?(type)
272
+ result = "#{result}!" if non_null?(type)
273
+ result
274
+ elsif type.respond_to?(:graphql_name)
275
+ type.graphql_name
276
+ else
277
+ type.to_s
278
+ end
279
+ end
280
+ end
281
+
282
+ def list?(type)
283
+ type.is_a?(GraphQL::Schema::List) ||
284
+ (type.respond_to?(:list?) && type.list?)
285
+ end
286
+
287
+ def non_null?(type)
288
+ type.is_a?(GraphQL::Schema::NonNull) ||
289
+ (type.respond_to?(:non_null?) && type.non_null?)
290
+ end
291
+ end
292
+
293
+ # Matcher for checking if a schema has a specific query field
294
+ #
295
+ # @example
296
+ # expect(schema).to have_query(:user).with_argument(:id, "ID!")
297
+ # expect(schema).to have_query(:users).of_type("[User!]!")
298
+ #
299
+ RSpec::Matchers.define :have_query do |query_name|
300
+ match do |schema|
301
+ @schema = schema
302
+ @query_name = query_name.to_s
303
+
304
+ # Get the query type
305
+ query_type = get_query_type(schema)
306
+ return false unless query_type
307
+
308
+ # Get the field
309
+ @field = get_field(query_type, @query_name)
310
+ return false unless @field
311
+
312
+ # Check field type if specified
313
+ if @expected_type
314
+ actual_type = field_type_string(@field)
315
+ return false unless actual_type == @expected_type
316
+ end
317
+
318
+ # Check arguments if specified
319
+ if @expected_arguments && @expected_arguments.any?
320
+ @expected_arguments.all? do |arg_name, arg_type|
321
+ check_argument(arg_name, arg_type)
322
+ end
323
+ else
324
+ true
325
+ end
326
+ end
327
+
328
+ chain :of_type do |expected_type|
329
+ @expected_type = expected_type
330
+ end
331
+
332
+ chain :with_argument do |arg_name, arg_type|
333
+ @expected_arguments ||= {}
334
+ @expected_arguments[arg_name.to_s] = arg_type
335
+ end
336
+
337
+ failure_message do
338
+ if !get_query_type(@schema)
339
+ "expected schema to have a query type, but it does not"
340
+ elsif !@field
341
+ "expected schema query to have field #{@query_name.inspect}, but it does not"
342
+ elsif @expected_type && field_type_string(@field) != @expected_type
343
+ "expected query field #{@query_name.inspect} to have type #{@expected_type.inspect}, " \
344
+ "got #{field_type_string(@field).inspect}"
345
+ elsif @expected_arguments
346
+ "expected query field #{@query_name.inspect} to have arguments #{@expected_arguments.inspect}"
347
+ end
348
+ end
349
+
350
+ private
351
+
352
+ def get_query_type(schema)
353
+ schema.respond_to?(:query) ? schema.query : nil
354
+ end
355
+
356
+ def get_field(type, field_name)
357
+ if type.respond_to?(:fields)
358
+ type.fields[field_name]
359
+ elsif type.respond_to?(:get_field)
360
+ type.get_field(field_name)
361
+ end
362
+ end
363
+
364
+ def field_type_string(field)
365
+ return nil unless field
366
+
367
+ type = field.type
368
+ type_to_string(type)
369
+ end
370
+
371
+ def type_to_string(type)
372
+ case type
373
+ when GraphQL::Schema::NonNull
374
+ "#{type_to_string(type.of_type)}!"
375
+ when GraphQL::Schema::List
376
+ "[#{type_to_string(type.of_type)}]"
377
+ when GraphQL::Schema::Member
378
+ type.graphql_name
379
+ else
380
+ if type.respond_to?(:unwrap)
381
+ unwrapped = type.unwrap
382
+ base_name = unwrapped.respond_to?(:graphql_name) ? unwrapped.graphql_name : unwrapped.to_s
383
+
384
+ result = base_name
385
+ result = "[#{result}]" if list?(type)
386
+ result = "#{result}!" if non_null?(type)
387
+ result
388
+ elsif type.respond_to?(:graphql_name)
389
+ type.graphql_name
390
+ else
391
+ type.to_s
392
+ end
393
+ end
394
+ end
395
+
396
+ def list?(type)
397
+ type.is_a?(GraphQL::Schema::List) ||
398
+ (type.respond_to?(:list?) && type.list?)
399
+ end
400
+
401
+ def non_null?(type)
402
+ type.is_a?(GraphQL::Schema::NonNull) ||
403
+ (type.respond_to?(:non_null?) && type.non_null?)
404
+ end
405
+
406
+ def check_argument(arg_name, expected_type)
407
+ return false unless @field.respond_to?(:arguments)
408
+
409
+ arg = @field.arguments[arg_name]
410
+ return false unless arg
411
+
412
+ if expected_type
413
+ actual_type = type_to_string(arg.type)
414
+ actual_type == expected_type
415
+ else
416
+ true
417
+ end
418
+ end
419
+ end
420
+
421
+ # Matcher for checking if a schema has a specific mutation field
422
+ #
423
+ # @example
424
+ # expect(schema).to have_mutation(:createUser).with_argument(:input, "CreateUserInput!")
425
+ # expect(schema).to have_mutation(:deleteUser).of_type("DeleteUserPayload!")
426
+ #
427
+ RSpec::Matchers.define :have_mutation do |mutation_name|
428
+ match do |schema|
429
+ @schema = schema
430
+ @mutation_name = mutation_name.to_s
431
+
432
+ # Get the mutation type
433
+ mutation_type = get_mutation_type(schema)
434
+ return false unless mutation_type
435
+
436
+ # Get the field
437
+ @field = get_field(mutation_type, @mutation_name)
438
+ return false unless @field
439
+
440
+ # Check field type if specified
441
+ if @expected_type
442
+ actual_type = field_type_string(@field)
443
+ return false unless actual_type == @expected_type
444
+ end
445
+
446
+ # Check arguments if specified
447
+ if @expected_arguments && @expected_arguments.any?
448
+ @expected_arguments.all? do |arg_name, arg_type|
449
+ check_argument(arg_name, arg_type)
450
+ end
451
+ else
452
+ true
453
+ end
454
+ end
455
+
456
+ chain :of_type do |expected_type|
457
+ @expected_type = expected_type
458
+ end
459
+
460
+ chain :with_argument do |arg_name, arg_type|
461
+ @expected_arguments ||= {}
462
+ @expected_arguments[arg_name.to_s] = arg_type
463
+ end
464
+
465
+ failure_message do
466
+ if !get_mutation_type(@schema)
467
+ "expected schema to have a mutation type, but it does not"
468
+ elsif !@field
469
+ "expected schema mutation to have field #{@mutation_name.inspect}, but it does not"
470
+ elsif @expected_type && field_type_string(@field) != @expected_type
471
+ "expected mutation field #{@mutation_name.inspect} to have type #{@expected_type.inspect}, " \
472
+ "got #{field_type_string(@field).inspect}"
473
+ elsif @expected_arguments
474
+ "expected mutation field #{@mutation_name.inspect} to have arguments #{@expected_arguments.inspect}"
475
+ end
476
+ end
477
+
478
+ private
479
+
480
+ def get_mutation_type(schema)
481
+ schema.respond_to?(:mutation) ? schema.mutation : nil
482
+ end
483
+
484
+ def get_field(type, field_name)
485
+ if type.respond_to?(:fields)
486
+ type.fields[field_name]
487
+ elsif type.respond_to?(:get_field)
488
+ type.get_field(field_name)
489
+ end
490
+ end
491
+
492
+ def field_type_string(field)
493
+ return nil unless field
494
+
495
+ type = field.type
496
+ type_to_string(type)
497
+ end
498
+
499
+ def type_to_string(type)
500
+ case type
501
+ when GraphQL::Schema::NonNull
502
+ "#{type_to_string(type.of_type)}!"
503
+ when GraphQL::Schema::List
504
+ "[#{type_to_string(type.of_type)}]"
505
+ when GraphQL::Schema::Member
506
+ type.graphql_name
507
+ else
508
+ if type.respond_to?(:unwrap)
509
+ unwrapped = type.unwrap
510
+ base_name = unwrapped.respond_to?(:graphql_name) ? unwrapped.graphql_name : unwrapped.to_s
511
+
512
+ result = base_name
513
+ result = "[#{result}]" if list?(type)
514
+ result = "#{result}!" if non_null?(type)
515
+ result
516
+ elsif type.respond_to?(:graphql_name)
517
+ type.graphql_name
518
+ else
519
+ type.to_s
520
+ end
521
+ end
522
+ end
523
+
524
+ def list?(type)
525
+ type.is_a?(GraphQL::Schema::List) ||
526
+ (type.respond_to?(:list?) && type.list?)
527
+ end
528
+
529
+ def non_null?(type)
530
+ type.is_a?(GraphQL::Schema::NonNull) ||
531
+ (type.respond_to?(:non_null?) && type.non_null?)
532
+ end
533
+
534
+ def check_argument(arg_name, expected_type)
535
+ return false unless @field.respond_to?(:arguments)
536
+
537
+ arg = @field.arguments[arg_name]
538
+ return false unless arg
539
+
540
+ if expected_type
541
+ actual_type = type_to_string(arg.type)
542
+ actual_type == expected_type
543
+ else
544
+ true
545
+ end
546
+ end
547
+ end
548
+ end
549
+ end
550
+ end
551
+
552
+ RSpec.configure do |config|
553
+ config.include Zenspec::Matchers::GraphQLTypeMatchers
554
+ end