Skip to content

Commit d256dd2

Browse files
author
Robert Mosolgo
authored
Merge pull request rmosolgo#1437 from rmosolgo/field-methods-super
Support `super` in field methods
2 parents 89c310e + 7674a59 commit d256dd2

File tree

8 files changed

+230
-29
lines changed

8 files changed

+230
-29
lines changed

lib/graphql/introspection/base_object.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ def self.field(*args, **kwargs, &block)
99

1010
def self.inherited(child_class)
1111
child_class.introspection(true)
12+
super
1213
end
1314
end
1415
end

lib/graphql/schema/field.rb

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -232,39 +232,49 @@ def resolve_field(obj, args, ctx)
232232
inner_obj = obj && obj.object
233233
prev_resolve.call(inner_obj, args, ctx)
234234
else
235-
resolve_field_dynamic(obj, args, ctx)
235+
public_send_field(obj, @method_sym, args, ctx)
236236
end
237237
end
238238

239-
private
240-
241-
# Try a few ways to resolve the field
242-
# @api private
243-
def resolve_field_dynamic(obj, args, ctx)
244-
if obj.respond_to?(@method_sym)
245-
public_send_field(obj, @method_sym, args, ctx)
246-
elsif obj.object.is_a?(Hash)
239+
# Find a way to resolve this field, checking:
240+
#
241+
# - Hash keys, if the wrapped object is a hash;
242+
# - A method on the wrapped object;
243+
# - Or, raise not implemented.
244+
#
245+
# This can be overridden by defining a method on the object type.
246+
# @param obj [GraphQL::Schema::Object]
247+
# @param ruby_kwargs [Hash<Symbol => Object>]
248+
# @param ctx [GraphQL::Query::Context]
249+
def resolve_field_method(obj, ruby_kwargs, ctx)
250+
if obj.object.is_a?(Hash)
247251
inner_object = obj.object
248252
if inner_object.key?(@method_sym)
249253
inner_object[@method_sym]
250254
else
251255
inner_object[@method_str]
252256
end
253257
elsif obj.object.respond_to?(@method_sym)
254-
public_send_field(obj.object, @method_sym, args, ctx)
258+
if ruby_kwargs.any?
259+
obj.object.public_send(@method_sym, **ruby_kwargs)
260+
else
261+
obj.object.public_send(@method_sym)
262+
end
255263
else
256264
raise <<-ERR
257-
Failed to implement #{ctx.irep_node.owner_type.name}.#{ctx.field.name}, tried:
265+
Failed to implement #{@owner.graphql_name}.#{@name}, tried:
258266
259-
- `#{obj.class}##{@method_sym}`, which did not exist
260-
- `#{obj.object.class}##{@method_sym}`, which did not exist
261-
- Looking up hash key `#{@method_sym.inspect}` or `#{@method_str.inspect}` on `#{obj.object}`, but it wasn't a Hash
267+
- `#{obj.class}##{@method_sym}`, which did not exist
268+
- `#{obj.object.class}##{@method_sym}`, which did not exist
269+
- Looking up hash key `#{@method_sym.inspect}` or `#{@method_str.inspect}` on `#{obj.object}`, but it wasn't a Hash
262270
263-
To implement this field, define one of the methods above (and check for typos)
264-
ERR
271+
To implement this field, define one of the methods above (and check for typos)
272+
ERR
265273
end
266274
end
267275

276+
private
277+
268278
NO_ARGS = {}.freeze
269279

270280
def public_send_field(obj, method_name, graphql_args, field_ctx)

lib/graphql/schema/member/has_fields.rb

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,47 @@ class Schema
44
class Member
55
# Shared code for Object and Interface
66
module HasFields
7+
class << self
8+
# When this module is added to a class,
9+
# add a place for that class's default behaviors
10+
def self.extended(child_class)
11+
add_default_resolve_module(child_class)
12+
super
13+
end
14+
15+
# Create a module which will have instance methods for implementing fields.
16+
# These will be `super` methods for fields in interfaces, objects and mutations.
17+
# Use an instance variable on the class instead of a constant
18+
# so that module namespaces won't be an issue. (If we used constants,
19+
# `child_class::DefaultResolve` might find a constant from an included module.)
20+
def add_default_resolve_module(child_class)
21+
if child_class.instance_variable_get(:@_default_resolve)
22+
# This can happen when an object implements an interface,
23+
# since that interface has the `included` hook above.
24+
return
25+
end
26+
27+
default_resolve_module = Module.new
28+
child_class.instance_variable_set(:@_default_resolve, default_resolve_module)
29+
child_class.include(default_resolve_module)
30+
end
31+
end
32+
33+
# When this is included into interfaces,
34+
# add a place for default field behaviors
35+
def included(child_class)
36+
HasFields.add_default_resolve_module(child_class)
37+
# Also, prepare a place for default field implementations
38+
super
39+
end
40+
41+
# When a subclass of objects are created,
42+
# add a place for that subclass's default field behaviors
43+
def inherited(child_class)
44+
HasFields.add_default_resolve_module(child_class)
45+
super
46+
end
47+
748
# Add a field to this object or interface with the given definition
849
# @see {GraphQL::Schema::Field#initialize} for method signature
950
# @return [void]
@@ -25,11 +66,29 @@ def fields
2566
all_fields
2667
end
2768

28-
# Register this field with the class, overriding a previous one if needed
69+
# Register this field with the class, overriding a previous one if needed.
70+
# Also, add a parent method for resolving this field.
2971
# @param field_defn [GraphQL::Schema::Field]
3072
# @return [void]
3173
def add_field(field_defn)
3274
own_fields[field_defn.name] = field_defn
75+
# Add a `super` method for implementing this field:
76+
field_key = field_defn.name.inspect
77+
default_resolve_module = self.instance_variable_get(:@_default_resolve)
78+
if default_resolve_module.nil?
79+
# This should have been set up in one of the inherited or included hooks above,
80+
# if it wasn't, it's because those hooks weren't called because `super` wasn't present.
81+
raise <<-ERR
82+
Uh oh! #{self} doesn't have a default resolve module. This probably means that an `inherited` hook didn't call super.
83+
Check `inherited` on #{self}'s superclasses.
84+
ERR
85+
end
86+
default_resolve_module.module_eval <<-RUBY, __FILE__, __LINE__ + 1
87+
def #{field_defn.method_sym}(**args)
88+
field_inst = self.class.fields[#{field_key}] || raise(%|Failed to find field #{field_key} for \#{self.class} among \#{self.class.fields.keys}|)
89+
field_inst.resolve_field_method(self, args, context)
90+
end
91+
RUBY
3392
nil
3493
end
3594

lib/graphql/schema/mutation.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ def resolve(**args)
8585
class << self
8686
def inherited(base)
8787
base.null(null)
88+
super
8889
end
8990

9091
# Override the method from HasFields to support `field: Mutation.field`, for backwards compat.

lib/graphql/schema/relay_classic_mutation.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ class RelayClassicMutation < GraphQL::Schema::Mutation
2626
class << self
2727
def inherited(base)
2828
base.null(true)
29+
super
2930
end
3031

3132
# The base class for generated input object types
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# frozen_string_literal: true
2+
require "spec_helper"
3+
4+
describe GraphQL::Schema::Member::HasFields do
5+
module SuperTest
6+
class BaseObject < GraphQL::Schema::Object
7+
end
8+
9+
module BaseInterface
10+
include GraphQL::Schema::Interface
11+
end
12+
13+
module InterfaceWithFloatField
14+
include BaseInterface
15+
field :float, Float, null: false
16+
def float
17+
# This should call the default implementation
18+
super * 0.5
19+
end
20+
end
21+
22+
module SubInterfaceWithFloatField
23+
include InterfaceWithFloatField
24+
def float
25+
# This should call `InterfaceWithFloatField#float`
26+
super * 0.1
27+
end
28+
end
29+
30+
class ObjectWithFloatField < BaseObject
31+
implements InterfaceWithFloatField
32+
end
33+
34+
class ObjectWithSubFloatField < BaseObject
35+
implements SubInterfaceWithFloatField
36+
end
37+
38+
module InterfaceWithStringField
39+
include BaseInterface
40+
field :string, String, null: false
41+
def string
42+
# Return a literal value to ensure this method was called
43+
"here's a string"
44+
end
45+
end
46+
47+
class ObjectWithStringField < BaseObject
48+
implements InterfaceWithStringField
49+
def string
50+
# This should call to `InterfaceWithStringField#string`
51+
super.upcase
52+
end
53+
end
54+
55+
class SubObjectWithStringField < ObjectWithStringField
56+
def string
57+
# This should call to `ObjectWithStringField#string`
58+
super.reverse
59+
end
60+
end
61+
62+
class Query < BaseObject
63+
field :int, Integer, null: false
64+
def int
65+
# This should call default resolution
66+
super * 2
67+
end
68+
69+
field :string1, ObjectWithStringField, null: false
70+
alias :string1 :object
71+
72+
field :string2, SubObjectWithStringField, null: false
73+
alias :string2 :object
74+
75+
field :float1, ObjectWithFloatField, null: false
76+
alias :float1 :object
77+
78+
field :float2, ObjectWithSubFloatField, null: false
79+
alias :float2 :object
80+
end
81+
82+
class Schema < GraphQL::Schema
83+
query(Query)
84+
end
85+
end
86+
87+
describe "Calling super in field methods" do
88+
# Test that calling `super` in field methods "works", which means:
89+
# - If there is a super method in the user-created hierarchy (either a class or module), it is called
90+
# This is tested by putting random transformations in method bodies,
91+
# then asserting that they are called.
92+
# - If there's no user-defined super method, it calls the built-in default behavior
93+
# This is tested by putting values in the `root_value` hash.
94+
# The default behavior is to fetch hash values by key, so we assert that
95+
# those values are subject to the specified transformations.
96+
97+
describe "Object methods" do
98+
it "may call super to default implementation" do
99+
res = SuperTest::Schema.execute("{ int }", root_value: { int: 4 })
100+
assert_equal 8, res["data"]["int"]
101+
end
102+
103+
it "may call super to interface method" do
104+
res = SuperTest::Schema.execute(" { string1 { string } }", root_value: {})
105+
assert_equal "HERE'S A STRING", res["data"]["string1"]["string"]
106+
end
107+
108+
it "may call super to superclass method" do
109+
res = SuperTest::Schema.execute(" { string2 { string } }", root_value: {})
110+
assert_equal "GNIRTS A S'EREH", res["data"]["string2"]["string"]
111+
end
112+
end
113+
114+
describe "Interface methods" do
115+
it "may call super to interface method" do
116+
res = SuperTest::Schema.execute(" { float1 { float } }", root_value: { float: 6.0 })
117+
assert_equal 3.0, res["data"]["float1"]["float"]
118+
end
119+
120+
it "may call super to superclass method" do
121+
res = SuperTest::Schema.execute(" { float2 { float } }", root_value: { float: 6.0 })
122+
assert_in_delta 0.001, 0.3, res["data"]["float2"]["float"]
123+
end
124+
end
125+
end
126+
end

spec/graphql/schema/object_spec.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
assert_equal "Ensemble", object_class.graphql_name
99
assert_equal "A group of musicians playing together", object_class.description
1010
assert_equal 6, object_class.fields.size
11-
assert_equal 2, object_class.interfaces.size
11+
assert_equal 3, object_class.interfaces.size
1212
# Compatibility methods are delegated to the underlying BaseType
1313
assert object_class.respond_to?(:connection_type)
1414
end
@@ -22,7 +22,7 @@
2222
# one more than the parent class
2323
assert_equal 7, new_object_class.fields.size
2424
# inherited interfaces are present
25-
assert_equal 2, new_object_class.interfaces.size
25+
assert_equal 3, new_object_class.interfaces.size
2626
# The new field is present
2727
assert new_object_class.fields.key?("newField")
2828
# The overridden field is present:

spec/support/jazz.rb

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -65,15 +65,13 @@ def initialize(*args, **options, &block)
6565
super(*args, **options, &block)
6666
end
6767

68-
def to_graphql
69-
field_defn = super
70-
if @upcase
71-
inner_resolve = field_defn.resolve_proc
72-
field_defn.resolve = ->(obj, args, ctx) {
73-
inner_resolve.call(obj, args, ctx).upcase
74-
}
68+
def resolve_field(*)
69+
result = super
70+
if @upcase && result
71+
result.upcase
72+
else
73+
result
7574
end
76-
field_defn
7775
end
7876
end
7977

@@ -168,15 +166,20 @@ def upcase_name
168166
end
169167
end
170168

169+
module HasMusicians
170+
include BaseInterface
171+
field :musicians, "[Jazz::Musician]", null: false
172+
end
173+
174+
171175
# Here's a new-style GraphQL type definition
172176
class Ensemble < ObjectWithUpcasedName
173177
# Test string type names
174178
# This method should override inherited one
175179
field :name, "String", null: false, method: :overridden_name
176-
implements GloballyIdentifiableType, NamedEntity
180+
implements GloballyIdentifiableType, NamedEntity, HasMusicians
177181
description "A group of musicians playing together"
178182
config :config, :configged
179-
field :musicians, "[Jazz::Musician]", null: false
180183
field :formed_at, String, null: true, hash_key: "formedAtDate"
181184

182185
def overridden_name

0 commit comments

Comments
 (0)