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.
- 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
|