graph_weaver 0.1.0 → 0.2.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +6 -0
  3. data/CHANGELOG.md +256 -0
  4. data/Gemfile.lock +32 -2
  5. data/Makefile +7 -2
  6. data/NOTES.md +1 -1
  7. data/PLAN.md +41 -1
  8. data/README.md +56 -41
  9. data/docs/cassettes.md +76 -0
  10. data/docs/errors.md +37 -9
  11. data/docs/generated_modules.md +178 -29
  12. data/docs/getting_started.md +136 -0
  13. data/docs/logging.md +33 -0
  14. data/docs/real_world.md +27 -20
  15. data/docs/scalars.md +122 -8
  16. data/docs/testing.md +55 -38
  17. data/docs/transports.md +124 -0
  18. data/graph_weaver.gemspec +7 -1
  19. data/lib/graph_weaver/client.rb +201 -0
  20. data/lib/graph_weaver/codegen/emit.rb +315 -76
  21. data/lib/graph_weaver/codegen/enum_type.rb +149 -0
  22. data/lib/graph_weaver/codegen/nodes.rb +123 -68
  23. data/lib/graph_weaver/codegen/scalar_type.rb +53 -35
  24. data/lib/graph_weaver/codegen.rb +272 -101
  25. data/lib/graph_weaver/errors.rb +37 -25
  26. data/lib/graph_weaver/hints.rb +63 -0
  27. data/lib/graph_weaver/inflect.rb +2 -1
  28. data/lib/graph_weaver/input_struct.rb +59 -0
  29. data/lib/graph_weaver/logging.rb +41 -0
  30. data/lib/graph_weaver/railtie.rb +30 -0
  31. data/lib/graph_weaver/retry.rb +97 -0
  32. data/lib/graph_weaver/rspec.rb +17 -14
  33. data/lib/graph_weaver/schema_loader.rb +156 -20
  34. data/lib/graph_weaver/selection.rb +3 -3
  35. data/lib/graph_weaver/tasks.rb +71 -0
  36. data/lib/graph_weaver/testing/cassette.rb +42 -21
  37. data/lib/graph_weaver/testing/failure.rb +22 -22
  38. data/lib/graph_weaver/testing/{fake_executor.rb → fake_client.rb} +13 -13
  39. data/lib/graph_weaver/testing/values.rb +2 -2
  40. data/lib/graph_weaver/testing.rb +31 -15
  41. data/lib/graph_weaver/transport/faraday.rb +56 -0
  42. data/lib/graph_weaver/transport/http.rb +87 -0
  43. data/lib/graph_weaver/transport.rb +120 -0
  44. data/lib/graph_weaver/version.rb +1 -1
  45. data/lib/graph_weaver.rb +289 -38
  46. metadata +74 -5
  47. data/lib/graph_weaver/faraday_executor.rb +0 -61
  48. data/lib/graph_weaver/http_executor.rb +0 -44
  49. data/lib/graph_weaver/testing/rspec.rb +0 -5
@@ -1,13 +1,33 @@
1
1
  # typed: true
2
2
  # frozen_string_literal: true
3
3
 
4
+ require "forwardable"
5
+
4
6
  # The typed intermediate representation of a query selection: one node
5
7
  # per GraphQL type shape, each knowing its Sorbet prop type and the
6
8
  # cast/serialize code to emit.
7
9
  class GraphWeaver::Codegen
8
- class Scalar
9
- def initialize(name)
10
- @scalar = GraphWeaver::Codegen.scalar(name)
10
+ # Protocol defaults — subclasses override what differs. The full node
11
+ # protocol: bare_type, prop_type, cast(expr, depth), identity?,
12
+ # serialize(expr, depth), serialize_identity?, coerce?, coerce(expr),
13
+ # coerce_input_type, hash_coerce(expr, depth), hash_coerce_identity?,
14
+ # non_null?, nested.
15
+ class Node
16
+ def bare_type = raise(GraphWeaver::Error, "#{self.class} must define bare_type")
17
+ def prop_type = "T.nilable(#{bare_type})"
18
+ def identity? = false
19
+ def serialize_identity? = false
20
+ def coerce? = false
21
+ def hash_coerce_identity? = false
22
+ def non_null? = false
23
+ def nested = nil
24
+ end
25
+
26
+ class Scalar < Node
27
+ # takes a resolved ScalarType — the generator picks it from the
28
+ # client-scoped overlay or the global registry
29
+ def initialize(scalar_type)
30
+ @scalar = scalar_type
11
31
  end
12
32
 
13
33
  def bare_type
@@ -15,7 +35,9 @@ class GraphWeaver::Codegen
15
35
  end
16
36
 
17
37
  def prop_type
18
- "T.nilable(#{bare_type})"
38
+ # unregistered scalars are already T.untyped — wrapping in
39
+ # T.nilable is redundant and an srb tc error under typed: strict
40
+ bare_type == "T.untyped" ? bare_type : "T.nilable(#{bare_type})"
19
41
  end
20
42
 
21
43
  def cast(expr, _depth)
@@ -41,36 +63,36 @@ class GraphWeaver::Codegen
41
63
  def coerce(expr) = @scalar.coerce_input(expr)
42
64
  def coerce_input_type = @scalar.coerce_type
43
65
 
44
- def hash_coerce(expr, _depth) = expr
45
- def hash_coerce_identity? = true
66
+ # inside input-struct hashes, scalars coerce exactly like variable
67
+ # kwargs do — the registry (incl. GraphWeaver.auto_coerce) decides
68
+ def hash_coerce(expr, _depth)
69
+ @scalar.coerce_input(expr) || expr
70
+ end
46
71
 
47
- def non_null? = false
48
- def nested = nil
72
+ def hash_coerce_identity? = !@scalar.coerce?
49
73
  end
50
74
 
51
- class NonNull
75
+ # NonNull is its inner node with the nilability stripped — everything
76
+ # else passes through.
77
+ class NonNull < Node
78
+ extend Forwardable
79
+
52
80
  attr_reader :of
53
81
 
82
+ def_delegators :@of, :bare_type, :cast, :identity?, :serialize, :serialize_identity?,
83
+ :coerce?, :coerce, :coerce_input_type, :hash_coerce, :hash_coerce_identity?, :nested
84
+
54
85
  def initialize(of)
55
86
  @of = of
56
87
  end
57
88
 
58
- def bare_type = @of.bare_type
59
89
  def prop_type = bare_type
60
- def cast(expr, depth) = @of.cast(expr, depth)
61
- def identity? = @of.identity?
62
- def serialize(expr, depth) = @of.serialize(expr, depth)
63
- def serialize_identity? = @of.serialize_identity?
64
- def coerce? = @of.coerce?
65
- def coerce(expr) = @of.coerce(expr)
66
- def coerce_input_type = @of.coerce_input_type
67
- def hash_coerce(expr, depth) = @of.hash_coerce(expr, depth)
68
- def hash_coerce_identity? = @of.hash_coerce_identity?
69
90
  def non_null? = true
70
- def nested = @of.nested
71
91
  end
72
92
 
73
- class List
93
+ class List < Node
94
+ attr_reader :of
95
+
74
96
  def initialize(of)
75
97
  @of = of
76
98
  end
@@ -79,10 +101,6 @@ class GraphWeaver::Codegen
79
101
  "T::Array[#{@of.prop_type}]"
80
102
  end
81
103
 
82
- def prop_type
83
- "T.nilable(#{bare_type})"
84
- end
85
-
86
104
  def cast(expr, depth)
87
105
  var = "v#{depth}"
88
106
  element = if @of.non_null? || @of.identity?
@@ -108,50 +126,46 @@ class GraphWeaver::Codegen
108
126
  end
109
127
 
110
128
  def serialize_identity? = @of.serialize_identity?
111
- def coerce? = false
112
129
 
113
130
  def hash_coerce(expr, depth)
114
131
  var = "v#{depth}"
115
132
  inner = if @of.non_null? || @of.hash_coerce_identity?
116
133
  @of.hash_coerce_identity? ? var : @of.hash_coerce(var, depth + 1)
117
134
  else
118
- "#{var}.nil? ? nil : #{@of.hash_coerce(var, depth + 1)}"
135
+ "#{var}&.then { |v#{depth + 1}| #{@of.hash_coerce("v#{depth + 1}", depth + 2)} }"
119
136
  end
120
137
 
121
138
  "#{expr}.map { |#{var}| #{inner} }"
122
139
  end
123
140
 
124
141
  def hash_coerce_identity? = @of.hash_coerce_identity?
125
- def non_null? = false
126
142
  def nested = @of.nested
127
143
  end
128
144
 
129
- class ObjectNode
145
+ class ObjectNode < Node
130
146
  Field = Struct.new(:prop, :key, :node)
131
147
 
132
148
  attr_reader :class_name, :fields
149
+ # the GraphQL type this struct was generated from, and any registered
150
+ # helper modules to include (see Codegen.register_type)
151
+ attr_accessor :graphql_type, :mixins
133
152
 
134
153
  def initialize(class_name)
135
154
  @class_name = class_name
136
155
  @fields = []
156
+ @mixins = []
137
157
  end
138
158
 
139
159
  def bare_type = class_name
140
160
 
141
- def prop_type
142
- "T.nilable(#{bare_type})"
143
- end
144
-
145
161
  def cast(expr, _depth)
146
162
  "#{class_name}.from_h(#{expr})"
147
163
  end
148
164
 
149
- def identity? = false
150
- def non_null? = false
151
165
  def nested = self
152
166
  end
153
167
 
154
- class EnumNode
168
+ class EnumNode < Node
155
169
  attr_reader :class_name, :values
156
170
 
157
171
  def initialize(class_name, values)
@@ -161,34 +175,92 @@ class GraphWeaver::Codegen
161
175
 
162
176
  def bare_type = class_name
163
177
 
164
- def prop_type
165
- "T.nilable(#{bare_type})"
166
- end
167
-
168
178
  def cast(expr, _depth)
169
179
  "#{class_name}.deserialize(#{expr})"
170
180
  end
171
181
 
172
- def identity? = false
173
-
174
182
  def serialize(expr, _depth)
175
183
  "#{expr}.serialize"
176
184
  end
177
185
 
178
- def serialize_identity? = false
179
- def coerce? = false
186
+ # enums always coerce: a kwarg or hash field accepts the T::Enum or
187
+ # its wire value (deserialize raises on anything else)
188
+ def coerce? = true
180
189
 
181
- # enum fields in a plain hash accept the T::Enum or its wire value
182
- def hash_coerce(expr, _depth)
190
+ def coerce(expr)
183
191
  "(#{expr}.is_a?(#{class_name}) ? #{expr} : #{class_name}.deserialize(#{expr}))"
184
192
  end
185
193
 
186
- def hash_coerce_identity? = false
187
- def non_null? = false
194
+ def coerce_input_type = "T.any(#{class_name}, String)"
195
+ def hash_coerce(expr, _depth) = coerce(expr)
188
196
  def nested = self
189
197
  end
190
198
 
191
- class UnionNode
199
+ # A GraphQL enum mapped onto an app-owned T::Enum (see EnumType): no
200
+ # generated enum class — instead module-level <NAME>_FROM_WIRE /
201
+ # <NAME>_TO_WIRE constants translate at the boundary. fallback: makes
202
+ # casting absorb unknown wire values (inputs stay strict).
203
+ class MappedEnum < Node
204
+ attr_reader :graphql_name, :mapping
205
+
206
+ def initialize(enum_type, wire_values)
207
+ @graphql_name = enum_type.graphql_name
208
+ @type_name = enum_type.type.name
209
+ @fallback = enum_type.fallback
210
+ @mapping = enum_type.mapping_for(wire_values)
211
+ end
212
+
213
+ def const_prefix = GraphWeaver::Inflect.underscore(@graphql_name).upcase
214
+ def fallback_const = @fallback && "#{@type_name}.deserialize(#{@fallback.serialize.to_s.inspect})"
215
+
216
+ def bare_type = @type_name
217
+
218
+ def cast(expr, _depth)
219
+ if @fallback
220
+ "#{const_prefix}_FROM_WIRE.fetch(#{expr}) { #{fallback_const} }"
221
+ else
222
+ "#{const_prefix}_FROM_WIRE.fetch(#{expr})"
223
+ end
224
+ end
225
+
226
+ def serialize(expr, _depth)
227
+ "#{const_prefix}_TO_WIRE.fetch(#{expr})"
228
+ end
229
+
230
+ # kwargs and hash fields accept the member or its wire value; unlike
231
+ # casting, bad input raises even with a fallback (a typo'd input is
232
+ # our bug, not server drift)
233
+ def coerce? = true
234
+
235
+ def coerce(expr)
236
+ "(#{expr}.is_a?(#{@type_name}) ? #{expr} : #{const_prefix}_FROM_WIRE.fetch(#{expr}))"
237
+ end
238
+
239
+ def coerce_input_type = "T.any(#{@type_name}, String)"
240
+ def hash_coerce(expr, _depth) = coerce(expr)
241
+ end
242
+
243
+ # A single-condition narrowing of an abstract field (`... on Pet { ... }`
244
+ # and nothing else): the member struct when the runtime type matches,
245
+ # nil when it doesn't — a non-match's response object carries no
246
+ # matching fields, so the hash arrives empty. Always nilable, whatever
247
+ # the schema's nullability, because narrowing filters.
248
+ class NarrowedNode < Node
249
+ def initialize(of)
250
+ @of = of
251
+ end
252
+
253
+ def class_name = @of.class_name
254
+ def bare_type = @of.bare_type
255
+
256
+ def cast(expr, depth)
257
+ "(#{expr}.empty? ? nil : #{@of.cast(expr, depth)})"
258
+ end
259
+
260
+ def nested = @of
261
+ end
262
+
263
+ class UnionNode < Node
192
264
  attr_reader :class_name, :members # graphql type name => ObjectNode
193
265
 
194
266
  def initialize(class_name, members)
@@ -198,16 +270,10 @@ class GraphWeaver::Codegen
198
270
 
199
271
  def bare_type = "#{class_name}::Type"
200
272
 
201
- def prop_type
202
- "T.nilable(#{bare_type})"
203
- end
204
-
205
273
  def cast(expr, _depth)
206
274
  "#{class_name}.from_h(#{expr})"
207
275
  end
208
276
 
209
- def identity? = false
210
- def non_null? = false
211
277
  def nested = self
212
278
  end
213
279
 
@@ -215,7 +281,7 @@ class GraphWeaver::Codegen
215
281
  # serialize produces the wire hash. Inputs never cast FROM the wire.
216
282
  # Joins the coerce protocol so execute kwargs accept plain hashes,
217
283
  # normalized (and type-checked) through the generated .coerce.
218
- class InputNode
284
+ class InputNode < Node
219
285
  Field = Struct.new(:prop, :wire, :node, :required)
220
286
 
221
287
  attr_reader :class_name, :fields
@@ -227,18 +293,12 @@ class GraphWeaver::Codegen
227
293
 
228
294
  def bare_type = class_name
229
295
 
230
- def prop_type
231
- "T.nilable(#{bare_type})"
232
- end
233
-
234
296
  def serialize(expr, _depth)
235
297
  "#{expr}.serialize"
236
298
  end
237
299
 
238
- def serialize_identity? = false
239
-
240
300
  def cast(_expr, _depth)
241
- raise NotImplementedError, "input objects are never cast from responses"
301
+ raise GraphWeaver::Error, "input objects are never cast from responses"
242
302
  end
243
303
 
244
304
  def coerce? = true
@@ -247,10 +307,5 @@ class GraphWeaver::Codegen
247
307
 
248
308
  # building a struct field from a caller-supplied plain hash value
249
309
  def hash_coerce(expr, _depth) = "#{class_name}.coerce(#{expr})"
250
- def hash_coerce_identity? = false
251
-
252
- def identity? = false
253
- def non_null? = false
254
- def nested = nil
255
310
  end
256
311
  end
@@ -1,6 +1,8 @@
1
1
  # typed: true
2
2
  # frozen_string_literal: true
3
3
 
4
+ require "date"
5
+
4
6
  class GraphWeaver::Codegen
5
7
 
6
8
  # How one GraphQL scalar maps to Ruby: the Sorbet prop type, the
@@ -15,17 +17,17 @@ class GraphWeaver::Codegen
15
17
  # from the Ruby type when it is a real class, by probing for a known
16
18
  # deserializer and pairing its serializer (see CODECS) — so the common
17
19
  # case needs no more than a class:
18
- # type: Money (defines .parse) => Money.parse(expr) / expr.to_s
19
- # type: Blob (defines .load) => Blob.load(expr) / Blob.dump(expr)
20
+ # type: Money (defines .parse) => Money.parse(expr) / expr.to_s
21
+ # type: Blob (defines .load) => Blob.load(expr) / Blob.dump(expr)
20
22
  # Probing the *deserialize* side is deliberate: every object has #to_s,
21
23
  # so inferring a serializer off it would wrongly wrap plain types (String,
22
24
  # Integer) — pairing off a deserializer the type actually defines avoids
23
25
  # that. Override with an explicit value:
24
26
  # - a Symbol names a method, so there is no string to misspell:
25
- # cast: :load => "Blob.load(expr)" (class method on type)
26
- # serialize: :to_json => "expr.to_json" (instance method)
27
+ # cast: :load => "Blob.load(expr)" (class method on type)
28
+ # serialize: :to_json => "expr.to_json" (instance method)
27
29
  # - a Proc handles anything a Symbol can't express:
28
- # cast: ->(e) { "Money.new(#{e})" }
30
+ # cast: ->(e) { "Money.new(#{e})" }
29
31
  # - :itself opts out — force identity pass-through even when a codec
30
32
  # would otherwise match (rare)
31
33
  # requires: a String or Array of paths emitted as `require`s atop the
@@ -56,23 +58,44 @@ class GraphWeaver::Codegen
56
58
 
57
59
  attr_reader :graphql_name, :type, :requires
58
60
 
59
- def initialize(graphql_name, type:, cast: nil, serialize: nil, requires: nil, coerce: false)
61
+ def initialize(graphql_name, type, cast: nil, serialize: nil, requires: nil, coerce: nil)
60
62
  @graphql_name = graphql_name.to_s
61
63
  @klass = type.is_a?(Module) ? type : nil
62
64
  @type = type_name(type)
65
+ # requires: load BEFORE codec probing — the probe method may come
66
+ # from the required file (core Time has no .parse until the "time"
67
+ # stdlib loads)
68
+ @requires = normalize_requires(requires)
63
69
  codec = @klass && CODECS.find { |c| @klass.respond_to?(c.probe) }
64
70
  @cast = normalize_cast(cast, codec&.cast)
65
71
  @serialize = normalize_serialize(serialize, codec&.serialize)
66
- @requires = normalize_requires(requires)
67
72
  @coerce = coerce
68
73
  validate_coerce!
69
74
  end
70
75
 
76
+ # conversions applied to the four convertible built-ins when the
77
+ # global GraphWeaver.auto_coerce is on and no explicit coerce: given
78
+ AUTO_CONVERSIONS = {
79
+ "ID" => :to_s, "String" => :to_s, "Int" => :to_i, "Float" => :to_f,
80
+ }.freeze
81
+
71
82
  def cast(expr) = @cast&.call(expr)
72
83
  def cast? = !@cast.nil?
73
84
  def serialize(expr) = @serialize&.call(expr)
74
85
  def serialize? = !@serialize.nil?
75
- def coerce? = !!@coerce
86
+ def coerce? = !!effective_coerce
87
+
88
+ # Explicit coerce: always wins (false means never). Left unset, the
89
+ # global GraphWeaver.auto_coerce decides — resolved HERE, at
90
+ # generation time, so registration order doesn't matter: convertible
91
+ # built-ins get their conversion, anything with a full cast/serialize
92
+ # pair gets parse-style coercion.
93
+ def effective_coerce
94
+ return @coerce unless @coerce.nil?
95
+ return false unless GraphWeaver.auto_coerce
96
+
97
+ AUTO_CONVERSIONS.fetch(@graphql_name) { (cast? && serialize?) || nil }
98
+ end
76
99
 
77
100
  # The code that normalizes a variable input before it's serialized. Two
78
101
  # shapes: coerce: true parses a raw value into the rich type via the cast
@@ -82,17 +105,17 @@ class GraphWeaver::Codegen
82
105
  # afterward, but is identity for the conversion built-ins, so the
83
106
  # converted value goes on the wire natively (a Float, not "5.0").
84
107
  def coerce_input(expr)
85
- case @coerce
108
+ case effective_coerce
86
109
  when true then "(#{expr}.is_a?(#{@type}) ? #{expr} : #{cast(expr)})"
87
- when Symbol then "#{expr}.#{@coerce}"
110
+ when Symbol then "#{expr}.#{effective_coerce}"
88
111
  end
89
112
  end
90
113
 
91
114
  # the accepted Sorbet type for a coercible variable kwarg
92
115
  def coerce_type
93
- case @coerce
116
+ case effective_coerce
94
117
  when true then "T.any(#{@type}, String)"
95
- when Symbol then CONVERT_INPUTS.fetch(@coerce, "T.untyped")
118
+ when Symbol then CONVERT_INPUTS.fetch(effective_coerce, "T.untyped")
96
119
  end
97
120
  end
98
121
 
@@ -175,9 +198,9 @@ class GraphWeaver::Codegen
175
198
  # the accepted cast:/serialize:/requires: forms. Later registrations
176
199
  # win, so an app can override a built-in (e.g. map Date onto its own
177
200
  # type).
178
- def register_scalar(graphql_name, type:, cast: nil, serialize: nil, requires: nil, coerce: false)
201
+ def register_scalar(graphql_name, type, cast: nil, serialize: nil, requires: nil, coerce: nil)
179
202
  scalar_registry[graphql_name.to_s] =
180
- ScalarType.new(graphql_name, type:, cast:, serialize:, requires:, coerce:)
203
+ ScalarType.new(graphql_name, type, cast:, serialize:, requires:, coerce:)
181
204
  end
182
205
 
183
206
  # The ScalarType for a scalar name; unknown scalars fall back to an
@@ -185,7 +208,7 @@ class GraphWeaver::Codegen
185
208
  # scalars outside the table.
186
209
  def scalar(graphql_name)
187
210
  scalar_registry.fetch(graphql_name.to_s) do
188
- ScalarType.new(graphql_name, type: "T.untyped")
211
+ ScalarType.new(graphql_name, "T.untyped")
189
212
  end
190
213
  end
191
214
 
@@ -201,14 +224,12 @@ class GraphWeaver::Codegen
201
224
  end
202
225
 
203
226
  # Drop every custom registration and restore the built-in scalars — the
204
- # clean slate to reach for between tests, or to undo overrides. Pass
205
- # coerce: true to reload the built-ins with input coercion enabled
206
- # (Float accepts 5/"5" and .to_f's it, etc.), then register your own
207
- # scalars on top — a one-liner alternative to re-registering each
208
- # built-in by hand.
209
- def reset_scalars!(coerce: false)
227
+ # clean slate to reach for between tests, or to undo overrides. (Want
228
+ # the built-ins to coerce loose input? That's GraphWeaver.auto_coerce,
229
+ # resolved at generation time no re-registering.)
230
+ def reset_scalars!
210
231
  clear_scalars!
211
- register_builtin_scalars!(coerce:)
232
+ register_builtin_scalars!
212
233
  self
213
234
  end
214
235
 
@@ -218,19 +239,16 @@ class GraphWeaver::Codegen
218
239
  # matches nothing and leaves them identity — which is exactly why we
219
240
  # can name them with the real class constants. Date deserializes via
220
241
  # ISO-8601 (it *does* define .parse, but we want iso8601 specifically,
221
- # so it's explicit).
222
- #
223
- # coerce: true opts the convertible scalars into input coercion via a
224
- # plain conversion method (to_f/to_i/to_s); the wire value stays native
225
- # (a Float, not "5.0"). Boolean and Date have no lossless one-method
226
- # conversion, so they stay strict either way.
227
- def register_builtin_scalars!(coerce: false)
228
- register_scalar "ID", type: String, coerce: (:to_s if coerce)
229
- register_scalar "String", type: String, coerce: (:to_s if coerce)
230
- register_scalar "Int", type: Integer, coerce: (:to_i if coerce)
231
- register_scalar "Float", type: Float, coerce: (:to_f if coerce)
232
- register_scalar "Boolean", type: "T::Boolean"
233
- register_scalar "Date", type: Date, cast: :iso8601, serialize: :iso8601, requires: "date"
242
+ # so it's explicit). Input coercion is a generation-time concern:
243
+ # GraphWeaver.auto_coerce gives the convertible built-ins their
244
+ # conversion (see ScalarType::AUTO_CONVERSIONS).
245
+ def register_builtin_scalars!
246
+ register_scalar "ID", String
247
+ register_scalar "String", String
248
+ register_scalar "Int", Integer
249
+ register_scalar "Float", Float
250
+ register_scalar "Boolean", "T::Boolean"
251
+ register_scalar "Date", Date, cast: :iso8601, serialize: :iso8601, requires: "date"
234
252
  end
235
253
  end
236
254