graph_weaver 0.0.1 → 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.
@@ -1,228 +1,92 @@
1
1
  # typed: true
2
2
  # frozen_string_literal: true
3
3
 
4
+ require "date"
4
5
  require "graphql"
5
6
  require "sorbet-runtime"
6
7
 
7
8
  # Generates plain, statically-typecheckable Ruby from a GraphQL query +
8
9
  # schema: nested T::Structs, from_h casting code, and a sig'd execute
9
- # method. Unlike StructTypes (which builds classes at parse time, visible
10
- # only at runtime), the output is source on disk — srb tc sees the exact
11
- # result type of each query.
10
+ # method. The output is source on disk, so srb tc sees the exact result
11
+ # type of each query.
12
12
  #
13
13
  # Supports queries and mutations; plain fields, inline fragments, named
14
14
  # fragment spreads (including interface type conditions), union- and
15
15
  # interface-typed fields (dispatch on __typename), enums (generated
16
- # T::Enum), and typed variables (kwargs on execute). Input objects and
17
- # subscriptions are still open.
18
- class GraphWeaver::Codegen
19
- # sorbet types for scalars
20
- SCALARS = {
21
- "ID" => "String",
22
- "String" => "String",
23
- "Int" => "Integer",
24
- "Float" => "Float",
25
- "Boolean" => "T::Boolean",
26
- "Date" => "Date",
27
- }.freeze
28
-
29
- # deserialization for scalars whose wire format differs from their Ruby
30
- # type; anything absent passes through untouched
31
- SCALAR_CASTS = {
32
- "Date" => ->(expr) { "Date.iso8601(#{expr})" },
33
- }.freeze
34
-
35
- # the inverse, for serializing variables onto the wire
36
- SCALAR_SERIALIZERS = {
37
- "Date" => ->(expr) { "#{expr}.iso8601" },
38
- }.freeze
39
-
40
- class Scalar
41
- def initialize(name)
42
- @name = name
43
- end
44
-
45
- def bare_type
46
- SCALARS.fetch(@name, "T.untyped")
47
- end
48
-
49
- def prop_type
50
- "T.nilable(#{bare_type})"
51
- end
52
-
53
- def cast(expr, _depth)
54
- SCALAR_CASTS.fetch(@name) { return expr }.call(expr)
55
- end
56
-
57
- def identity?
58
- !SCALAR_CASTS.key?(@name)
59
- end
60
-
61
- def serialize(expr, _depth)
62
- SCALAR_SERIALIZERS.fetch(@name) { return expr }.call(expr)
63
- end
64
-
65
- def serialize_identity?
66
- !SCALAR_SERIALIZERS.key?(@name)
67
- end
68
-
69
- def non_null? = false
70
- def nested = nil
71
- end
72
-
73
- class NonNull
74
- def initialize(of)
75
- @of = of
76
- end
77
-
78
- def bare_type = @of.bare_type
79
- def prop_type = bare_type
80
- def cast(expr, depth) = @of.cast(expr, depth)
81
- def identity? = @of.identity?
82
- def serialize(expr, depth) = @of.serialize(expr, depth)
83
- def serialize_identity? = @of.serialize_identity?
84
- def non_null? = true
85
- def nested = @of.nested
86
- end
87
-
88
- class List
89
- def initialize(of)
90
- @of = of
91
- end
92
-
93
- def bare_type
94
- "T::Array[#{@of.prop_type}]"
95
- end
96
-
97
- def prop_type
98
- "T.nilable(#{bare_type})"
99
- end
100
-
101
- def cast(expr, depth)
102
- var = "v#{depth}"
103
- element = if @of.non_null? || @of.identity?
104
- @of.identity? ? var : @of.cast(var, depth + 1)
105
- else
106
- "#{var}&.then { |v#{depth + 1}| #{@of.cast("v#{depth + 1}", depth + 2)} }"
107
- end
108
-
109
- "#{expr}.map { |#{var}| #{element} }"
110
- end
111
-
112
- def identity? = @of.identity?
113
-
114
- def serialize(expr, depth)
115
- var = "v#{depth}"
116
- element = if @of.non_null? || @of.serialize_identity?
117
- @of.serialize_identity? ? var : @of.serialize(var, depth + 1)
118
- else
119
- "#{var}&.then { |v#{depth + 1}| #{@of.serialize("v#{depth + 1}", depth + 2)} }"
120
- end
121
-
122
- "#{expr}.map { |#{var}| #{element} }"
123
- end
124
-
125
- def serialize_identity? = @of.serialize_identity?
126
- def non_null? = false
127
- def nested = @of.nested
128
- end
129
-
130
- class ObjectNode
131
- Field = Struct.new(:prop, :key, :node)
132
-
133
- attr_reader :class_name, :fields
134
-
135
- def initialize(class_name)
136
- @class_name = class_name
137
- @fields = []
138
- end
139
-
140
- def bare_type = class_name
141
-
142
- def prop_type
143
- "T.nilable(#{bare_type})"
144
- end
145
-
146
- def cast(expr, _depth)
147
- "#{class_name}.from_h(#{expr})"
148
- end
149
-
150
- def identity? = false
151
- def non_null? = false
152
- def nested = self
153
- end
154
-
155
- class EnumNode
156
- attr_reader :class_name, :values
157
-
158
- def initialize(class_name, values)
159
- @class_name = class_name
160
- @values = values
161
- end
162
-
163
- def bare_type = class_name
164
-
165
- def prop_type
166
- "T.nilable(#{bare_type})"
167
- end
168
-
169
- def cast(expr, _depth)
170
- "#{class_name}.deserialize(#{expr})"
171
- end
16
+ # T::Enum), and typed variables (kwargs on execute). Subscriptions are
17
+ # still open.
18
+ #
19
+ # Split across: codegen/scalar_type.rb (the scalar registry),
20
+ # codegen/nodes.rb (the typed IR), codegen/emit.rb (source emission);
21
+ # this file holds the public API and the query walk.
22
+ require_relative "inflect"
23
+ require_relative "selection"
24
+ require_relative "codegen/scalar_type"
25
+ require_relative "codegen/nodes"
26
+ require_relative "codegen/emit"
172
27
 
173
- def identity? = false
28
+ class GraphWeaver::Codegen
29
+ include GraphWeaver::Inflect
30
+ include GraphWeaver::Selection
31
+ include Emit
32
+
33
+ attr_reader :module_name
34
+
35
+ # An executor is anything responding to `execute(query, variables:)`
36
+ # whose result `to_h`s into {"data" => ..., "errors" => ...}.
37
+ #
38
+ # executor: (a constant, or its name as a string) becomes the generated
39
+ # module's default transport; when omitted, generated code falls back
40
+ # to GraphWeaver.executor. module_name: defaults to the operation's
41
+ # name; default_module_name: is parse's container-scoped fallback (file
42
+ # generation stays strict — a checked-in file deserves a deliberate
43
+ # name).
44
+ def initialize(schema:, query:, module_name: nil, executor: nil, default_module_name: nil)
45
+ @schema = schema
46
+ @query = query.strip
47
+ @module_name = module_name
48
+ @default_module_name = default_module_name
49
+ @executor_const = self.class.executor_const(executor)
174
50
 
175
- def serialize(expr, _depth)
176
- "#{expr}.serialize"
51
+ if executor && @executor_const.nil?
52
+ # a live object can't be spelled in generated source — parse can
53
+ # set one via the module's writer, but file generation cannot
54
+ raise ArgumentError, "executor: must be a named constant or String (got #{executor.inspect}); pass live objects to parse"
177
55
  end
178
-
179
- def serialize_identity? = false
180
- def non_null? = false
181
- def nested = self
182
56
  end
183
57
 
184
- class UnionNode
185
- attr_reader :class_name, :members # graphql type name => ObjectNode
186
-
187
- def initialize(class_name, members)
188
- @class_name = class_name
189
- @members = members
190
- end
191
-
192
- def bare_type = "#{class_name}::Type"
193
-
194
- def prop_type
195
- "T.nilable(#{bare_type})"
196
- end
197
-
198
- def cast(expr, _depth)
199
- "#{class_name}.from_h(#{expr})"
58
+ # The constant name an executor can be referenced by in generated
59
+ # source nil when it can't be (live objects, anonymous modules).
60
+ def self.executor_const(executor)
61
+ case executor
62
+ when String then executor
63
+ when Module then executor.name
200
64
  end
201
-
202
- def identity? = false
203
- def non_null? = false
204
- def nested = self
205
65
  end
206
66
 
207
- # executor_const names anything responding to
208
- # `execute(query, variables:)` whose result `to_h`s into
209
- # {"data" => ..., "errors" => ...} — a Schema class for in-process
210
- # execution, or an HttpExecutor for a remote endpoint. The generated
211
- # execute also accepts an executor: override at runtime.
212
- def initialize(schema:, executor_const:, query:, module_name:)
213
- @schema = schema
214
- @executor_const = executor_const
215
- @query = query.strip
216
- @module_name = module_name
67
+ # one-step shorthand
68
+ def self.generate(schema:, query:, module_name: nil, executor: nil)
69
+ new(schema:, query:, module_name:, executor:).generate
217
70
  end
218
71
 
219
72
  # Development convenience: generate + eval in one step, no build
220
73
  # artifact or checked-in file. Same runtime semantics as the generated
221
74
  # file, but invisible to srb tc — use the build step for static typing.
222
- def self.load(schema:, executor_const:, query:, module_name:)
223
- source = new(schema:, executor_const:, query:, module_name:).generate
224
- Object.class_eval(source, "(struct_codegen)", 1)
225
- Object.const_get(module_name)
75
+ # Evaluates into an anonymous container, so no global constants leak;
76
+ # executor: additionally accepts a live object (set via .executor=).
77
+ def self.parse(schema:, query:, module_name: nil, executor: nil)
78
+ executor_const = executor_const(executor)
79
+
80
+ codegen = new(schema:, query:, module_name:, executor: executor_const, default_module_name: "Query")
81
+ source = codegen.generate
82
+
83
+ container = Module.new
84
+ container.module_eval(source, "(graph_weaver)", 1)
85
+ mod = container.const_get(codegen.module_name)
86
+ # live objects (or anonymous modules) can't be referenced from
87
+ # generated source — set them via the module's writer instead
88
+ mod.executor = executor if executor && executor_const.nil?
89
+ mod
226
90
  end
227
91
 
228
92
  VarDef = Struct.new(:kwarg, :wire, :node, :required)
@@ -230,20 +94,26 @@ class GraphWeaver::Codegen
230
94
  def generate
231
95
  errors = @schema.validate(@query)
232
96
  if errors.any?
233
- raise ArgumentError, "invalid query: #{errors.map(&:message).join("; ")}"
97
+ raise GraphWeaver::ValidationError.new(errors.map { |e| validation_detail(e) })
234
98
  end
235
99
 
236
- doc = GraphQL.parse(@query)
237
- @fragments = doc.definitions
238
- .grep(GraphQL::Language::Nodes::FragmentDefinition)
239
- .to_h { |fragment| [fragment.name, fragment] }
240
100
  @variable_enums = {}
101
+ @variable_inputs = {}
102
+ @inputs_in_progress = []
103
+ # requires contributed by the custom scalars this query actually uses
104
+ @scalar_requires = []
105
+
106
+ operation = load_operation(@query)
107
+ root_type = operation_root_type(operation)
241
108
 
242
- operation = doc.definitions.grep(GraphQL::Language::Nodes::OperationDefinition).first
243
- root_type = case operation&.operation_type
244
- when "query", nil then @schema.query
245
- when "mutation" then @schema.mutation
246
- else raise NotImplementedError, "unsupported operation: #{operation.operation_type}"
109
+ @module_name ||= operation.name || @default_module_name
110
+ unless @module_name
111
+ raise ArgumentError, "module_name: required for anonymous operations"
112
+ end
113
+
114
+ # generated source is eval'd by parse — never let a name inject code
115
+ unless @module_name.match?(/\A[A-Z]\w*(::[A-Z]\w*)*\z/)
116
+ raise ArgumentError, "module_name: must be a constant name, got #{@module_name.inspect}"
247
117
  end
248
118
 
249
119
  variables = operation.variables.map do |var|
@@ -262,17 +132,31 @@ class GraphWeaver::Codegen
262
132
  out << ""
263
133
  out << "# Generated by GraphWeaver — do not edit."
264
134
  out << ""
135
+ requires = @scalar_requires.uniq.sort
136
+ if requires.any?
137
+ requires.each { |req| out << "require #{req.inspect}" }
138
+ out << ""
139
+ end
265
140
  out << "module #{@module_name}"
266
141
  out << " extend T::Sig"
267
142
  out << ""
268
- out << " QUERY = T.let(<<~'GRAPHQL', String)"
143
+ # a GraphQL block string could contain a bare GRAPHQL line, which
144
+ # would terminate the heredoc early — pick a delimiter the query
145
+ # can't collide with
146
+ delimiter = "GRAPHQL"
147
+ delimiter += "_" while @query.match?(/^\s*#{delimiter}\s*$/)
148
+ out << " QUERY = T.let(<<~'#{delimiter}', String)"
269
149
  @query.each_line { |line| out << " #{line}".rstrip }
270
- out << " GRAPHQL"
150
+ out << " #{delimiter}"
271
151
  out << ""
272
152
  @variable_enums.each_value do |enum|
273
153
  emit_enum(enum, out, 1)
274
154
  out << ""
275
155
  end
156
+ @variable_inputs.each_value do |input|
157
+ emit_input(input, out, 1)
158
+ out << ""
159
+ end
276
160
  emit_nested(root, out, 1)
277
161
  out << ""
278
162
  emit_execute(out, variables)
@@ -283,53 +167,14 @@ class GraphWeaver::Codegen
283
167
 
284
168
  private
285
169
 
286
- # GraphQL names are plain camelCase/SCREAMING_SNAKE no acronym edge
287
- # cases, so minimal inflection beats an activesupport dependency
288
- def underscore(name)
289
- name.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
290
- end
291
-
292
- def camelize(name)
293
- name.split("_").map { |part| "#{part[0].upcase}#{part[1..]}" }.join
294
- end
295
-
296
- # Flatten a selection set as seen by `type`: plain fields collect
297
- # directly; inline fragments and named spreads recurse when their type
298
- # condition matches (exact name match — interface conditions are out of
299
- # scope for this spike).
300
- def gather(type, selections, out = {})
301
- selections.each do |selection|
302
- case selection
303
- when GraphQL::Language::Nodes::Field
304
- (out[selection.alias || selection.name] ||= []) << selection
305
- when GraphQL::Language::Nodes::InlineFragment
306
- gather(type, selection.selections, out) if applies?(selection.type&.name, type)
307
- when GraphQL::Language::Nodes::FragmentSpread
308
- fragment = @fragments.fetch(selection.name) do
309
- raise ArgumentError, "unknown fragment: #{selection.name}"
310
- end
311
- gather(type, fragment.selections, out) if applies?(fragment.type.name, type)
312
- else
313
- raise NotImplementedError, "unsupported selection: #{selection.class}"
314
- end
315
- end
316
-
170
+ # Selection#each_field, collected by result key (codegen groups
171
+ # repeated selections of one field so it can merge them)
172
+ def gather(type, selections)
173
+ out = {}
174
+ each_field(type, selections) { |key, node| (out[key] ||= []) << node }
317
175
  out
318
176
  end
319
177
 
320
- # A fragment's type condition applies when it names this type exactly,
321
- # or an interface/union this type belongs to (`... on Named { ... }`).
322
- def applies?(condition, type)
323
- return true if condition.nil? || condition == type.graphql_name
324
-
325
- condition_type = @schema.get_type(condition)
326
- return false unless condition_type
327
-
328
- kind = condition_type.kind.name
329
- (kind == "INTERFACE" || kind == "UNION") &&
330
- @schema.possible_types(condition_type).include?(type)
331
- end
332
-
333
178
  def object_node(type, selections, class_name)
334
179
  node = ObjectNode.new(class_name)
335
180
  taken = [class_name]
@@ -357,12 +202,18 @@ class GraphWeaver::Codegen
357
202
  # (SDL round-trips reorder values alphabetically)
358
203
  type_ref(field_type) { EnumNode.new(name, core.values.keys.sort) }
359
204
  when "SCALAR"
360
- type_ref(field_type) { Scalar.new(core.graphql_name) }
205
+ type_ref(field_type) { scalar_node(core.graphql_name) }
361
206
  else
362
207
  raise NotImplementedError, "unsupported kind: #{core.kind.name}"
363
208
  end
364
209
  end
365
210
 
211
+ # a field under @skip/@include may be absent from the response no
212
+ # matter what the schema says — its type must admit nil
213
+ if field_nodes.any? { |n| n.directives.any? { |d| %w[skip include].include?(d.name) } }
214
+ child = child.of if child.is_a?(NonNull)
215
+ end
216
+
366
217
  node.fields << ObjectNode::Field.new(prop, key, child)
367
218
  end
368
219
 
@@ -395,21 +246,59 @@ class GraphWeaver::Codegen
395
246
  when GraphQL::Language::Nodes::ListType
396
247
  List.new(ast_type_ref(ast_type.of_type))
397
248
  when GraphQL::Language::Nodes::TypeName
398
- core = @schema.get_type(ast_type.name)
399
- case core.kind.name
400
- when "SCALAR"
401
- Scalar.new(core.graphql_name)
402
- when "ENUM"
403
- @variable_enums[core.graphql_name] ||=
404
- EnumNode.new(core.graphql_name, core.values.keys.sort)
405
- else
406
- raise NotImplementedError, "unsupported variable kind: #{core.kind.name}"
407
- end
249
+ variable_core(@schema.get_type(ast_type.name))
408
250
  else
409
251
  raise NotImplementedError, "unsupported type node: #{ast_type.class}"
410
252
  end
411
253
  end
412
254
 
255
+ # the input-side core kinds a variable (or input-object field) can have
256
+ def variable_core(core)
257
+ case core.kind.name
258
+ when "SCALAR"
259
+ scalar_node(core.graphql_name)
260
+ when "ENUM"
261
+ @variable_enums[core.graphql_name] ||=
262
+ EnumNode.new(core.graphql_name, core.values.keys.sort)
263
+ when "INPUT_OBJECT"
264
+ input_node(core)
265
+ else
266
+ raise NotImplementedError, "unsupported variable kind: #{core.kind.name}"
267
+ end
268
+ end
269
+
270
+ # A module-level T::Struct per input type, with a serialize method
271
+ # producing the wire hash. Registered once per type; nested inputs
272
+ # register their dependencies first (insertion order = emission order).
273
+ def input_node(core)
274
+ return @variable_inputs[core.graphql_name] if @variable_inputs.key?(core.graphql_name)
275
+
276
+ if @inputs_in_progress.include?(core.graphql_name)
277
+ raise NotImplementedError, "recursive input type: #{core.graphql_name}"
278
+ end
279
+ @inputs_in_progress << core.graphql_name
280
+
281
+ node = InputNode.new(core.graphql_name)
282
+ # sorted so output is deterministic across schema sources
283
+ core.arguments.values.sort_by(&:graphql_name).each do |argument|
284
+ child = type_ref(argument.type) { variable_core(unwrap(argument.type)) }
285
+ required = child.non_null? && !argument.default_value?
286
+ node.fields << InputNode::Field.new(
287
+ underscore(argument.graphql_name), argument.graphql_name, child, required
288
+ )
289
+ end
290
+
291
+ @inputs_in_progress.delete(core.graphql_name)
292
+ @variable_inputs[core.graphql_name] = node
293
+ end
294
+
295
+ # A Scalar node, recording any requires its registered type needs so the
296
+ # generated file can require them (collected across the whole query).
297
+ def scalar_node(name)
298
+ @scalar_requires.concat(GraphWeaver::Codegen.scalar(name).requires)
299
+ Scalar.new(name)
300
+ end
301
+
413
302
  # rebuild the NON_NULL/LIST wrappers around the core node
414
303
  def type_ref(type, &core)
415
304
  case type.kind.name
@@ -436,132 +325,4 @@ class GraphWeaver::Codegen
436
325
  candidate
437
326
  end
438
327
 
439
- def emit_nested(node, out, indent)
440
- case node
441
- when UnionNode then emit_union(node, out, indent)
442
- when EnumNode then emit_enum(node, out, indent)
443
- else emit_object(node, out, indent)
444
- end
445
- end
446
-
447
- def emit_enum(node, out, indent)
448
- pad = " " * indent
449
-
450
- out << "#{pad}class #{node.class_name} < T::Enum"
451
- out << "#{pad} enums do"
452
- node.values.each do |value|
453
- out << "#{pad} #{camelize(value.downcase)} = new(#{value.inspect})"
454
- end
455
- out << "#{pad} end"
456
- out << "#{pad}end"
457
- end
458
-
459
- def emit_object(node, out, indent)
460
- pad = " " * indent
461
-
462
- out << "#{pad}class #{node.class_name} < T::Struct"
463
- out << "#{pad} extend T::Sig"
464
- out << ""
465
-
466
- node.fields.filter_map { |field| field.node.nested }.each do |child|
467
- emit_nested(child, out, indent + 1)
468
- out << ""
469
- end
470
-
471
- node.fields.each do |field|
472
- out << "#{pad} const :#{field.prop}, #{field.node.prop_type}"
473
- end
474
-
475
- out << ""
476
- out << "#{pad} sig { params(data: T::Hash[String, T.untyped]).returns(#{node.class_name}) }"
477
- out << "#{pad} def self.from_h(data)"
478
- out << "#{pad} new("
479
- node.fields.each do |field|
480
- out << "#{pad} #{field.prop}: #{field_cast(field)},"
481
- end
482
- out << "#{pad} )"
483
- out << "#{pad} end"
484
- out << "#{pad}end"
485
- end
486
-
487
- def emit_union(node, out, indent)
488
- pad = " " * indent
489
-
490
- out << "#{pad}module #{node.class_name}"
491
- out << "#{pad} extend T::Sig"
492
- out << ""
493
-
494
- node.members.each_value do |member|
495
- emit_object(member, out, indent + 1)
496
- out << ""
497
- end
498
-
499
- member_names = node.members.values.map(&:class_name)
500
- type_alias = member_names.size == 1 ? member_names.first : "T.any(#{member_names.join(", ")})"
501
- out << "#{pad} Type = T.type_alias { #{type_alias} }"
502
- out << ""
503
- out << "#{pad} sig { params(data: T::Hash[String, T.untyped]).returns(Type) }"
504
- out << "#{pad} def self.from_h(data)"
505
- out << "#{pad} case (typename = data.fetch(\"__typename\"))"
506
- node.members.each do |graphql_name, member|
507
- out << "#{pad} when #{graphql_name.inspect} then #{member.class_name}.from_h(data)"
508
- end
509
- out << "#{pad} else raise \"unexpected __typename: \#{typename}\""
510
- out << "#{pad} end"
511
- out << "#{pad} end"
512
- out << "#{pad}end"
513
- end
514
-
515
- def emit_execute(out, variables)
516
- sig_params = variables.map do |var|
517
- kwarg_type = var.required ? var.node.prop_type : "T.nilable(#{var.node.bare_type})"
518
- "#{var.kwarg}: #{kwarg_type}"
519
- end
520
- sig_params << "executor: T.untyped"
521
-
522
- kwargs = variables.map { |var| var.required ? "#{var.kwarg}:" : "#{var.kwarg}: nil" }
523
- kwargs << "executor: #{@executor_const}"
524
-
525
- out << " sig { params(#{sig_params.join(", ")}).returns(Result) }"
526
- out << " def self.execute(#{kwargs.join(", ")})"
527
-
528
- required, optional = variables.partition(&:required)
529
- if required.empty?
530
- out << " variables = {}"
531
- else
532
- out << " variables = {"
533
- required.each do |var|
534
- out << " #{var.wire.inspect} => #{variable_serialize(var)},"
535
- end
536
- out << " }"
537
- end
538
- optional.each do |var|
539
- out << " variables[#{var.wire.inspect}] = #{variable_serialize(var)} unless #{var.kwarg}.nil?"
540
- end
541
-
542
- out << ""
543
- out << " result = executor.execute(QUERY, variables: variables).to_h"
544
- out << " if (errors = result[\"errors\"])"
545
- out << " raise \"query failed: \#{errors.inspect}\""
546
- out << " end"
547
- out << ""
548
- out << " Result.from_h(result.fetch(\"data\"))"
549
- out << " end"
550
- end
551
-
552
- def variable_serialize(var)
553
- var.node.serialize_identity? ? var.kwarg : var.node.serialize(var.kwarg, 1)
554
- end
555
-
556
- def field_cast(field)
557
- node = field.node
558
-
559
- if node.non_null?
560
- raw = "data.fetch(#{field.key.inspect})"
561
- node.identity? ? raw : node.cast(raw, 1)
562
- else
563
- raw = "data[#{field.key.inspect}]"
564
- node.identity? ? raw : "#{raw}&.then { |v1| #{node.cast("v1", 2)} }"
565
- end
566
- end
567
328
  end