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 May 24, 2022
1 parent c7d7026 commit 80ca884
Show file tree
Hide file tree
Showing 31 changed files with 827 additions and 42 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
16 changes: 10 additions & 6 deletions src/execution/__tests__/union-interface-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ const CatType: GraphQLObjectType = new GraphQLObjectType({

const PetType = new GraphQLUnionType({
name: 'Pet',
interfaces: [MammalType, LifeType, NamedType],
types: [DogType, CatType],
resolveType(value) {
if (value instanceof Dog) {
Expand Down Expand Up @@ -211,8 +212,13 @@ describe('Execute: Union and intersection types', () => {
Pet: {
kind: 'UNION',
name: 'Pet',
fields: null,
interfaces: null,
fields: [
{ name: 'progeny' },
{ name: 'mother' },
{ name: 'father' },
{ name: 'name' },
],
interfaces: [{ name: 'Mammal' }, { name: 'Life' }, { name: 'Named' }],
possibleTypes: [{ name: 'Dog' }, { name: 'Cat' }],
enumValues: null,
inputFields: null,
Expand Down Expand Up @@ -264,12 +270,11 @@ describe('Execute: Union and intersection types', () => {
name
pets {
__typename
name
... on Dog {
name
barks
}
... on Cat {
name
meows
}
}
Expand Down Expand Up @@ -436,12 +441,11 @@ describe('Execute: Union and intersection types', () => {
fragment PetFields on Pet {
__typename
name
... on Dog {
name
barks
}
... on Cat {
name
meows
}
}
Expand Down
19 changes: 19 additions & 0 deletions src/jsutils/mapValues.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { ObjMap, ReadOnlyObjMap } from './ObjMap';

/**
* Creates an object map from an array of `maps` with the same keys as each `map`
* in `maps` and values generated by running each value of `map` thru `fn`.
*/
export function mapValues<T, V>(
maps: ReadonlyArray<ReadOnlyObjMap<T>>,
fn: (value: T, key: string) => V,
): ObjMap<V> {
const result = Object.create(null);

for (const map of maps) {
for (const key of Object.keys(map)) {
result[key] = fn(map[key], key);
}
}
return result;
}
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
Loading

0 comments on commit 80ca884

Please sign in to comment.