Skip to content

Commit

Permalink
Add UnionType#resolve_type for more ganular control in unions
Browse files Browse the repository at this point in the history
The purpose of this feature is to allow a union type to resolve to
more specific types when handling a value. This is useful when working
with objects that may fit into multiple types. When executing the query
will call resolve_type(value, field_ctx) on either Unions or Interfaces.

Union type's call can be definable via `resolve_type ->(value, ctx) { }`.
Interface type's call returns `ctx.query.resolve_type(value)` which is the
default implementation for resolving both types.
  • Loading branch information
coleturner committed Jul 9, 2017
1 parent db45cdb commit ddbf77d
Show file tree
Hide file tree
Showing 8 changed files with 122 additions and 11 deletions.
2 changes: 1 addition & 1 deletion lib/graphql/base_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def unwrap

# Find out which possible type to use for `value`.
# Returns self if there are no possible types (ie, not Union or Interface)
def resolve_type(value)
def resolve_type(value, ctx)
self
end

Expand Down
2 changes: 1 addition & 1 deletion lib/graphql/execution/execute.rb
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ def resolve_value(owner, parent_type, field_defn, field_type, value, selection,
)
when GraphQL::TypeKinds::UNION, GraphQL::TypeKinds::INTERFACE
query = field_ctx.query
resolved_type = query.resolve_type(value)
resolved_type = field_type.resolve_type(value, field_ctx)
possible_types = query.possible_types(field_type)

if !possible_types.include?(resolved_type)
Expand Down
4 changes: 4 additions & 0 deletions lib/graphql/interface_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ def kind
GraphQL::TypeKinds::INTERFACE
end

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

# @return [GraphQL::Field] The defined field for `field_name`
def get_field(field_name)
fields[field_name]
Expand Down
22 changes: 20 additions & 2 deletions lib/graphql/union_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,14 @@ module GraphQL
# }
#
class UnionType < GraphQL::BaseType
accepts_definitions :possible_types
accepts_definitions :possible_types, :resolve_type
ensure_defined :possible_types

def initialize
super
@dirty_possible_types = []
@clean_possible_types = nil
@resolve_type_proc = nil
end

def initialize_copy(other)
Expand Down Expand Up @@ -64,8 +65,25 @@ def possible_types
end
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)
end

def resolve_type=(new_resolve_type_proc)
@resolve_type_proc = new_resolve_type_proc
end

protected

attr_reader :dirty_possible_types
attr_reader :dirty_possible_types, :resolve_type_proc
end
end
2 changes: 2 additions & 0 deletions spec/graphql/introspection/schema_type_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
"types" => Dummy::Schema.types.values.map { |t| t.name.nil? ? (p t; raise("no name for #{t}")) : {"name" => t.name} },
"queryType"=>{
"fields"=>[
{"name"=>"allAnimal"},
{"name"=>"allAnimalAsCow"},
{"name"=>"allDairy"},
{"name"=>"allEdible"},
{"name"=>"cheese"},
Expand Down
52 changes: 52 additions & 0 deletions spec/graphql/union_type_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,58 @@
assert_equal(false, union.include?(type_3))
end

it '#resolve_type raises error if resolved type is not in possible_types' do
assert_raises(NotImplementedError) {
test_str = 'Hello world'
union.resolve_type = ->(value, ctx) {
"This is not the types you are looking for"
}

union.resolve_type(test_str, nil)
}
end

describe "#resolve_type" do
let(:result) { Dummy::Schema.execute(query_string) }
let(:query_string) {%|
{
allAnimal {
type: __typename
... on Cow {
cowName: name
}
... on Goat {
goatName: name
}
}
allAnimalAsCow {
type: __typename
... on Cow {
name
}
}
}
|}

it 'returns correct types for general schema and specific union' do
expected_result = {
# When using Query#resolve_type
"allAnimal" => [
{ "type" => "Cow", "cowName" => "Billy" },
{ "type" => "Goat", "goatName" => "Gilly" }
],

# When using UnionType#resolve_type
"allAnimalAsCow" => [
{ "type" => "Cow", "name" => "Billy" },
{ "type" => "Cow", "name" => "Gilly" }
]
}
assert_equal expected_result, result["data"]
end
end

describe "typecasting from union to union" do
let(:result) { Dummy::Schema.execute(query_string) }
let(:query_string) {%|
Expand Down
14 changes: 9 additions & 5 deletions spec/support/dummy/data.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,13 @@ def ==(other)
milks: [MILKS[1]]
)

COW = OpenStruct.new(
id: 1,
name: "Billy",
last_produced_dairy: MILKS[1]
)
Cow = Struct.new(:id, :name, :last_produced_dairy)
COWS = {
1 => Cow.new(1, "Billy", MILKS[1])
}

Goat = Struct.new(:id, :name, :last_produced_dairy)
GOATS = {
1 => Goat.new(1, "Gilly", MILKS[1]),
}
end
35 changes: 33 additions & 2 deletions spec/support/dummy/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ class NoSuchDairyError < StandardError; end

CowType = GraphQL::ObjectType.define do
name "Cow"
description "A farm where milk is harvested and cheese is produced"
description "A bovine animal that produces milk"
field :id, !types.ID
field :name, types.String
field :last_produced_dairy, DairyProductUnion
Expand All @@ -168,6 +168,29 @@ class NoSuchDairyError < StandardError; end
end
end

GoatType = GraphQL::ObjectType.define do
name "Goat"
description "An caprinae animal that produces milk"
field :id, !types.ID
field :name, types.String
field :last_produced_dairy, DairyProductUnion
end

AnimalUnion = GraphQL::UnionType.define do
name "Animal"
description "Species of living things"
possible_types [CowType, GoatType]
end

AnimalAsCowUnion = GraphQL::UnionType.define do
name "AnimalAsCow"
description "All animals go mooooo!"
possible_types [CowType]
resolve_type ->(obj, ctx) {
CowType
}
end

ResourceOrderType = GraphQL::InputObjectType.define {
name "ResourceOrderType"
description "Properties used to determine ordering"
Expand Down Expand Up @@ -271,7 +294,7 @@ def call(obj, args, ctx)
field :dairy, function: GetSingleton.new(type: DairyType, data: DAIRY)
field :fromSource, &SourceFieldDefn
field :favoriteEdible, FavoriteFieldDefn
field :cow, function: GetSingleton.new(type: CowType, data: COW)
field :cow, function: GetSingleton.new(type: CowType, data: COWS[1])
field :searchDairy do
description "Find dairy products matching a description"
type !DairyProductUnion
Expand All @@ -287,6 +310,14 @@ def call(obj, args, ctx)
}
end

field :allAnimal, !types[AnimalUnion] do
resolve ->(obj, args, ctx) { COWS.values + GOATS.values }
end

field :allAnimalAsCow, !types[AnimalAsCowUnion] do
resolve ->(obj, args, ctx) { COWS.values + GOATS.values }
end

field :allDairy, types[DairyProductUnion] do
argument :executionErrorAtIndex, types.Int
resolve ->(obj, args, ctx) {
Expand Down

0 comments on commit ddbf77d

Please sign in to comment.