yogurt 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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