graph_weaver 0.7.4 → 0.7.6

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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +2 -2
  3. data/README.md +1 -0
  4. data/docs/errors.md +12 -5
  5. data/docs/generated_modules.md +134 -17
  6. data/docs/getting_started.md +182 -20
  7. data/docs/logging.md +79 -35
  8. data/docs/migrating.md +126 -0
  9. data/docs/scalars.md +50 -6
  10. data/docs/testing.md +78 -14
  11. data/docs/upgrading.md +44 -2
  12. data/examples/README.md +4 -2
  13. data/examples/github/generate.rb +22 -8
  14. data/examples/github/generated/star_mutation.rb +2 -2
  15. data/examples/github/generated/stargazers_query.rb +2 -2
  16. data/examples/github/generated/starred_query.rb +2 -2
  17. data/examples/github/run.rb +1 -0
  18. data/examples/github/setup.rb +16 -8
  19. data/graph_weaver.gemspec +15 -6
  20. data/lib/generators/graph_weaver/install_generator.rb +49 -2
  21. data/lib/graph_weaver/client.rb +0 -23
  22. data/lib/graph_weaver/codegen/aliases.rb +36 -3
  23. data/lib/graph_weaver/codegen/emit.rb +20 -15
  24. data/lib/graph_weaver/codegen/enum_type.rb +52 -11
  25. data/lib/graph_weaver/codegen/nodes.rb +75 -32
  26. data/lib/graph_weaver/codegen.rb +260 -100
  27. data/lib/graph_weaver/coerce.rb +1 -1
  28. data/lib/graph_weaver/federation.rb +1 -6
  29. data/lib/graph_weaver/graph.rb +55 -5
  30. data/lib/graph_weaver/hints.rb +20 -5
  31. data/lib/graph_weaver/in_process.rb +2 -4
  32. data/lib/graph_weaver/input_struct.rb +50 -10
  33. data/lib/graph_weaver/internal/overrides.rb +126 -14
  34. data/lib/graph_weaver/internal/subgraphs.rb +1 -10
  35. data/lib/graph_weaver/internal/test_clients.rb +29 -7
  36. data/lib/graph_weaver/internal/unused.rb +62 -18
  37. data/lib/graph_weaver/internal/values.rb +24 -7
  38. data/lib/graph_weaver/internal.rb +23 -6
  39. data/lib/graph_weaver/log_subscriber.rb +27 -17
  40. data/lib/graph_weaver/logging.rb +115 -82
  41. data/lib/graph_weaver/parsing.rb +32 -3
  42. data/lib/graph_weaver/query_module.rb +67 -12
  43. data/lib/graph_weaver/railtie.rb +7 -2
  44. data/lib/graph_weaver/rspec.rb +41 -18
  45. data/lib/graph_weaver/schema_diff.rb +24 -5
  46. data/lib/graph_weaver/schema_loader.rb +29 -17
  47. data/lib/graph_weaver/tasks.rb +98 -16
  48. data/lib/graph_weaver/testing/fake_client.rb +28 -31
  49. data/lib/graph_weaver/testing/router.rb +26 -25
  50. data/lib/graph_weaver/testing.rb +27 -8
  51. data/lib/graph_weaver/transport.rb +1 -1
  52. data/lib/graph_weaver/version.rb +1 -1
  53. data/lib/graph_weaver.rb +72 -42
  54. metadata +3 -2
@@ -113,11 +113,11 @@ class GraphWeaver::Codegen
113
113
 
114
114
  def cast(expr, depth)
115
115
  var = "v#{depth}"
116
- element = if @of.non_null? || @of.identity?
117
- @of.identity? ? var : @of.cast(var, depth + 1)
118
- else
119
- "#{var}&.then { |v#{depth + 1}| #{@of.cast("v#{depth + 1}", depth + 2)} }"
120
- end
116
+ element =
117
+ if @of.identity? then var
118
+ elsif @of.non_null? then @of.cast(var, depth + 1)
119
+ else "#{var}&.then { |v#{depth + 1}| #{@of.cast("v#{depth + 1}", depth + 2)} }"
120
+ end
121
121
 
122
122
  "#{expr}.map { |#{var}| #{element} }"
123
123
  end
@@ -127,11 +127,11 @@ class GraphWeaver::Codegen
127
127
 
128
128
  def serialize(expr, depth)
129
129
  var = "v#{depth}"
130
- element = if @of.non_null? || @of.serialize_identity?
131
- @of.serialize_identity? ? var : @of.serialize(var, depth + 1)
132
- else
133
- "#{var}&.then { |v#{depth + 1}| #{@of.serialize("v#{depth + 1}", depth + 2)} }"
134
- end
130
+ element =
131
+ if @of.serialize_identity? then var
132
+ elsif @of.non_null? then @of.serialize(var, depth + 1)
133
+ else "#{var}&.then { |v#{depth + 1}| #{@of.serialize("v#{depth + 1}", depth + 2)} }"
134
+ end
135
135
 
136
136
  "#{expr}.map { |#{var}| #{element} }"
137
137
  end
@@ -153,11 +153,11 @@ class GraphWeaver::Codegen
153
153
 
154
154
  def hash_coerce(expr, depth)
155
155
  var = "v#{depth}"
156
- inner = if @of.non_null? || @of.hash_coerce_identity?
157
- @of.hash_coerce_identity? ? var : @of.hash_coerce(var, depth + 1)
158
- else
159
- "#{var}&.then { |v#{depth + 1}| #{@of.hash_coerce("v#{depth + 1}", depth + 2)} }"
160
- end
156
+ inner =
157
+ if @of.hash_coerce_identity? then var
158
+ elsif @of.non_null? then @of.hash_coerce(var, depth + 1)
159
+ else "#{var}&.then { |v#{depth + 1}| #{@of.hash_coerce("v#{depth + 1}", depth + 2)} }"
160
+ end
161
161
  return "#{expr}.map { |#{var}| #{inner} }" if hash_coerce_identity?
162
162
 
163
163
  # the index is a path segment — `where._and.0._not.species` needs the 0
@@ -207,10 +207,13 @@ class GraphWeaver::Codegen
207
207
  class EnumNode < Node
208
208
  attr_reader :class_name, :values, :aliases
209
209
 
210
- def initialize(class_name, values, aliases = {})
210
+ # fallback: the name of the member unknown wire values cast to (register_enum
211
+ # fallback: true), or nil for a strict enum
212
+ def initialize(class_name, values, aliases = {}, fallback: nil)
211
213
  @class_name = class_name
212
214
  @values = values
213
215
  @aliases = aliases
216
+ @fallback = fallback
214
217
  end
215
218
 
216
219
  def bare_type = class_name
@@ -220,10 +223,19 @@ class GraphWeaver::Codegen
220
223
  def aliased? = !@aliases.empty?
221
224
  def alias_const = "#{GraphWeaver::Inflect.underscore(class_name).upcase}_ALIASES"
222
225
 
226
+ # register_enum fallback: true — the extra member every value the schema
227
+ # doesn't declare casts to, and the one member a variable can't send
228
+ attr_reader :fallback
229
+ def fallback? = !@fallback.nil?
230
+ def fallback_const = "#{class_name}::#{@fallback}"
231
+
223
232
  def cast(expr, _depth)
224
- "GraphWeaver::Hints.enum(#{class_name}, #{expr}#{", #{alias_const}" if aliased?})"
233
+ "GraphWeaver::Hints.enum(#{class_name}, #{expr}#{runtime_args})"
225
234
  end
226
235
 
236
+ # Other serializes to ENUM_FALLBACK_WIRE, which casts back to Other — so a
237
+ # result carrying it still round-trips through #as_json, even though no
238
+ # server would accept that spelling.
227
239
  def serialize(expr, _depth)
228
240
  "#{expr}.serialize"
229
241
  end
@@ -235,7 +247,11 @@ class GraphWeaver::Codegen
235
247
  def coerce? = true
236
248
 
237
249
  def coerce(expr)
238
- "GraphWeaver::InputStruct.enum(#{class_name}, #{expr}#{", #{alias_const}" if aliased?})"
250
+ "GraphWeaver::InputStruct.enum(#{class_name}, #{expr}#{runtime_args})"
251
+ end
252
+
253
+ def runtime_args
254
+ "#{", #{alias_const}" if aliased?}#{", fallback: #{fallback_const}" if fallback?}"
239
255
  end
240
256
 
241
257
  def input_type = "T.any(#{class_name}, String)"
@@ -277,7 +293,9 @@ class GraphWeaver::Codegen
277
293
  # #as_json. Without a fallback the table is total and a miss is a real
278
294
  # mistake, so it still raises.
279
295
  def serialize(expr, _depth)
280
- return "#{const_prefix}_TO_WIRE.fetch(#{expr})" unless @fallback
296
+ unless @fallback
297
+ return "GraphWeaver::InputStruct.enum_wire(#{@graphql_name.inspect}, #{const_prefix}_TO_WIRE, #{expr})"
298
+ end
281
299
 
282
300
  "#{const_prefix}_TO_WIRE.fetch(#{expr}) { |member| member.serialize }"
283
301
  end
@@ -358,20 +376,22 @@ class GraphWeaver::Codegen
358
376
  def nested = self
359
377
  end
360
378
 
361
- # A reference to a union hoisted into the shared types module (a named
362
- # shared fragment spread as a whole union field): the query references
363
- # <Name>::Type and dispatches through <Name>.from_h, where <Name> is the
364
- # alias the query module gives GraphQLTypes::<Name>. The type family lives
365
- # once in the shared module, so the same union across queries is one Ruby
366
- # type nested is nil, nothing is emitted here.
367
- class UnionRefNode < Node
368
- attr_reader :class_name
369
-
370
- def initialize(class_name)
379
+ # A reference to a type hoisted into the shared types module (a named shared
380
+ # fragment spread as a whole field): the query casts through <Name>.from_h,
381
+ # where <Name> is the alias the query module gives GraphQLTypes::<Name>. The
382
+ # type lives once in the shared module, so the same fragment across queries
383
+ # is one Ruby type nested is nil, nothing is emitted here.
384
+ class HoistedRefNode < Node
385
+ # graphql_type is carried for refusals alone — an alias path that tries to
386
+ # read into the hoisted struct says which type to register itself on
387
+ attr_reader :class_name, :graphql_type
388
+
389
+ def initialize(class_name, graphql_type = nil)
371
390
  @class_name = class_name
391
+ @graphql_type = graphql_type
372
392
  end
373
393
 
374
- def bare_type = "#{class_name}::Type"
394
+ def bare_type = class_name
375
395
 
376
396
  def cast(expr, _depth)
377
397
  "#{class_name}.from_h(#{expr})"
@@ -382,6 +402,29 @@ class GraphWeaver::Codegen
382
402
  end
383
403
  end
384
404
 
405
+ # The same, on an abstract type: the hoisted name is a dispatch module, so
406
+ # the Ruby type is its member union rather than the module itself.
407
+ class UnionRefNode < HoistedRefNode
408
+ def bare_type = "#{class_name}::Type"
409
+ end
410
+
411
+ # An abstract fragment narrowing to one member hoists to that member's
412
+ # struct, and narrowing filters — so the match is tested at the reference,
413
+ # the way NarrowedNode does it, rather than inside a T::Struct.from_h that
414
+ # has no way to answer nil. See NarrowedNode for what `typename` being
415
+ # absent means.
416
+ class NarrowedRefNode < HoistedRefNode
417
+ def initialize(class_name, graphql_type = nil, typename: nil)
418
+ super(class_name, graphql_type)
419
+ @typename = typename
420
+ end
421
+
422
+ def cast(expr, depth)
423
+ match = @typename ? "#{expr}[\"__typename\"] == #{@typename.inspect}" : "!#{expr}.empty?"
424
+ "(#{match} ? #{super} : nil)"
425
+ end
426
+ end
427
+
385
428
  # An input-object variable: emitted as a module-level T::Struct whose
386
429
  # serialize produces the wire hash. Inputs never cast FROM the wire.
387
430
  # Joins the coerce protocol so execute kwargs accept plain hashes,
@@ -454,6 +497,6 @@ class GraphWeaver::Codegen
454
497
  # The IR is codegen's own vocabulary — every node type is reachable only
455
498
  # from inside the walk.
456
499
  private_constant :Node, :Scalar, :NonNull, :List, :ObjectNode, :EnumNode,
457
- :MappedEnum, :NarrowedNode, :UnionNode, :UnionRefNode, :InputNode,
458
- :RepresentationNode
500
+ :MappedEnum, :NarrowedNode, :UnionNode, :HoistedRefNode, :UnionRefNode,
501
+ :NarrowedRefNode, :InputNode, :RepresentationNode
459
502
  end