Skip to content

Commit

Permalink
allow unions to declare implementation of interfaces
Browse files Browse the repository at this point in the history
WIP: more tests required

complete code coverage is already there, but goal is to have a test
where union implements an interface wherever there is a test for an
interface implementing interface
  • Loading branch information
yaacovCR committed Apr 5, 2022
1 parent 5f247e0 commit e47f0ad
Show file tree
Hide file tree
Showing 18 changed files with 428 additions and 16 deletions.
22 changes: 22 additions & 0 deletions src/__testUtils__/kitchenSinkSDL.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,28 @@ extend union Feed = Photo | Video
extend union Feed @onUnion
interface Node {
id: ID
}
interface Resource {
url: String
}
extend type Photo implements Node {
id: ID
url: String
}
extend type Video implements Node {
id: ID
url: String
}
union Media implements Node = Photo | Video
extend union Media implements Resource
scalar CustomScalar
scalar AnnotatedScalar @onScalar
Expand Down
2 changes: 1 addition & 1 deletion src/execution/__tests__/union-interface-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ describe('Execute: Union and intersection types', () => {
kind: 'UNION',
name: 'Pet',
fields: null,
interfaces: null,
interfaces: [],
possibleTypes: [{ name: 'Dog' }, { name: 'Cat' }],
enumValues: null,
inputFields: null,
Expand Down
117 changes: 117 additions & 0 deletions src/language/__tests__/schema-parser-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,24 @@ describe('Schema Parser', () => {
});
});

it('Union extension without types', () => {
const doc = parse('extend union HelloOrGoodbye implements Greeting');
expectJSON(doc).toDeepEqual({
kind: 'Document',
definitions: [
{
kind: 'UnionTypeExtension',
name: nameNode('HelloOrGoodbye', { start: 13, end: 27 }),
interfaces: [typeNode('Greeting', { start: 39, end: 47 })],
directives: [],
types: [],
loc: { start: 0, end: 47 },
},
],
loc: { start: 0, end: 47 },
});
});

it('Object extension without fields followed by extension', () => {
const doc = parse(`
extend type Hello implements Greeting
Expand Down Expand Up @@ -323,6 +341,36 @@ describe('Schema Parser', () => {
});
});

it('Union extension without types followed by extension', () => {
const doc = parse(`
extend union HelloOrGoodbye implements Greeting
extend union HelloOrGoodbye implements SecondGreeting
`);
expectJSON(doc).toDeepEqual({
kind: 'Document',
definitions: [
{
kind: 'UnionTypeExtension',
name: nameNode('HelloOrGoodbye', { start: 20, end: 34 }),
interfaces: [typeNode('Greeting', { start: 46, end: 54 })],
directives: [],
types: [],
loc: { start: 7, end: 54 },
},
{
kind: 'UnionTypeExtension',
name: nameNode('HelloOrGoodbye', { start: 75, end: 89 }),
interfaces: [typeNode('SecondGreeting', { start: 101, end: 115 })],
directives: [],
types: [],
loc: { start: 62, end: 115 },
},
],
loc: { start: 0, end: 120 },
});
});

it('Object extension do not include descriptions', () => {
expectSyntaxError(`
"Description"
Expand Down Expand Up @@ -517,6 +565,26 @@ describe('Schema Parser', () => {
});
});

it('Simple union inheriting interface', () => {
const doc = parse('union Hello implements World = Subtype');

expectJSON(doc).toDeepEqual({
kind: 'Document',
definitions: [
{
kind: 'UnionTypeDefinition',
name: nameNode('Hello', { start: 6, end: 11 }),
description: undefined,
interfaces: [typeNode('World', { start: 23, end: 28 })],
directives: [],
types: [typeNode('Subtype', { start: 31, end: 38 })],
loc: { start: 0, end: 38 },
},
],
loc: { start: 0, end: 38 },
});
});

it('Simple type inheriting multiple interfaces', () => {
const doc = parse('type Hello implements Wo & rld { field: String }');

Expand Down Expand Up @@ -574,6 +642,29 @@ describe('Schema Parser', () => {
});
});

it('Simple union inheriting multiple interfaces', () => {
const doc = parse('union Hello implements Wo & rld = Subtype');

expectJSON(doc).toDeepEqual({
kind: 'Document',
definitions: [
{
kind: 'UnionTypeDefinition',
name: nameNode('Hello', { start: 6, end: 11 }),
description: undefined,
interfaces: [
typeNode('Wo', { start: 23, end: 25 }),
typeNode('rld', { start: 28, end: 31 }),
],
directives: [],
types: [typeNode('Subtype', { start: 34, end: 41 })],
loc: { start: 0, end: 41 },
},
],
loc: { start: 0, end: 41 },
});
});

it('Simple type inheriting multiple interfaces with leading ampersand', () => {
const doc = parse('type Hello implements & Wo & rld { field: String }');

Expand Down Expand Up @@ -633,6 +724,29 @@ describe('Schema Parser', () => {
});
});

it('Simple union inheriting multiple interfaces with leading ampersand', () => {
const doc = parse('union Hello implements & Wo & rld = Subtype');

expectJSON(doc).toDeepEqual({
kind: 'Document',
definitions: [
{
kind: 'UnionTypeDefinition',
name: nameNode('Hello', { start: 6, end: 11 }),
description: undefined,
interfaces: [
typeNode('Wo', { start: 25, end: 27 }),
typeNode('rld', { start: 30, end: 33 }),
],
directives: [],
types: [typeNode('Subtype', { start: 36, end: 43 })],
loc: { start: 0, end: 43 },
},
],
loc: { start: 0, end: 43 },
});
});

it('Single value enum', () => {
const doc = parse('enum Hello { WORLD }');

Expand Down Expand Up @@ -880,6 +994,7 @@ describe('Schema Parser', () => {
kind: 'UnionTypeDefinition',
name: nameNode('Hello', { start: 6, end: 11 }),
description: undefined,
interfaces: [],
directives: [],
types: [typeNode('World', { start: 14, end: 19 })],
loc: { start: 0, end: 19 },
Expand All @@ -899,6 +1014,7 @@ describe('Schema Parser', () => {
kind: 'UnionTypeDefinition',
name: nameNode('Hello', { start: 6, end: 11 }),
description: undefined,
interfaces: [],
directives: [],
types: [
typeNode('Wo', { start: 14, end: 16 }),
Expand All @@ -921,6 +1037,7 @@ describe('Schema Parser', () => {
kind: 'UnionTypeDefinition',
name: nameNode('Hello', { start: 6, end: 11 }),
description: undefined,
interfaces: [],
directives: [],
types: [
typeNode('Wo', { start: 16, end: 18 }),
Expand Down
22 changes: 22 additions & 0 deletions src/language/__tests__/schema-printer-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,28 @@ describe('Printer: SDL document', () => {
extend union Feed @onUnion
interface Node {
id: ID
}
interface Resource {
url: String
}
extend type Photo implements Node {
id: ID
url: String
}
extend type Video implements Node {
id: ID
url: String
}
union Media implements Node = Photo | Video
extend union Media implements Resource
scalar CustomScalar
scalar AnnotatedScalar @onScalar
Expand Down
12 changes: 10 additions & 2 deletions src/language/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,13 @@ export const QueryDocumentKeys: {
'directives',
'fields',
],
UnionTypeDefinition: ['description', 'name', 'directives', 'types'],
UnionTypeDefinition: [
'description',
'name',
'interfaces',
'directives',
'types',
],
EnumTypeDefinition: ['description', 'name', 'directives', 'values'],
EnumValueDefinition: ['description', 'name', 'directives'],
InputObjectTypeDefinition: ['description', 'name', 'directives', 'fields'],
Expand All @@ -274,7 +280,7 @@ export const QueryDocumentKeys: {
ScalarTypeExtension: ['name', 'directives'],
ObjectTypeExtension: ['name', 'interfaces', 'directives', 'fields'],
InterfaceTypeExtension: ['name', 'interfaces', 'directives', 'fields'],
UnionTypeExtension: ['name', 'directives', 'types'],
UnionTypeExtension: ['name', 'interfaces', 'directives', 'types'],
EnumTypeExtension: ['name', 'directives', 'values'],
InputObjectTypeExtension: ['name', 'directives', 'fields'],
};
Expand Down Expand Up @@ -624,6 +630,7 @@ export interface UnionTypeDefinitionNode {
readonly loc?: Location;
readonly description?: StringValueNode;
readonly name: NameNode;
readonly interfaces?: ReadonlyArray<NamedTypeNode>;
readonly directives?: ReadonlyArray<ConstDirectiveNode>;
readonly types?: ReadonlyArray<NamedTypeNode>;
}
Expand Down Expand Up @@ -716,6 +723,7 @@ export interface UnionTypeExtensionNode {
readonly kind: Kind.UNION_TYPE_EXTENSION;
readonly loc?: Location;
readonly name: NameNode;
readonly interfaces?: ReadonlyArray<NamedTypeNode>;
readonly directives?: ReadonlyArray<ConstDirectiveNode>;
readonly types?: ReadonlyArray<NamedTypeNode>;
}
Expand Down
10 changes: 9 additions & 1 deletion src/language/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -970,12 +970,14 @@ export class Parser {
const description = this.parseDescription();
this.expectKeyword('union');
const name = this.parseName();
const interfaces = this.parseImplementsInterfaces();
const directives = this.parseConstDirectives();
const types = this.parseUnionMemberTypes();
return this.node<UnionTypeDefinitionNode>(start, {
kind: Kind.UNION_TYPE_DEFINITION,
description,
name,
interfaces,
directives,
types,
});
Expand Down Expand Up @@ -1249,14 +1251,20 @@ export class Parser {
this.expectKeyword('extend');
this.expectKeyword('union');
const name = this.parseName();
const interfaces = this.parseImplementsInterfaces();
const directives = this.parseConstDirectives();
const types = this.parseUnionMemberTypes();
if (directives.length === 0 && types.length === 0) {
if (
interfaces.length === 0 &&
directives.length === 0 &&
types.length === 0
) {
throw this.unexpected();
}
return this.node<UnionTypeExtensionNode>(start, {
kind: Kind.UNION_TYPE_EXTENSION,
name,
interfaces,
directives,
types,
});
Expand Down
13 changes: 10 additions & 3 deletions src/language/printer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,10 +202,16 @@ const printDocASTReducer: ASTReducer<string> = {
},

UnionTypeDefinition: {
leave: ({ description, name, directives, types }) =>
leave: ({ description, name, interfaces, directives, types }) =>
wrap('', description, '\n') +
join(
['union', name, join(directives, ' '), wrap('= ', join(types, ' | '))],
[
'union',
name,
wrap('implements ', join(interfaces, ' & ')),
join(directives, ' '),
wrap('= ', join(types, ' | ')),
],
' ',
),
},
Expand Down Expand Up @@ -282,11 +288,12 @@ const printDocASTReducer: ASTReducer<string> = {
},

UnionTypeExtension: {
leave: ({ name, directives, types }) =>
leave: ({ name, interfaces, directives, types }) =>
join(
[
'extend union',
name,
wrap('implements ', join(interfaces, ' & ')),
join(directives, ' '),
wrap('= ', join(types, ' | ')),
],
Expand Down
25 changes: 25 additions & 0 deletions src/type/__tests__/schema-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
GraphQLList,
GraphQLObjectType,
GraphQLScalarType,
GraphQLUnionType,
} from '../definition';
import { GraphQLDirective } from '../directives';
import { GraphQLBoolean, GraphQLInt, GraphQLString } from '../scalars';
Expand Down Expand Up @@ -212,6 +213,30 @@ describe('Type System: Schema', () => {
expect(schema.getType('SomeSubtype')).to.equal(SomeSubtype);
});

it("includes unions's thunk subtypes in the type map", () => {
const SomeUnion = new GraphQLUnionType({
name: 'SomeUnion',
types: () => [SomeSubtype],
interfaces: () => [SomeInterface],
});

const SomeInterface = new GraphQLInterfaceType({
name: 'SomeInterface',
fields: {},
});

const SomeSubtype = new GraphQLObjectType({
name: 'SomeSubtype',
fields: {},
});

const schema = new GraphQLSchema({ types: [SomeUnion] });

expect(schema.getType('SomeUnion')).to.equal(SomeUnion);
expect(schema.getType('SomeInterface')).to.equal(SomeInterface);
expect(schema.getType('SomeSubtype')).to.equal(SomeSubtype);
});

it('includes nested input objects in the map', () => {
const NestedInputObject = new GraphQLInputObjectType({
name: 'NestedInputObject',
Expand Down
Loading

0 comments on commit e47f0ad

Please sign in to comment.