yogurt 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 (74) hide show
  1. checksums.yaml +7 -0
  2. data/.DS_Store +0 -0
  3. data/.github/workflows/tests.yml +117 -0
  4. data/.gitignore +12 -0
  5. data/.rspec +3 -0
  6. data/.rubocop.yml +113 -0
  7. data/.ruby-gemset +1 -0
  8. data/.ruby-version +1 -0
  9. data/.travis.yml +7 -0
  10. data/CHANGELOG.md +0 -0
  11. data/Gemfile +6 -0
  12. data/Gemfile.lock +82 -0
  13. data/LICENSE +201 -0
  14. data/README.md +38 -0
  15. data/Rakefile +9 -0
  16. data/bin/bundle +105 -0
  17. data/bin/byebug +29 -0
  18. data/bin/coderay +29 -0
  19. data/bin/console +16 -0
  20. data/bin/htmldiff +29 -0
  21. data/bin/ldiff +29 -0
  22. data/bin/pry +29 -0
  23. data/bin/rake +29 -0
  24. data/bin/rspec +29 -0
  25. data/bin/rubocop +29 -0
  26. data/bin/ruby-parse +29 -0
  27. data/bin/ruby-rewrite +29 -0
  28. data/bin/setup +8 -0
  29. data/bin/srb +29 -0
  30. data/bin/srb-rbi +29 -0
  31. data/lib/yogurt.rb +95 -0
  32. data/lib/yogurt/code_generator.rb +706 -0
  33. data/lib/yogurt/code_generator/defined_class.rb +22 -0
  34. data/lib/yogurt/code_generator/defined_class_sorter.rb +44 -0
  35. data/lib/yogurt/code_generator/defined_method.rb +24 -0
  36. data/lib/yogurt/code_generator/enum_class.rb +56 -0
  37. data/lib/yogurt/code_generator/field_access_method.rb +273 -0
  38. data/lib/yogurt/code_generator/field_access_path.rb +56 -0
  39. data/lib/yogurt/code_generator/generated_file.rb +53 -0
  40. data/lib/yogurt/code_generator/input_class.rb +52 -0
  41. data/lib/yogurt/code_generator/leaf_class.rb +68 -0
  42. data/lib/yogurt/code_generator/operation_declaration.rb +12 -0
  43. data/lib/yogurt/code_generator/root_class.rb +130 -0
  44. data/lib/yogurt/code_generator/type_wrapper.rb +13 -0
  45. data/lib/yogurt/code_generator/typed_input.rb +12 -0
  46. data/lib/yogurt/code_generator/typed_output.rb +16 -0
  47. data/lib/yogurt/code_generator/utils.rb +140 -0
  48. data/lib/yogurt/code_generator/variable_definition.rb +33 -0
  49. data/lib/yogurt/converters.rb +50 -0
  50. data/lib/yogurt/error_result.rb +30 -0
  51. data/lib/yogurt/http.rb +80 -0
  52. data/lib/yogurt/inspectable.rb +22 -0
  53. data/lib/yogurt/memoize.rb +33 -0
  54. data/lib/yogurt/query.rb +23 -0
  55. data/lib/yogurt/query_container.rb +89 -0
  56. data/lib/yogurt/query_container/interfaces_and_unions_have_typename.rb +107 -0
  57. data/lib/yogurt/query_declaration.rb +11 -0
  58. data/lib/yogurt/query_executor.rb +23 -0
  59. data/lib/yogurt/query_result.rb +25 -0
  60. data/lib/yogurt/scalar_converter.rb +19 -0
  61. data/lib/yogurt/unexpected_object_type.rb +30 -0
  62. data/lib/yogurt/validation_error.rb +6 -0
  63. data/lib/yogurt/version.rb +6 -0
  64. data/sorbet/config +10 -0
  65. data/sorbet/rbi/fake_schema.rbi +14 -0
  66. data/sorbet/rbi/graphql.rbi +11 -0
  67. data/sorbet/rbi/hidden-definitions/errors.txt +20513 -0
  68. data/sorbet/rbi/hidden-definitions/hidden.rbi +42882 -0
  69. data/sorbet/rbi/sorbet-typed/lib/graphql/all/graphql.rbi +48 -0
  70. data/sorbet/rbi/sorbet-typed/lib/rainbow/all/rainbow.rbi +276 -0
  71. data/sorbet/rbi/sorbet-typed/lib/rubocop/~>0.85/rubocop.rbi +2072 -0
  72. data/sorbet/rbi/todo.rbi +6 -0
  73. data/yogurt.gemspec +54 -0
  74. metadata +286 -0
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'ruby-rewrite' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("parser", "ruby-rewrite")
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/bin/srb ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'srb' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("sorbet", "srb")
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'srb-rbi' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("sorbet", "srb-rbi")
@@ -0,0 +1,95 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require 'graphql'
5
+ require 'sorbet-runtime'
6
+ require 'zeitwerk'
7
+
8
+ module Yogurt
9
+ extend T::Sig
10
+
11
+ GRAPHQL_SCHEMA = T.type_alias {T.class_of(GraphQL::Schema)}
12
+ SCALAR_TYPE = T.type_alias {T.any(::String, T::Boolean, Numeric)}
13
+ OBJECT_TYPE = T.type_alias {T::Hash[String, T.untyped]}
14
+
15
+ sig do
16
+ params(
17
+ schema: GRAPHQL_SCHEMA,
18
+ query: String,
19
+ operation_name: String,
20
+ variables: T.nilable(T::Hash[String, T.untyped]),
21
+ options: T.untyped,
22
+ ).returns(T::Hash[String, T.untyped])
23
+ end
24
+ def self.execute(schema:, query:, operation_name:, variables:, options:)
25
+ execute = Yogurt.registered_schemas.fetch(schema)
26
+ execute.execute(
27
+ query,
28
+ operation_name: operation_name,
29
+ variables: variables,
30
+ options: options,
31
+ )
32
+ end
33
+
34
+ sig {returns(T.nilable(T.class_of(GraphQL::Schema)))}
35
+ def self.default_schema
36
+ @default_schema
37
+ end
38
+
39
+ sig {returns(T::Hash[GRAPHQL_SCHEMA, QueryExecutor])}
40
+ def self.registered_schemas
41
+ @registered_schemas = T.let(@registered_schemas, T.nilable(T::Hash[GRAPHQL_SCHEMA, QueryExecutor]))
42
+ @registered_schemas ||= {}
43
+ end
44
+
45
+ sig {params(schema: GRAPHQL_SCHEMA, execute: QueryExecutor, default: T::Boolean).void}
46
+ def self.add_schema(schema, execute, default: true)
47
+ raise ArgumentError, "GraphQL schema must be assigned to a constant" if schema.name.nil?
48
+
49
+ registered_schemas[schema] = execute
50
+ @default_schema = T.let(schema, T.nilable(T.class_of(GraphQL::Schema))) if default
51
+ end
52
+
53
+ SCALAR_CONVERTER = T.type_alias {T.all(Module, ScalarConverter)}
54
+
55
+ sig {params(schema: GRAPHQL_SCHEMA).returns(T::Hash[String, SCALAR_CONVERTER])}
56
+ def self.scalar_converters(schema)
57
+ raise ArgumentError, "GraphQL Schema has not been registered." if !registered_schemas.key?(schema)
58
+
59
+ @scalar_converters = T.let(
60
+ @scalar_converters,
61
+ T.nilable(
62
+ T::Hash[
63
+ GRAPHQL_SCHEMA,
64
+ T::Hash[String, SCALAR_CONVERTER]
65
+ ],
66
+ ),
67
+ )
68
+
69
+ @scalar_converters ||= Hash.new {|hash, key| hash[key] = {}}
70
+ @scalar_converters[schema]
71
+ end
72
+
73
+ sig do
74
+ params(
75
+ schema: GRAPHQL_SCHEMA,
76
+ graphql_type_name: String,
77
+ deserializer: SCALAR_CONVERTER,
78
+ ).void
79
+ end
80
+ def self.register_scalar(schema, graphql_type_name, deserializer)
81
+ raise ArgumentError, "Schema does not contain the type #{graphql_type_name}" if !schema.types.key?(graphql_type_name)
82
+
83
+ raise ArgumentError, "ScalarConverters must be assigned to constants" if deserializer.name.nil?
84
+
85
+ scalar_converters(schema)[graphql_type_name] = deserializer
86
+ end
87
+ end
88
+
89
+ loader = Zeitwerk::Loader.new
90
+ loader.inflector.inflect({
91
+ 'graphql_client' => 'Yogurt',
92
+ 'version' => 'VERSION'
93
+ })
94
+ loader.push_dir(__dir__)
95
+ loader.setup
@@ -0,0 +1,706 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Yogurt
5
+ class CodeGenerator
6
+ extend T::Sig
7
+ include Utils
8
+
9
+ PROTECTED_NAMES = T.let([
10
+ *Object.instance_methods,
11
+ *Yogurt::QueryResult.instance_methods,
12
+ *Yogurt::ErrorResult.instance_methods
13
+ ].map(&:to_s).sort.uniq.freeze, T::Array[String])
14
+
15
+ sig {returns(T::Hash[String, DefinedClass])}
16
+ attr_reader :classes
17
+
18
+ sig {params(schema: GRAPHQL_SCHEMA).void}
19
+ def initialize(schema)
20
+ @schema = T.let(schema, GRAPHQL_SCHEMA)
21
+
22
+ # Maps GraphQL enum name to class name
23
+ @enums = T.let({}, T::Hash[String, String])
24
+
25
+ # Maps GraphQL input type name to class name
26
+ @input_types = T.let({}, T::Hash[String, String])
27
+ @scalars = T.let(Yogurt.scalar_converters(schema), T::Hash[String, SCALAR_CONVERTER])
28
+ @classes = T.let({}, T::Hash[String, DefinedClass])
29
+ end
30
+
31
+ # Returns the contents of the generated classes, in dependency order, as a single file.
32
+ sig {returns(String)}
33
+ def contents
34
+ definitions = DefinedClassSorter.new(classes.values)
35
+ .sorted_classes
36
+ .map(&:to_ruby)
37
+ .join("\n")
38
+
39
+ <<~STRING
40
+ # typed: strict
41
+ # frozen_string_literal: true
42
+
43
+ require 'pp'
44
+
45
+ #{definitions}
46
+ STRING
47
+ end
48
+
49
+ # Returns the contents of the generated classes, split into separate files (one per class).
50
+ # Classes are returned in dependency order.
51
+ sig {returns(T::Array[GeneratedFile])}
52
+ def content_files
53
+ DefinedClassSorter.new(classes.values).sorted_classes.map do |klass|
54
+ GeneratedFile.new(
55
+ constant_name: klass.name,
56
+ dependencies: klass.dependencies,
57
+ code: klass.to_ruby,
58
+ type: case klass
59
+ when RootClass
60
+ GeneratedFile::FileType::OPERATION
61
+ when LeafClass
62
+ GeneratedFile::FileType::OBJECT_RESULT
63
+ when InputClass
64
+ GeneratedFile::FileType::INPUT_OBJECT
65
+ when EnumClass
66
+ GeneratedFile::FileType::ENUM
67
+ else
68
+ raise "Unhandled class type: #{klass.inspect}"
69
+ end,
70
+ )
71
+ end
72
+ end
73
+
74
+ # Returns the contents with syntax highlighting (if CodeRay is available)
75
+ sig {returns(String)}
76
+ def formatted_contents
77
+ if defined?(CodeRay)
78
+ CodeRay.scan(contents, :ruby).term
79
+ else
80
+ contents
81
+ end
82
+ end
83
+
84
+ sig {params(declaration: QueryDeclaration).void}
85
+ def generate(declaration)
86
+ query = GraphQL::Query.new(declaration.schema, declaration.query_text)
87
+
88
+ query.operations.each do |name, op_def|
89
+ owner_type = case op_def.operation_type
90
+ when 'query'
91
+ schema.query
92
+ when 'mutation'
93
+ schema.mutation
94
+ when 'subscription'
95
+ schema.subscription
96
+ else
97
+ Kernel.raise("Unknown operation type: #{op_def.type}")
98
+ end
99
+
100
+ ensure_constant_name(name)
101
+ module_name = "::#{declaration.container.name}::#{name}"
102
+ generate_result_class(
103
+ module_name,
104
+ owner_type,
105
+ op_def.selections,
106
+ operation_declaration: OperationDeclaration.new(
107
+ declaration: declaration,
108
+ operation_name: name,
109
+ variables: op_def.variables,
110
+ ),
111
+ )
112
+ end
113
+ end
114
+
115
+ sig {params(definition: DefinedClass).void}
116
+ def add_class(definition)
117
+ raise "Attempting to redefine class #{definition.name}" if @classes.key?(definition.name)
118
+
119
+ @classes[definition.name] = definition
120
+ end
121
+
122
+ sig {returns(GRAPHQL_SCHEMA)}
123
+ def schema
124
+ @schema
125
+ end
126
+
127
+ sig {params(name: String).void}
128
+ def ensure_constant_name(name)
129
+ return if name.match?(/\A[A-Z][a-zA-Z0-9_]+\z/)
130
+
131
+ raise "You must use valid Ruby constant names for query names (got #{name})"
132
+ end
133
+
134
+ sig {params(enum_type: T.class_of(GraphQL::Schema::Enum)).returns(String)}
135
+ def enum_class(enum_type)
136
+ enum_class_name = @enums[enum_type.graphql_name]
137
+ return enum_class_name if enum_class_name
138
+
139
+ # TODO: sanitize the name
140
+ klass_name = "::#{schema.name}::#{enum_type.graphql_name}"
141
+ add_class(EnumClass.new(name: klass_name, serialized_values: enum_type.values.keys))
142
+ @enums[enum_type.graphql_name] = klass_name
143
+ end
144
+
145
+ sig {params(graphql_name: String).returns(String)}
146
+ def input_class(graphql_name)
147
+ input_class_name = @input_types[graphql_name]
148
+ return input_class_name if input_class_name
149
+
150
+ klass_name = "::#{schema.name}::#{graphql_name}"
151
+ graphql_type = schema.types[graphql_name]
152
+
153
+ arguments = graphql_type.arguments.each_value.map do |argument|
154
+ variable_definition(argument)
155
+ end
156
+
157
+ add_class(InputClass.new(name: klass_name, arguments: arguments))
158
+ @input_types[graphql_name] = klass_name
159
+ end
160
+
161
+ sig do
162
+ params(
163
+ module_name: String,
164
+ owner_type: T.untyped,
165
+ selections: T::Array[T.untyped],
166
+ operation_declaration: T.nilable(OperationDeclaration),
167
+ dependencies: T::Array[String],
168
+ )
169
+ .returns(TypedOutput)
170
+ end
171
+ private def generate_result_class(module_name, owner_type, selections, operation_declaration: nil, dependencies: [])
172
+ methods = T.let({}, T::Hash[Symbol, T::Array[FieldAccessPath]])
173
+ next_dependencies = [module_name, *dependencies]
174
+
175
+ generate_methods_from_selections(
176
+ module_name: module_name,
177
+ owner_type: owner_type,
178
+ parent_types: [owner_type],
179
+ selections: selections,
180
+ methods: methods,
181
+ next_dependencies: next_dependencies,
182
+ )
183
+
184
+ defined_methods = methods.map do |name, paths|
185
+ FieldAccessMethod.new(
186
+ name: name,
187
+ field_access_paths: paths,
188
+ schema: schema,
189
+ )
190
+ end
191
+
192
+ if operation_declaration
193
+ variable_definitions = operation_declaration.variables.map {|v| variable_definition(v)}
194
+ variable_dependencies = variable_definitions.map(&:dependency).compact
195
+ add_class(
196
+ RootClass.new(
197
+ name: module_name,
198
+ schema: schema,
199
+ operation_name: operation_declaration.operation_name,
200
+ graphql_type: owner_type,
201
+ query_container: operation_declaration.declaration.container,
202
+ defined_methods: defined_methods,
203
+ variables: variable_definitions,
204
+ dependencies: dependencies + variable_dependencies,
205
+ ),
206
+ )
207
+ else
208
+ klass_definition = @classes[module_name]
209
+ if klass_definition.nil?
210
+ add_class(
211
+ LeafClass.new(
212
+ name: module_name,
213
+ schema: schema,
214
+ defined_methods: defined_methods,
215
+ dependencies: dependencies,
216
+ graphql_type: owner_type,
217
+ ),
218
+ )
219
+ elsif klass_definition.is_a?(LeafClass)
220
+ raise "Attempting to extend existing class with a different owner type: #{klass_definition.graphql_type.graphql_name} vs #{owner_type.graphql_name}" if klass_definition.graphql_type != owner_type
221
+
222
+ klass_definition.merge_defined_methods(defined_methods)
223
+ klass_definition.dependencies |= dependencies
224
+ else
225
+ raise "Attempting to extend a class that is intended to be an object result class, but found the wrong type: #{klass_definition.inspect}"
226
+ end
227
+ end
228
+
229
+ TypedOutput.new(
230
+ signature: module_name,
231
+ deserializer: <<~STRING,
232
+ #{module_name}.new(raw_value)
233
+ STRING
234
+ )
235
+ end
236
+
237
+ sig do
238
+ params(
239
+ module_name: String,
240
+ owner_type: T.untyped,
241
+ parent_types: T::Array[T.untyped],
242
+ selections: T::Array[T.untyped],
243
+ methods: T::Hash[Symbol, T::Array[FieldAccessPath]],
244
+ next_dependencies: T::Array[String],
245
+ ).void
246
+ end
247
+ private def generate_methods_from_selections(module_name:, owner_type:, parent_types:, selections:, methods:, next_dependencies:)
248
+ # First pass, handle the fields that are directly selected.
249
+ selections.each do |node|
250
+ next unless node.is_a?(GraphQL::Language::Nodes::Field)
251
+
252
+ # Get the result type for this particular selection
253
+ field_name = node.name
254
+
255
+ # We always include a `__typename` method on query result objects,
256
+ # so it's redundant here.
257
+ next if field_name == '__typename' && node.alias.nil?
258
+
259
+ field_definition = owner_type.get_field(field_name)
260
+
261
+ if field_definition.nil?
262
+ field_definition = if owner_type == schema.query && (entry_point_field = schema.introspection_system.entry_point(name: field_name))
263
+ entry_point_field
264
+ elsif (dynamic_field = schema.introspection_system.dynamic_field(name: field_name))
265
+ dynamic_field
266
+ else
267
+ raise "Invariant: no field for #{owner_type}.#{field_name}"
268
+ end
269
+ end
270
+
271
+ input_name = node.alias || node.name
272
+ next_name = if node.alias
273
+ "#{module_name}::#{camelize(node.alias)}_#{camelize(node.name)}"
274
+ else
275
+ "#{module_name}::#{camelize(input_name)}"
276
+ end
277
+ return_type = generate_output_type(
278
+ field_definition.type,
279
+ node.selections,
280
+ next_name,
281
+ input_name,
282
+ next_dependencies,
283
+ )
284
+
285
+ method_name = generate_method_name(underscore(input_name))
286
+ method_array = methods[method_name] ||= T.let([], T::Array[FieldAccessPath])
287
+ method_array << FieldAccessPath.new(
288
+ name: method_name,
289
+ schema: schema,
290
+ signature: return_type.signature,
291
+ expression: return_type.deserializer,
292
+ fragment_types: parent_types.map(&:graphql_name),
293
+ )
294
+ end
295
+
296
+ # Second pass, handle fragment spreads
297
+ selections.each do |node| # rubocop:disable Style/CombinableLoops
298
+ next unless node.is_a?(GraphQL::Language::Nodes::InlineFragment)
299
+
300
+ subselections = node.selections
301
+ fragment_type = schema.types[node.type.name]
302
+ fragment_methods = {}
303
+
304
+ generate_methods_from_selections(
305
+ module_name: module_name,
306
+ owner_type: fragment_type,
307
+ parent_types: [*parent_types, fragment_type],
308
+ selections: subselections,
309
+ methods: fragment_methods,
310
+ next_dependencies: next_dependencies,
311
+ )
312
+
313
+ fragment_methods.each do |method_name, submethods|
314
+ method_array = methods[method_name] ||= T.let([], T::Array[FieldAccessPath])
315
+ method_array.concat(submethods)
316
+ end
317
+ end
318
+ end
319
+
320
+ sig do
321
+ params(
322
+ wrappers: T::Array[TypeWrapper],
323
+ variable_name: String,
324
+ array_wrappers: Integer,
325
+ level: Integer,
326
+ core_expression: String,
327
+ ).returns(String)
328
+ end
329
+ def build_expression(wrappers, variable_name, array_wrappers, level, core_expression)
330
+ next_wrapper = wrappers.shift
331
+ case next_wrapper
332
+ when TypeWrapper::ARRAY
333
+ array_wrappers -= 1
334
+ next_variable_name = if array_wrappers.zero?
335
+ "raw_value"
336
+ else
337
+ "inner_value#{array_wrappers}"
338
+ end
339
+
340
+ indent(<<~STRING.rstrip, level.positive? ? 1 : 0)
341
+ #{variable_name}.map do |#{next_variable_name}|
342
+ #{indent(build_expression(wrappers, next_variable_name, array_wrappers, level + 1, core_expression), 1)}
343
+ end
344
+ STRING
345
+ when TypeWrapper::NILABLE
346
+ break_word = level.zero? ? 'return' : 'next'
347
+ <<~STRING.rstrip
348
+ #{break_word} if #{variable_name}.nil?
349
+ #{build_expression(wrappers, variable_name, array_wrappers, level, core_expression)}
350
+ STRING
351
+ when nil
352
+ if level.zero?
353
+ core_expression.gsub(/raw_value/, variable_name)
354
+ else
355
+ core_expression
356
+ end
357
+ else
358
+ T.absurd(next_wrapper)
359
+ end
360
+ end
361
+
362
+ # Returns the TypedOutput object for this graphql type.
363
+ sig do
364
+ params(
365
+ graphql_type: T.untyped,
366
+ subselections: T::Array[T.untyped],
367
+ next_module_name: String,
368
+ input_name: String,
369
+ dependencies: T::Array[String],
370
+ ).returns(TypedOutput)
371
+ end
372
+ def generate_output_type(graphql_type, subselections, next_module_name, input_name, dependencies)
373
+ # Unwrap the graphql type, but keep track of the wrappers that it had
374
+ # so that we can build the sorbet signature and return expression.
375
+ wrappers = T.let([], T::Array[TypeWrapper])
376
+ fully_unwrapped_type = T.let(graphql_type, T.untyped)
377
+
378
+ # Sorbet uses nullable wrappers, whereas GraphQL uses non-nullable wrappers.
379
+ # This boolean is used to help with the conversion.
380
+ skip_nilable = T.let(false, T::Boolean)
381
+ array_wrappers = 0
382
+
383
+ loop do
384
+ if fully_unwrapped_type.non_null?
385
+ fully_unwrapped_type = T.unsafe(fully_unwrapped_type).of_type
386
+ skip_nilable = true
387
+ next
388
+ end
389
+
390
+ wrappers << TypeWrapper::NILABLE if !skip_nilable
391
+ skip_nilable = false
392
+
393
+ if fully_unwrapped_type.list?
394
+ wrappers << TypeWrapper::ARRAY
395
+ array_wrappers += 1
396
+ fully_unwrapped_type = T.unsafe(fully_unwrapped_type).of_type
397
+ next
398
+ end
399
+
400
+ break
401
+ end
402
+
403
+ core_typed_expression = unwrapped_graphql_type_to_output_type(fully_unwrapped_type, subselections, next_module_name, dependencies)
404
+
405
+ signature = core_typed_expression.signature
406
+ variable_name = "raw_result[#{input_name.inspect}]"
407
+ method_body = build_expression(wrappers.dup, variable_name, array_wrappers, 0, core_typed_expression.deserializer)
408
+
409
+ wrappers.reverse_each do |wrapper|
410
+ case wrapper
411
+ when TypeWrapper::ARRAY
412
+ signature = "T::Array[#{signature}]"
413
+ when TypeWrapper::NILABLE
414
+ signature = "T.nilable(#{signature})"
415
+ else
416
+ T.absurd(wrapper)
417
+ end
418
+ end
419
+
420
+ TypedOutput.new(
421
+ signature: signature,
422
+ deserializer: method_body,
423
+ )
424
+ end
425
+
426
+ sig {params(scalar_converter: SCALAR_CONVERTER).returns(TypedOutput)}
427
+ def output_type_from_scalar_converter(scalar_converter)
428
+ name = scalar_converter.name
429
+ raise "Expected scalar deserializer to be assigned to a constant" if name.nil?
430
+
431
+ TypedOutput.new(
432
+ signature: scalar_converter.type_alias.name,
433
+ deserializer: "#{name}.deserialize(raw_value)",
434
+ )
435
+ end
436
+
437
+ sig do
438
+ params(
439
+ type_name: String,
440
+ block: T.proc.returns(TypedOutput),
441
+ ).returns(TypedOutput)
442
+ end
443
+ def deserializer_or_default(type_name, &block)
444
+ deserializer = @scalars[type_name]
445
+ return output_type_from_scalar_converter(deserializer) if deserializer
446
+
447
+ yield
448
+ end
449
+
450
+ # Given an (unwrapped) GraphQL type, returns the definition for the type to use
451
+ # for the signature and method body.
452
+ sig do
453
+ params(
454
+ graphql_type: T.untyped,
455
+ subselections: T::Array[T.untyped],
456
+ next_module_name: String,
457
+ dependencies: T::Array[String],
458
+ ).returns(TypedOutput)
459
+ end
460
+ def unwrapped_graphql_type_to_output_type(graphql_type, subselections, next_module_name, dependencies)
461
+ # TODO: Once https://github.com/sorbet/sorbet/issues/649 is fixed, change the `cast`'s back to `let`'s
462
+ if graphql_type == GraphQL::Types::Boolean
463
+ TypedOutput.new(
464
+ signature: "T::Boolean",
465
+ deserializer: 'T.cast(raw_value, T::Boolean)',
466
+ )
467
+ elsif graphql_type == GraphQL::Types::BigInt
468
+ deserializer_or_default(T.unsafe(GraphQL::Types::BigInt).graphql_name) do
469
+ TypedOutput.new(
470
+ signature: "Integer",
471
+ deserializer: 'T.cast(raw_value, T.any(String, Integer)).to_i',
472
+ )
473
+ end
474
+ elsif graphql_type == GraphQL::Types::ID
475
+ deserializer_or_default('ID') do
476
+ TypedOutput.new(
477
+ signature: "String",
478
+ deserializer: 'T.cast(raw_value, String)',
479
+ )
480
+ end
481
+ elsif graphql_type == GraphQL::Types::ISO8601Date
482
+ deserializer_or_default(T.unsafe(GraphQL::Types::ISO8601Date).graphql_name) do
483
+ output_type_from_scalar_converter(Converters::Date)
484
+ end
485
+ elsif graphql_type == GraphQL::Types::ISO8601DateTime
486
+ deserializer_or_default(T.unsafe(GraphQL::Types::ISO8601DateTime).graphql_name) do
487
+ output_type_from_scalar_converter(Converters::Time)
488
+ end
489
+ elsif graphql_type == GraphQL::Types::Int
490
+ TypedOutput.new(
491
+ signature: "Integer",
492
+ deserializer: 'T.cast(raw_value, Integer)',
493
+ )
494
+ elsif graphql_type == GraphQL::Types::Float
495
+ TypedOutput.new(
496
+ signature: "Float",
497
+ deserializer: 'T.cast(raw_value, Float)',
498
+ )
499
+ elsif graphql_type == GraphQL::Types::String
500
+ TypedOutput.new(
501
+ signature: "String",
502
+ deserializer: 'T.cast(raw_value, String)',
503
+ )
504
+ else
505
+ type_kind = graphql_type.kind
506
+ if type_kind.enum?
507
+ klass_name = enum_class(graphql_type)
508
+ dependencies.push(klass_name)
509
+
510
+ TypedOutput.new(
511
+ signature: klass_name,
512
+ deserializer: "#{klass_name}.deserialize(raw_value)",
513
+ )
514
+ elsif type_kind.scalar?
515
+ deserializer_or_default(graphql_type.graphql_name) do
516
+ TypedOutput.new(
517
+ signature: T.unsafe(Yogurt::SCALAR_TYPE).name,
518
+ deserializer: "T.cast(raw_value, #{T.unsafe(Yogurt::SCALAR_TYPE).name})",
519
+ )
520
+ end
521
+ elsif type_kind.composite?
522
+ generate_result_class(
523
+ next_module_name,
524
+ graphql_type,
525
+ subselections,
526
+ dependencies: dependencies,
527
+ )
528
+ else
529
+ raise "Unknown GraphQL type kind: #{graphql_type.graphql_name} (#{graphql_type.kind.inspect})"
530
+ end
531
+ end
532
+ end
533
+
534
+ sig {params(variable: T.any(GraphQL::Language::Nodes::VariableDefinition, GraphQL::Schema::Argument)).returns(VariableDefinition)}
535
+ def variable_definition(variable)
536
+ wrappers = T.let([], T::Array[TypeWrapper])
537
+ fully_unwrapped_type = T.let(variable.type, T.untyped)
538
+
539
+ skip_nilable = T.let(false, T::Boolean)
540
+ array_wrappers = 0
541
+
542
+ loop do
543
+ non_null = fully_unwrapped_type.is_a?(GraphQL::Schema::NonNull) || fully_unwrapped_type.is_a?(GraphQL::Language::Nodes::NonNullType)
544
+ if non_null
545
+ fully_unwrapped_type = T.unsafe(fully_unwrapped_type).of_type
546
+ skip_nilable = true
547
+ next
548
+ end
549
+
550
+ wrappers << TypeWrapper::NILABLE if !skip_nilable
551
+ skip_nilable = false
552
+
553
+ list = fully_unwrapped_type.is_a?(GraphQL::Schema::List) || fully_unwrapped_type.is_a?(GraphQL::Language::Nodes::ListType)
554
+ if list
555
+ wrappers << TypeWrapper::ARRAY
556
+ array_wrappers += 1
557
+ fully_unwrapped_type = T.unsafe(fully_unwrapped_type).of_type
558
+ next
559
+ end
560
+
561
+ break
562
+ end
563
+
564
+ core_input_type = unwrapped_graphql_type_to_input_type(fully_unwrapped_type)
565
+ variable_name = underscore(variable.name).to_sym
566
+ signature = core_input_type.signature
567
+ serializer = core_input_type.serializer
568
+
569
+ wrappers.reverse_each do |wrapper|
570
+ case wrapper
571
+ when TypeWrapper::NILABLE
572
+ signature = "T.nilable(#{signature})"
573
+ serializer = <<~STRING
574
+ if raw_value
575
+ #{indent(serializer, 1).strip}
576
+ end
577
+ STRING
578
+ when TypeWrapper::ARRAY
579
+ signature = "T::Array[#{signature}]"
580
+ intermediate_name = "#{variable_name}#{array_wrappers}"
581
+ serializer = serializer.gsub(/\braw_value\b/, intermediate_name)
582
+ serializer = <<~STRING
583
+ raw_value.map do |#{intermediate_name}|
584
+ #{indent(serializer, 1).strip}
585
+ end
586
+ STRING
587
+ else
588
+ T.absurd(wrapper)
589
+ end
590
+ end
591
+
592
+ serializer = serializer.gsub(/\braw_value\b/, variable_name.to_s)
593
+
594
+ VariableDefinition.new(
595
+ name: variable_name,
596
+ graphql_name: variable.name,
597
+ signature: signature,
598
+ serializer: serializer.strip,
599
+ dependency: core_input_type.dependency,
600
+ )
601
+ end
602
+
603
+ sig {params(scalar_converter: SCALAR_CONVERTER).returns(TypedInput)}
604
+ def input_type_from_scalar_converter(scalar_converter)
605
+ name = scalar_converter.name
606
+ raise "Expected scalar deserializer to be assigned to a constant" if name.nil?
607
+
608
+ TypedInput.new(
609
+ signature: scalar_converter.type_alias.name,
610
+ serializer: "#{name}.serialize(raw_value)",
611
+ )
612
+ end
613
+
614
+ sig do
615
+ params(
616
+ type_name: String,
617
+ block: T.proc.returns(TypedInput),
618
+ ).returns(TypedInput)
619
+ end
620
+ def serializer_or_default(type_name, &block)
621
+ deserializer = @scalars[type_name]
622
+ return input_type_from_scalar_converter(deserializer) if deserializer
623
+
624
+ yield
625
+ end
626
+
627
+ sig do
628
+ params(graphql_type: T.untyped).returns(TypedInput)
629
+ end
630
+ def unwrapped_graphql_type_to_input_type(graphql_type)
631
+ graphql_type = schema.types[T.unsafe(graphql_type).name] if graphql_type.is_a?(GraphQL::Language::Nodes::TypeName)
632
+
633
+ if graphql_type == GraphQL::Types::Boolean
634
+ TypedInput.new(
635
+ signature: "T::Boolean",
636
+ serializer: "raw_value",
637
+ )
638
+ elsif graphql_type == GraphQL::Types::BigInt
639
+ serializer_or_default(T.unsafe(GraphQL::Types::BigInt).graphql_name) do
640
+ TypedInput.new(
641
+ signature: "Integer",
642
+ serializer: "raw_value",
643
+ )
644
+ end
645
+ elsif graphql_type == GraphQL::Types::ID
646
+ serializer_or_default('ID') do
647
+ TypedInput.new(
648
+ signature: "String",
649
+ serializer: "raw_value",
650
+ )
651
+ end
652
+ elsif graphql_type == GraphQL::Types::ISO8601Date
653
+ serializer_or_default(T.unsafe(GraphQL::Types::ISO8601Date).graphql_name) do
654
+ input_type_from_scalar_converter(Converters::Date)
655
+ end
656
+ elsif graphql_type == GraphQL::Types::ISO8601DateTime
657
+ serializer_or_default(T.unsafe(GraphQL::Types::ISO8601DateTime).graphql_name) do
658
+ input_type_from_scalar_converter(Converters::Time)
659
+ end
660
+ elsif graphql_type == GraphQL::Types::Int
661
+ TypedInput.new(
662
+ signature: "Integer",
663
+ serializer: "raw_value",
664
+ )
665
+ elsif graphql_type == GraphQL::Types::Float
666
+ TypedInput.new(
667
+ signature: "Float",
668
+ serializer: "raw_value",
669
+ )
670
+ elsif graphql_type == GraphQL::Types::String
671
+ TypedInput.new(
672
+ signature: "String",
673
+ serializer: "raw_value",
674
+ )
675
+ elsif graphql_type.is_a?(Class)
676
+ if graphql_type < GraphQL::Schema::Enum
677
+ klass_name = enum_class(graphql_type)
678
+
679
+ TypedInput.new(
680
+ signature: klass_name,
681
+ serializer: "raw_value.serialize",
682
+ dependency: klass_name,
683
+ )
684
+ elsif graphql_type < GraphQL::Schema::Scalar
685
+ serializer_or_default(graphql_type.graphql_name) do
686
+ TypedInput.new(
687
+ signature: T.unsafe(Yogurt::SCALAR_TYPE).name,
688
+ serializer: "raw_value",
689
+ )
690
+ end
691
+ elsif graphql_type < GraphQL::Schema::InputObject
692
+ klass_name = input_class(T.unsafe(graphql_type).graphql_name)
693
+ TypedInput.new(
694
+ signature: klass_name,
695
+ serializer: "raw_value.serialize",
696
+ dependency: klass_name,
697
+ )
698
+ else
699
+ raise "Unknown GraphQL type: #{graphql_type.inspect}"
700
+ end
701
+ else
702
+ raise "Unknown GraphQL type: #{graphql_type.inspect}"
703
+ end
704
+ end
705
+ end
706
+ end