Skip to content

Commit eedf7ea

Browse files
authored
Introspection
Introspection resolvers
2 parents e7a5a45 + 87bad01 commit eedf7ea

File tree

10 files changed

+963
-25
lines changed

10 files changed

+963
-25
lines changed

Rakefile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ namespace :benchmark do
3232
GraphQLBenchmark.benchmark_lazy_execution
3333
end
3434

35+
desc "Benchmark introspection"
36+
task :introspection do
37+
prepare_benchmark
38+
GraphQLBenchmark.benchmark_introspection
39+
end
40+
3541
desc "Memory profile"
3642
task :memory do
3743
prepare_benchmark

benchmark/run.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,28 @@ def benchmark_lazy_execution
9393
end
9494
end
9595

96+
def benchmark_introspection
97+
document = GraphQL.parse(GraphQL::Introspection.query)
98+
99+
Benchmark.ips do |x|
100+
x.report("graphql-ruby: introspection") do
101+
GRAPHQL_GEM_SCHEMA.execute(document: document)
102+
end
103+
104+
x.report("graphql-cardinal introspection") do
105+
GraphQL::Cardinal::Executor.new(
106+
SCHEMA,
107+
BREADTH_RESOLVERS,
108+
document,
109+
{},
110+
tracers: [CARDINAL_TRACER],
111+
).perform
112+
end
113+
114+
x.compare!
115+
end
116+
end
117+
96118
def memory_profile
97119
default_data_sizes = "10, 1000"
98120
sizes = ENV.fetch("SIZES", default_data_sizes).split(",").map(&:to_i)

lib/graphql/cardinal.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@ module Cardinal
1515
require_relative "cardinal/loader"
1616
require_relative "cardinal/tracer"
1717
require_relative "cardinal/field_resolvers"
18+
require_relative "cardinal/introspection"
1819
require_relative "cardinal/executor"
1920
require_relative "cardinal/version"

lib/graphql/cardinal/executor.rb

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def initialize(schema, resolvers, document, root_object, variables: {}, context:
2424
@root_object = root_object
2525
@tracers = tracers
2626
@variables = variables
27-
@context = context
27+
@context = @query.context
2828
@data = {}
2929
@errors = []
3030
@exec_queue = []
@@ -211,10 +211,8 @@ def resolve_execution_field(exec_field, resolved_sources)
211211
# DANGER: HOT PATH!
212212
parent_responses[i][field_key] = if val.nil? || val.is_a?(StandardError)
213213
build_missing_value(exec_field, field_type, val)
214-
elsif return_type.kind.scalar?
215-
coerce_scalar_value(return_type, val)
216-
elsif return_type.kind.enum?
217-
coerce_enum_value(return_type, val)
214+
elsif return_type.kind.leaf?
215+
coerce_leaf_value(exec_field, field_type, val)
218216
else
219217
val
220218
end

lib/graphql/cardinal/executor/hot_paths.rb

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
module GraphQL::Cardinal
44
class Executor
55
module HotPaths
6+
INCORRECT_LIST_VALUE = "Incorrect result for list field. Expected Array, got ".freeze
7+
68
# DANGER: HOT PATH!
79
# Overhead added here scales dramatically...
810
def build_composite_response(exec_field, current_type, source, next_sources, next_responses)
@@ -13,7 +15,7 @@ def build_composite_response(exec_field, current_type, source, next_sources, nex
1315
build_missing_value(exec_field, current_type, source)
1416
elsif current_type.list?
1517
unless source.is_a?(Array)
16-
report_exception("Incorrect result for list field. Expected Array, got #{source.class}", field: exec_field)
18+
report_exception("#{INCORRECT_LIST_VALUE}#{source.class}", field: exec_field)
1719
return build_missing_value(exec_field, current_type, nil)
1820
end
1921

@@ -47,28 +49,25 @@ def build_missing_value(exec_field, current_type, val)
4749

4850
# DANGER: HOT PATH!
4951
# Overhead added here scales dramatically...
50-
def coerce_scalar_value(type, value)
51-
case type.graphql_name
52-
when "String"
53-
value.is_a?(String) ? value : value.to_s
54-
when "ID"
55-
value.is_a?(String) || value.is_a?(Numeric) ? value : value.to_s
56-
when "Int"
57-
value.is_a?(Integer) ? value : Integer(value)
58-
when "Float"
59-
value.is_a?(Float) ? value : Float(value)
60-
when "Boolean"
61-
value == TrueClass || value == FalseClass ? value : !!value
52+
def coerce_leaf_value(exec_field, current_type, val)
53+
if current_type.list?
54+
unless val.is_a?(Array)
55+
report_exception("#{INCORRECT_LIST_VALUE}#{val.class}", field: exec_field)
56+
return build_missing_value(exec_field, current_type, nil)
57+
end
58+
59+
current_type = current_type.of_type while current_type.non_null?
60+
61+
val.each { coerce_leaf_value(exec_field, current_type.of_type, _1) }
6262
else
63-
value
63+
begin
64+
current_type.unwrap.coerce_result(val, @context)
65+
rescue StandardError => e
66+
report_exception("Coercion error", field: exec_field)
67+
build_missing_value(exec_field, current_type, val)
68+
end
6469
end
6570
end
66-
67-
# DANGER: HOT PATH!
68-
# Overhead added here scales dramatically...
69-
def coerce_enum_value(type, value)
70-
value
71-
end
7271
end
7372
end
7473
end

lib/graphql/cardinal/field_resolvers.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,18 @@ def resolve(objects, _args, _ctx, _scope)
3636
end
3737
end
3838

39+
class MethodResolver < FieldResolver
40+
def initialize(name)
41+
@name = name
42+
end
43+
44+
def resolve(objects, _args, _ctx, _scope)
45+
map_sources(objects) do |obj|
46+
obj.public_send(@name)
47+
end
48+
end
49+
end
50+
3951
class TypenameResolver < FieldResolver
4052
def resolve(objects, _args, _ctx, scope)
4153
typename = scope.parent_type.graphql_name.freeze
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
# frozen_string_literal: true
2+
3+
module GraphQL
4+
module Cardinal
5+
module Introspection
6+
module Schema
7+
class EndpointResolver < FieldResolver
8+
def resolve(objects, _args, ctx, _exec_field)
9+
Array.new(objects.length, ctx.query.schema)
10+
end
11+
end
12+
13+
class TypesResolver < FieldResolver
14+
def resolve(objects, _args, ctx, _exec_field)
15+
types = ctx.query.types.all_types
16+
Array.new(objects.length, types)
17+
end
18+
end
19+
20+
class DirectivesResolver < FieldResolver
21+
def resolve(objects, _args, ctx, _exec_field)
22+
directives = ctx.query.types.directives
23+
Array.new(objects.length, directives)
24+
end
25+
end
26+
end
27+
28+
module Type
29+
class EndpointResolver < FieldResolver
30+
def resolve(objects, args, ctx, _exec_field)
31+
type = ctx.query.get_type(args["name"])
32+
Array.new(objects.length, type)
33+
end
34+
end
35+
36+
class TypeKindResolver < FieldResolver
37+
def resolve(objects, _args, ctx, _exec_field)
38+
map_sources(objects) do |type|
39+
type.kind.name
40+
end
41+
end
42+
end
43+
44+
class EnumValuesResolver < FieldResolver
45+
def resolve(objects, args, ctx, _exec_field)
46+
map_sources(objects) do |type|
47+
if type.kind.enum?
48+
enum_values = ctx.query.types.enum_values(type)
49+
enum_values = enum_values.reject(&:deprecation_reason) unless args["includeDeprecated"]
50+
enum_values
51+
end
52+
end
53+
end
54+
end
55+
56+
class FieldsResolver < FieldResolver
57+
def resolve(objects, args, ctx, _exec_field)
58+
map_sources(objects) do |type|
59+
if type.kind.fields?
60+
fields = ctx.query.types.fields(type)
61+
fields = fields.reject(&:deprecation_reason) unless args["includeDeprecated"]
62+
fields
63+
end
64+
end
65+
end
66+
end
67+
68+
class InputFieldsResolver < FieldResolver
69+
def resolve(objects, args, ctx, _exec_field)
70+
map_sources(objects) do |type|
71+
if type.kind.input_object?
72+
fields = ctx.query.types.arguments(type)
73+
fields = fields.reject(&:deprecation_reason) unless args["includeDeprecated"]
74+
fields
75+
end
76+
end
77+
end
78+
end
79+
80+
class InterfacesResolver < FieldResolver
81+
def resolve(objects, args, ctx, _exec_field)
82+
map_sources(objects) do |type|
83+
ctx.query.types.interfaces(type) if type.kind.fields?
84+
end
85+
end
86+
end
87+
88+
class PossibleTypesResolver < FieldResolver
89+
def resolve(objects, args, ctx, _exec_field)
90+
map_sources(objects) do |type|
91+
ctx.query.possible_types(type) if type.kind.abstract?
92+
end
93+
end
94+
end
95+
96+
class OfTypeResolver < FieldResolver
97+
def resolve(objects, args, ctx, _exec_field)
98+
map_sources(objects) do |type|
99+
type.of_type if type.kind.wraps?
100+
end
101+
end
102+
end
103+
104+
class SpecifiedByUrlResolver < FieldResolver
105+
def resolve(objects, args, ctx, _exec_field)
106+
map_sources(objects) do |type|
107+
type.specified_by_url if type.kind.scalar?
108+
end
109+
end
110+
end
111+
end
112+
113+
class ArgumentsResolver < FieldResolver
114+
def resolve(objects, args, ctx, _exec_field)
115+
map_sources(objects) do |owner|
116+
owner_args = ctx.query.types.arguments(owner)
117+
owner_args = owner_args.reject(&:deprecation_reason) unless args["includeDeprecated"]
118+
owner_args
119+
end
120+
end
121+
end
122+
123+
class ArgumentDefaultValueResolver < FieldResolver
124+
def resolve(objects, args, ctx, _exec_field)
125+
builder = nil
126+
printer = nil
127+
map_sources(objects) do |arg|
128+
next nil unless arg.default_value?
129+
130+
builder ||= GraphQL::Language::DocumentFromSchemaDefinition.new(ctx.query.schema, context: ctx)
131+
printer ||= GraphQL::Language::Printer.new
132+
printer.print(builder.build_default_value(arg.default_value, arg.type))
133+
end
134+
end
135+
end
136+
137+
class IsDeprecatedResolver < FieldResolver
138+
def resolve(objects, args, ctx, _exec_field)
139+
map_sources(objects) { !!_1.deprecation_reason }
140+
end
141+
end
142+
143+
ENTRYPOINT_RESOLVERS = {
144+
"__schema" => Schema::EndpointResolver.new,
145+
"__type" => Type::EndpointResolver.new,
146+
}.freeze
147+
148+
TYPE_RESOLVERS = {
149+
"__Schema" => {
150+
"description" => MethodResolver.new(:description),
151+
"directives" => Schema::DirectivesResolver.new,
152+
"mutationType" => MethodResolver.new(:mutation),
153+
"queryType" => MethodResolver.new(:query),
154+
"subscriptionType" => MethodResolver.new(:subscription),
155+
"types" => Schema::TypesResolver.new,
156+
},
157+
"__Type" => {
158+
"description" => MethodResolver.new(:description),
159+
"enumValues" => Type::EnumValuesResolver.new,
160+
"fields" => Type::FieldsResolver.new,
161+
"inputFields" => Type::InputFieldsResolver.new,
162+
"interfaces" => Type::InterfacesResolver.new,
163+
"kind" => Type::TypeKindResolver.new,
164+
"name" => MethodResolver.new(:graphql_name),
165+
"ofType" => Type::OfTypeResolver.new,
166+
"possibleTypes" => Type::PossibleTypesResolver.new,
167+
"specifiedByURL" => Type::SpecifiedByUrlResolver.new,
168+
},
169+
"__Field" => {
170+
"args" => ArgumentsResolver.new,
171+
"deprecationReason" => MethodResolver.new(:deprecation_reason),
172+
"description" => MethodResolver.new(:description),
173+
"isDeprecated" => IsDeprecatedResolver.new,
174+
"name" => MethodResolver.new(:graphql_name),
175+
"type" => MethodResolver.new(:type),
176+
},
177+
"__InputValue" => {
178+
"defaultValue" => ArgumentDefaultValueResolver.new,
179+
"deprecationReason" => MethodResolver.new(:deprecation_reason),
180+
"description" => MethodResolver.new(:description),
181+
"isDeprecated" => IsDeprecatedResolver.new,
182+
"name" => MethodResolver.new(:graphql_name),
183+
"type" => MethodResolver.new(:type),
184+
},
185+
"__EnumValue" => {
186+
"deprecationReason" => MethodResolver.new(:deprecation_reason),
187+
"description" => MethodResolver.new(:description),
188+
"isDeprecated" => IsDeprecatedResolver.new,
189+
"name" => MethodResolver.new(:graphql_name),
190+
},
191+
"__Directive" => {
192+
"args" => ArgumentsResolver.new,
193+
"description" => MethodResolver.new(:description),
194+
"isRepeatable" => MethodResolver.new(:repeatable?),
195+
"locations" => MethodResolver.new(:locations),
196+
"name" => MethodResolver.new(:graphql_name),
197+
}
198+
}.freeze
199+
end
200+
end
201+
end

test/fixtures.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ def perform(keys)
9696
end
9797

9898
BREADTH_RESOLVERS = {
99+
**GraphQL::Cardinal::Introspection::TYPE_RESOLVERS,
99100
"Node" => {
100101
"id" => GraphQL::Cardinal::HashKeyResolver.new("id"),
101102
"__type__" => ->(obj, ctx) { ctx[:query].get_type(obj["__typename__"]) },
@@ -130,6 +131,7 @@ def perform(keys)
130131
"value" => GraphQL::Cardinal::HashKeyResolver.new("value"),
131132
},
132133
"Query" => {
134+
**GraphQL::Cardinal::Introspection::ENTRYPOINT_RESOLVERS,
133135
"products" => GraphQL::Cardinal::HashKeyResolver.new("products"),
134136
"nodes" => GraphQL::Cardinal::HashKeyResolver.new("nodes"),
135137
"node" => GraphQL::Cardinal::HashKeyResolver.new("node"),

0 commit comments

Comments
 (0)