Skip to content

Commit

Permalink
Merge pull request #834 from rmosolgo/granular-interface-resolve-type
Browse files Browse the repository at this point in the history
feat(InterfaceType) add type-level resolve_type support
  • Loading branch information
Robert Mosolgo committed Jul 13, 2017
2 parents 4f1c12c + dbf2924 commit 4ae1614
Show file tree
Hide file tree
Showing 29 changed files with 169 additions and 57 deletions.
22 changes: 18 additions & 4 deletions lib/graphql/backwards_compatibility.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,23 @@ module BackwardsCompatibility
# check its arity, and if needed, apply a wrapper so that
# it can be called with `to` arguments.
# If a wrapper is applied, warn the application with `name`.
def wrap_arity(callable, from:, to:, name:)
#
# If `last`, then use the last arguments to call the function.
def wrap_arity(callable, from:, to:, name:, last: false)
arity = get_arity(callable)
case arity
when to
# It already matches, return it as is
callable
when from
# It has the old arity, so wrap it with an arity converter
warn("#{name} with #{from} arguments is deprecated, it now accepts #{to} arguments")
ArityWrapper.new(callable, from)
message ="#{name} with #{from} arguments is deprecated, it now accepts #{to} arguments, see:"
backtrace = caller(0, 20)
# Find the first line in the trace that isn't library internals:
user_line = backtrace.find {|l| l !~ /lib\/graphql/ }
warn(message + "\n" + user_line + "\n")
wrapper = last ? LastArgumentsWrapper : FirstArgumentsWrapper
wrapper.new(callable, from)
else
raise "Can't wrap #{callable} (arity: #{arity}) to have arity #{to}"
end
Expand All @@ -32,7 +39,7 @@ def get_arity(callable)
end
end

class ArityWrapper
class FirstArgumentsWrapper
def initialize(callable, old_arity)
@callable = callable
@old_arity = old_arity
Expand All @@ -43,5 +50,12 @@ def call(*args)
@callable.call(*backwards_compat_args)
end
end

class LastArgumentsWrapper < FirstArgumentsWrapper
def call(*args)
backwards_compat_args = args.last(@old_arity)
@callable.call(*backwards_compat_args)
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def self.build(execution_strategy)

schema = GraphQL::Schema.define(
query: query_type,
resolve_type: ->(o, c) { o == :counter ? counter_type : nil },
resolve_type: ->(t, o, c) { o == :counter ? counter_type : nil },
orphan_types: [alt_counter_type, counter_type],
query_execution_strategy: execution_strategy,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ def self.build(execution_strategy)
query_execution_strategy execution_strategy
query query_type

resolve_type ->(obj, ctx) {
resolve_type ->(type, obj, ctx) {
if obj.respond_to?(:birthdate)
person_type
elsif obj.respond_to?(:leader_id)
Expand Down
13 changes: 9 additions & 4 deletions lib/graphql/interface_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,15 @@ module GraphQL
# end
#
class InterfaceType < GraphQL::BaseType
accepts_definitions :fields, field: GraphQL::Define::AssignObjectField
accepts_definitions :fields, :resolve_type, field: GraphQL::Define::AssignObjectField

attr_accessor :fields
ensure_defined :fields
attr_accessor :fields, :resolve_type_proc
ensure_defined :fields, :resolve_type_proc, :resolve_type

def initialize
super
@fields = {}
@resolve_type_proc = nil
end

def initialize_copy(other)
Expand All @@ -43,7 +44,11 @@ def kind
end

def resolve_type(value, ctx)
ctx.query.resolve_type(value)
ctx.query.resolve_type(self, value)
end

def resolve_type=(resolve_type_callable)
@resolve_type_proc = resolve_type_callable
end

# @return [GraphQL::Field] The defined field for `field_name`
Expand Down
18 changes: 15 additions & 3 deletions lib/graphql/query.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,13 @@ def initialize(schema, query_string = nil, query: nil, document: nil, context: n
@query_string = query_string || query
@document = document

@resolved_types_cache = Hash.new { |h, k| h[k] = @schema.resolve_type(k, @context) }
# A two-layer cache of type resolution:
# { abstract_type => { value => resolved_type } }
@resolved_types_cache = Hash.new do |h1, k1|
h1[k1] = Hash.new do |h2, k2|
h2[k2] = @schema.resolve_type(k1, k2, @context)
end
end

@arguments_cache = ArgumentsCache.build(self)

Expand Down Expand Up @@ -187,11 +193,17 @@ def warden

def_delegators :warden, :get_type, :get_field, :possible_types, :root_type_for_operation

# @param abstract_type [GraphQL::UnionType, GraphQL::InterfaceType]
# @param value [Object] Any runtime value
# @return [GraphQL::ObjectType, nil] The runtime type of `value` from {Schema#resolve_type}
# @see {#possible_types} to apply filtering from `only` / `except`
def resolve_type(value)
@resolved_types_cache[value]
def resolve_type(abstract_type, value = :__undefined__)
if value.is_a?(Symbol) && value == :__undefined__
# Old method signature
value = abstract_type
abstract_type = nil
end
@resolved_types_cache[abstract_type][value]
end

def mutation?
Expand Down
25 changes: 20 additions & 5 deletions lib/graphql/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -337,15 +337,29 @@ def execution_strategy_for_operation(operation)
# Determine the GraphQL type for a given object.
# This is required for unions and interfaces (including Relay's `Node` interface)
# @see [GraphQL::Schema::Warden] Restricted access to members of a schema
# @param type [GraphQL::UnionType, GraphQL:InterfaceType] the abstract type which is being resolved
# @param object [Any] An application object which GraphQL is currently resolving on
# @param ctx [GraphQL::Query::Context] The context for the current query
# @return [GraphQL::ObjectType] The type for exposing `object` in GraphQL
def resolve_type(object, ctx)
if @resolve_type_proc.nil?
raise(NotImplementedError, "Can't determine GraphQL type for: #{object.inspect}, define `resolve_type (obj, ctx) -> { ... }` inside `Schema.define`.")
def resolve_type(type, object, ctx = :__undefined__)
if ctx == :__undefined__
# Old method signature
ctx = object
object = type
type = nil
end

# Prefer a type-local function; fall back to the schema-level function
type_proc = type && type.resolve_type_proc
type_result = if type_proc
type_proc.call(object, ctx)
else
if @resolve_type_proc.nil?
raise(NotImplementedError, "Can't determine GraphQL type for: #{object.inspect}, define `resolve_type (obj, ctx) -> { ... }` inside `Schema.define`.")
end
@resolve_type_proc.call(type, object, ctx)
end

type_result = @resolve_type_proc.call(object, ctx)
if type_result.nil?
nil
elsif !type_result.is_a?(GraphQL::BaseType)
Expand All @@ -357,7 +371,8 @@ def resolve_type(object, ctx)
end

def resolve_type=(new_resolve_type_proc)
@resolve_type_proc = new_resolve_type_proc
callable = GraphQL::BackwardsCompatibility.wrap_arity(new_resolve_type_proc, from: 2, to: 3, last: true, name: "Schema#resolve_type(type, obj, ctx)")
@resolve_type_proc = callable
end

# Fetch an application object by its unique id
Expand Down
2 changes: 1 addition & 1 deletion lib/graphql/schema/build_from_definition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ def build(document, default_resolve: DefaultResolve)
schema
end

NullResolveType = ->(obj, ctx) {
NullResolveType = ->(type, obj, ctx) {
raise(NotImplementedError, "Generated Schema cannot use Interface or Union types for execution.")
}

Expand Down
4 changes: 2 additions & 2 deletions lib/graphql/schema/build_from_definition/resolve_map.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ def initialize(user_resolve_hash)

# Check the normalized hash, not the user input:
if @resolve_hash.key?("resolve_type")
define_singleton_method :resolve_type do |type, ctx|
@resolve_hash.fetch("resolve_type").call(type, ctx)
define_singleton_method :resolve_type do |type, obj, ctx|
@resolve_hash.fetch("resolve_type").call(type, obj, ctx)
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/graphql/schema/loader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def load(introspection_result)
Schema.define(**kargs, raise_definition_error: true)
end

NullResolveType = ->(obj, ctx) {
NullResolveType = ->(type, obj, ctx) {
raise(NotImplementedError, "This schema was loaded from string, so it can't resolve types for objects")
}

Expand Down
17 changes: 5 additions & 12 deletions lib/graphql/union_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ module GraphQL
#
class UnionType < GraphQL::BaseType
accepts_definitions :possible_types, :resolve_type
ensure_defined :possible_types
ensure_defined :possible_types, :resolve_type, :resolve_type_proc

attr_accessor :resolve_type_proc

def initialize
super
Expand Down Expand Up @@ -66,16 +68,7 @@ def possible_types
end

def resolve_type(value, ctx)
if !@resolve_type_proc.nil?
resolved_type = @resolve_type_proc.call(value, ctx)
if !include?(resolved_type)
raise(NotImplementedError, "Unrecognized possible type kind: #{resolved_type}")
end

return resolved_type
end

ctx.query.resolve_type(value)
ctx.query.resolve_type(self, value)
end

def resolve_type=(new_resolve_type_proc)
Expand All @@ -84,6 +77,6 @@ def resolve_type=(new_resolve_type_proc)

protected

attr_reader :dirty_possible_types, :resolve_type_proc
attr_reader :dirty_possible_types
end
end
2 changes: 1 addition & 1 deletion spec/graphql/analysis/query_complexity_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@
GraphQL::Schema.define(
query: query_type,
orphan_types: [double_complexity_type],
resolve_type: :pass
resolve_type: ->(a,b,c) { :pass }
)
}
let(:query_string) {%|
Expand Down
44 changes: 44 additions & 0 deletions spec/graphql/interface_type_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,48 @@
assert_equal 4, interface_2.fields.size
end
end

describe "#resolve_type" do
let(:result) { Dummy::Schema.execute(query_string) }
let(:query_string) {%|
{
allEdible {
__typename
... on Milk {
milkFatContent: fatContent
}
... on Cheese {
cheeseFatContent: fatContent
}
}
allEdibleAsMilk {
__typename
... on Milk {
fatContent
}
}
}
|}

it 'returns correct types for general schema and specific interface' do
expected_result = {
# Uses schema-level resolve_type
"allEdible"=>[
{"__typename"=>"Cheese", "cheeseFatContent"=>0.19},
{"__typename"=>"Cheese", "cheeseFatContent"=>0.3},
{"__typename"=>"Cheese", "cheeseFatContent"=>0.065},
{"__typename"=>"Milk", "milkFatContent"=>0.04}
],
# Uses type-level resolve_type
"allEdibleAsMilk"=>[
{"__typename"=>"Milk", "fatContent"=>0.19},
{"__typename"=>"Milk", "fatContent"=>0.3},
{"__typename"=>"Milk", "fatContent"=>0.065},
{"__typename"=>"Milk", "fatContent"=>0.04}
]
}
assert_equal expected_result, result["data"]
end
end
end
1 change: 1 addition & 0 deletions spec/graphql/introspection/schema_type_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
{"name"=>"allAnimalAsCow"},
{"name"=>"allDairy"},
{"name"=>"allEdible"},
{"name"=>"allEdibleAsMilk"},
{"name"=>"cheese"},
{"name"=>"cow"},
{"name"=>"dairy"},
Expand Down
1 change: 1 addition & 0 deletions spec/graphql/introspection/type_type_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"milkType"=>{
"interfaces"=>[
{"name"=>"Edible"},
{"name"=>"EdibleAsMilk"},
{"name"=>"AnimalProduct"},
{"name"=>"LocalProduct"},
],
Expand Down
11 changes: 8 additions & 3 deletions spec/graphql/object_type_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@

describe "interfaces" do
it "may have interfaces" do
assert_equal([Dummy::EdibleInterface, Dummy::AnimalProductInterface, Dummy::LocalProductInterface], type.interfaces)
assert_equal([
Dummy::EdibleInterface,
Dummy::EdibleAsMilkInterface,
Dummy::AnimalProductInterface,
Dummy::LocalProductInterface
], type.interfaces)
end

it "raises if the interfaces arent an array" do
Expand Down Expand Up @@ -129,8 +134,8 @@

type_2.fields["nonsense"] = GraphQL::Field.define(name: "nonsense", type: type)

assert_equal 3, type.interfaces.size
assert_equal 4, type_2.interfaces.size
assert_equal 4, type.interfaces.size
assert_equal 5, type_2.interfaces.size
assert_equal 8, type.fields.size
assert_equal 9, type_2.fields.size
end
Expand Down
2 changes: 1 addition & 1 deletion spec/graphql/query/executor_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@
end
end

GraphQL::Schema.define(query: DummyQueryType, mutation: Dummy::DairyAppMutationType, resolve_type: :pass, id_from_object: :pass)
GraphQL::Schema.define(query: DummyQueryType, mutation: Dummy::DairyAppMutationType, resolve_type: ->(a,b,c) { :pass }, id_from_object: :pass)
}
let(:variables) { nil }
let(:query_string) { %|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
GraphQL::Schema.define do
query(query_root)
orphan_types [some_object]
resolve_type ->(obj, ctx) do
resolve_type ->(type, obj, ctx) do
if obj.is_a?(Symbol)
other_object
else
Expand Down
2 changes: 1 addition & 1 deletion spec/graphql/relay/mutation_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@

GraphQL::Schema.define do
mutation(mutation_root)
resolve_type ->(obj, ctx) { "not really used" }
resolve_type NO_OP_RESOLVE_TYPE
end
}

Expand Down
2 changes: 1 addition & 1 deletion spec/graphql/schema/build_from_definition_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -735,7 +735,7 @@ def capitalize(args)
val.to_f
}
},
resolve_type: ->(obj, ctx) {
resolve_type: ->(type, obj, ctx) {
return ctx.schema.types['A']
},
Query: {
Expand Down
2 changes: 1 addition & 1 deletion spec/graphql/schema/loader_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@
query: query_root,
mutation: mutation_root,
orphan_types: [audio_type, video_type],
resolve_type: :pass,
resolve_type: ->(a,b,c) { :pass },
)
}

Expand Down
2 changes: 1 addition & 1 deletion spec/graphql/schema/printer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@
query: query_root,
mutation: mutation_root,
subscription: subscription_root,
resolve_type: :pass,
resolve_type: ->(a,b,c) { :pass },
orphan_types: [media_union_type]
)
}
Expand Down
Loading

0 comments on commit 4ae1614

Please sign in to comment.