yogurt 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.DS_Store +0 -0
- data/.github/workflows/tests.yml +117 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.rubocop.yml +113 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.md +0 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +82 -0
- data/LICENSE +201 -0
- data/README.md +38 -0
- data/Rakefile +9 -0
- data/bin/bundle +105 -0
- data/bin/byebug +29 -0
- data/bin/coderay +29 -0
- data/bin/console +16 -0
- data/bin/htmldiff +29 -0
- data/bin/ldiff +29 -0
- data/bin/pry +29 -0
- data/bin/rake +29 -0
- data/bin/rspec +29 -0
- data/bin/rubocop +29 -0
- data/bin/ruby-parse +29 -0
- data/bin/ruby-rewrite +29 -0
- data/bin/setup +8 -0
- data/bin/srb +29 -0
- data/bin/srb-rbi +29 -0
- data/lib/yogurt.rb +95 -0
- data/lib/yogurt/code_generator.rb +706 -0
- data/lib/yogurt/code_generator/defined_class.rb +22 -0
- data/lib/yogurt/code_generator/defined_class_sorter.rb +44 -0
- data/lib/yogurt/code_generator/defined_method.rb +24 -0
- data/lib/yogurt/code_generator/enum_class.rb +56 -0
- data/lib/yogurt/code_generator/field_access_method.rb +273 -0
- data/lib/yogurt/code_generator/field_access_path.rb +56 -0
- data/lib/yogurt/code_generator/generated_file.rb +53 -0
- data/lib/yogurt/code_generator/input_class.rb +52 -0
- data/lib/yogurt/code_generator/leaf_class.rb +68 -0
- data/lib/yogurt/code_generator/operation_declaration.rb +12 -0
- data/lib/yogurt/code_generator/root_class.rb +130 -0
- data/lib/yogurt/code_generator/type_wrapper.rb +13 -0
- data/lib/yogurt/code_generator/typed_input.rb +12 -0
- data/lib/yogurt/code_generator/typed_output.rb +16 -0
- data/lib/yogurt/code_generator/utils.rb +140 -0
- data/lib/yogurt/code_generator/variable_definition.rb +33 -0
- data/lib/yogurt/converters.rb +50 -0
- data/lib/yogurt/error_result.rb +30 -0
- data/lib/yogurt/http.rb +80 -0
- data/lib/yogurt/inspectable.rb +22 -0
- data/lib/yogurt/memoize.rb +33 -0
- data/lib/yogurt/query.rb +23 -0
- data/lib/yogurt/query_container.rb +89 -0
- data/lib/yogurt/query_container/interfaces_and_unions_have_typename.rb +107 -0
- data/lib/yogurt/query_declaration.rb +11 -0
- data/lib/yogurt/query_executor.rb +23 -0
- data/lib/yogurt/query_result.rb +25 -0
- data/lib/yogurt/scalar_converter.rb +19 -0
- data/lib/yogurt/unexpected_object_type.rb +30 -0
- data/lib/yogurt/validation_error.rb +6 -0
- data/lib/yogurt/version.rb +6 -0
- data/sorbet/config +10 -0
- data/sorbet/rbi/fake_schema.rbi +14 -0
- data/sorbet/rbi/graphql.rbi +11 -0
- data/sorbet/rbi/hidden-definitions/errors.txt +20513 -0
- data/sorbet/rbi/hidden-definitions/hidden.rbi +42882 -0
- data/sorbet/rbi/sorbet-typed/lib/graphql/all/graphql.rbi +48 -0
- data/sorbet/rbi/sorbet-typed/lib/rainbow/all/rainbow.rbi +276 -0
- data/sorbet/rbi/sorbet-typed/lib/rubocop/~>0.85/rubocop.rbi +2072 -0
- data/sorbet/rbi/todo.rbi +6 -0
- data/yogurt.gemspec +54 -0
- metadata +286 -0
data/bin/ruby-rewrite
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 '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")
|
data/bin/setup
ADDED
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")
|
data/bin/srb-rbi
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-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")
|
data/lib/yogurt.rb
ADDED
@@ -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
|