Skip to content

Commit

Permalink
RFC: Allow interfaces to implement other interfaces
Browse files Browse the repository at this point in the history
This is an initial implementation of the RFC described by spec PR #373
graphql/graphql-spec#373
  • Loading branch information
mike-marcacci committed Aug 13, 2019
1 parent cd9fc8e commit 2570536
Show file tree
Hide file tree
Showing 34 changed files with 1,299 additions and 119 deletions.
2 changes: 1 addition & 1 deletion src/__fixtures__/github-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -56052,7 +56052,7 @@
},
{
"name": "INTERFACE",
"description": "Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.",
"description": "Indicates this type is an interface. `fields`, `interfaces`, and `possibleTypes` are valid fields.",
"isDeprecated": false,
"deprecationReason": null
},
Expand Down
6 changes: 6 additions & 0 deletions src/__fixtures__/schema-kitchen-sink.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ extend interface Bar {

extend interface Bar @onInterface

interface Baz implements Bar {
one: Type
two(argument: InputType!): Type
four(argument: String = "string"): String
}

union Feed =
| Story
| Article
Expand Down
64 changes: 54 additions & 10 deletions src/execution/__tests__/union-interface-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,23 +55,46 @@ const NamedType = new GraphQLInterfaceType({
},
});

const LifeType = new GraphQLInterfaceType({
name: 'Life',
fields: () => ({
progeny: { type: GraphQLList(LifeType) },
}),
});

const MammalType = new GraphQLInterfaceType({
name: 'Mammal',
interfaces: [LifeType],
fields: () => ({
progeny: { type: GraphQLList(MammalType) },
mother: { type: MammalType },
father: { type: MammalType },
}),
});

const DogType = new GraphQLObjectType({
name: 'Dog',
interfaces: [NamedType],
fields: {
interfaces: [MammalType, LifeType, NamedType],
fields: () => ({
name: { type: GraphQLString },
barks: { type: GraphQLBoolean },
},
progeny: { type: GraphQLList(DogType) },
mother: { type: DogType },
father: { type: DogType },
}),
isTypeOf: value => value instanceof Dog,
});

const CatType = new GraphQLObjectType({
name: 'Cat',
interfaces: [NamedType],
fields: {
interfaces: [MammalType, LifeType, NamedType],
fields: () => ({
name: { type: GraphQLString },
meows: { type: GraphQLBoolean },
},
progeny: { type: GraphQLList(CatType) },
mother: { type: CatType },
father: { type: CatType },
}),
isTypeOf: value => value instanceof Cat,
});

Expand All @@ -90,12 +113,15 @@ const PetType = new GraphQLUnionType({

const PersonType = new GraphQLObjectType({
name: 'Person',
interfaces: [NamedType],
fields: {
interfaces: [NamedType, MammalType, LifeType],
fields: () => ({
name: { type: GraphQLString },
pets: { type: GraphQLList(PetType) },
friends: { type: GraphQLList(NamedType) },
},
progeny: { type: GraphQLList(PersonType) },
mother: { type: PersonType },
father: { type: PersonType },
}),
isTypeOf: value => value instanceof Person,
});

Expand All @@ -122,6 +148,15 @@ describe('Execute: Union and intersection types', () => {
enumValues { name }
inputFields { name }
}
Mammal: __type(name: "Mammal") {
kind
name
fields { name }
interfaces { name }
possibleTypes { name }
enumValues { name }
inputFields { name }
}
Pet: __type(name: "Pet") {
kind
name
Expand All @@ -140,7 +175,16 @@ describe('Execute: Union and intersection types', () => {
kind: 'INTERFACE',
name: 'Named',
fields: [{ name: 'name' }],
interfaces: null,
interfaces: [],
possibleTypes: [{ name: 'Person' }, { name: 'Dog' }, { name: 'Cat' }],
enumValues: null,
inputFields: null,
},
Mammal: {
kind: 'INTERFACE',
name: 'Mammal',
fields: [{ name: 'progeny' }, { name: 'mother' }, { name: 'father' }],
interfaces: [{ name: 'Life' }],
possibleTypes: [{ name: 'Person' }, { name: 'Dog' }, { name: 'Cat' }],
enumValues: null,
inputFields: null,
Expand Down
168 changes: 164 additions & 4 deletions src/language/__tests__/schema-parser-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ describe('Schema Parser', () => {
});
});

it('Extension without fields', () => {
it('Object extension without fields', () => {
const doc = parse('extend type Hello implements Greeting');

expect(toJSONDeep(doc)).to.deep.equal({
Expand All @@ -190,7 +190,25 @@ describe('Schema Parser', () => {
});
});

it('Extension without fields followed by extension', () => {
it('Interface extension without fields', () => {
const doc = parse('extend interface Hello implements Greeting');
expect(toJSONDeep(doc)).to.deep.equal({
kind: 'Document',
definitions: [
{
kind: 'InterfaceTypeExtension',
name: nameNode('Hello', { start: 17, end: 22 }),
interfaces: [typeNode('Greeting', { start: 34, end: 42 })],
directives: [],
fields: [],
loc: { start: 0, end: 42 },
},
],
loc: { start: 0, end: 42 },
});
});

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

it('Extension without anything throws', () => {
it('Interface extension without fields followed by extension', () => {
const doc = parse(`
extend interface Hello implements Greeting
extend interface Hello implements SecondGreeting
`);
expect(toJSONDeep(doc)).to.deep.equal({
kind: 'Document',
definitions: [
{
kind: 'InterfaceTypeExtension',
name: nameNode('Hello', { start: 24, end: 29 }),
interfaces: [typeNode('Greeting', { start: 41, end: 49 })],
directives: [],
fields: [],
loc: { start: 7, end: 49 },
},
{
kind: 'InterfaceTypeExtension',
name: nameNode('Hello', { start: 74, end: 79 }),
interfaces: [typeNode('SecondGreeting', { start: 91, end: 105 })],
directives: [],
fields: [],
loc: { start: 57, end: 105 },
},
],
loc: { start: 0, end: 110 },
});
});

it('Object extension without anything throws', () => {
expectSyntaxError('extend type Hello', 'Unexpected <EOF>', {
line: 1,
column: 18,
});
});

it('Extension do not include descriptions', () => {
it('Interface extension without anything throws', () => {
expectSyntaxError('extend interface Hello', 'Unexpected <EOF>', {
line: 1,
column: 23,
});
});

it('Object extension do not include descriptions', () => {
expectSyntaxError(
`
"Description"
Expand All @@ -249,6 +304,27 @@ describe('Schema Parser', () => {
);
});

it('Interface extension do not include descriptions', () => {
expectSyntaxError(
`
"Description"
extend interface Hello {
world: String
}`,
'Unexpected Name "extend"',
{ line: 3, column: 7 },
);

expectSyntaxError(
`
extend "Description" interface Hello {
world: String
}`,
'Unexpected String "Description"',
{ line: 2, column: 14 },
);
});

it('Schema extension', () => {
const body = `
extend schema {
Expand Down Expand Up @@ -341,6 +417,31 @@ describe('Schema Parser', () => {
});
});

it('Simple interface inheriting interface', () => {
const doc = parse('interface Hello implements World { field: String }');
expect(toJSONDeep(doc)).to.deep.equal({
kind: 'Document',
definitions: [
{
kind: 'InterfaceTypeDefinition',
name: nameNode('Hello', { start: 10, end: 15 }),
description: undefined,
interfaces: [typeNode('World', { start: 27, end: 32 })],
directives: [],
fields: [
fieldNode(
nameNode('field', { start: 35, end: 40 }),
typeNode('String', { start: 42, end: 48 }),
{ start: 35, end: 48 },
),
],
loc: { start: 0, end: 50 },
},
],
loc: { start: 0, end: 50 },
});
});

it('Simple type inheriting interface', () => {
const doc = parse('type Hello implements World { field: String }');

Expand Down Expand Up @@ -396,6 +497,34 @@ describe('Schema Parser', () => {
});
});

it('Simple interface inheriting multiple interfaces', () => {
const doc = parse('interface Hello implements Wo & rld { field: String }');
expect(toJSONDeep(doc)).to.deep.equal({
kind: 'Document',
definitions: [
{
kind: 'InterfaceTypeDefinition',
name: nameNode('Hello', { start: 10, end: 15 }),
description: undefined,
interfaces: [
typeNode('Wo', { start: 27, end: 29 }),
typeNode('rld', { start: 32, end: 35 }),
],
directives: [],
fields: [
fieldNode(
nameNode('field', { start: 38, end: 43 }),
typeNode('String', { start: 45, end: 51 }),
{ start: 38, end: 51 },
),
],
loc: { start: 0, end: 53 },
},
],
loc: { start: 0, end: 53 },
});
});

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

Expand Down Expand Up @@ -425,6 +554,36 @@ describe('Schema Parser', () => {
});
});

it('Simple interface inheriting multiple interfaces with leading ampersand', () => {
const doc = parse(
'interface Hello implements & Wo & rld { field: String }',
);
expect(toJSONDeep(doc)).to.deep.equal({
kind: 'Document',
definitions: [
{
kind: 'InterfaceTypeDefinition',
name: nameNode('Hello', { start: 10, end: 15 }),
description: undefined,
interfaces: [
typeNode('Wo', { start: 29, end: 31 }),
typeNode('rld', { start: 34, end: 37 }),
],
directives: [],
fields: [
fieldNode(
nameNode('field', { start: 40, end: 45 }),
typeNode('String', { start: 47, end: 53 }),
{ start: 40, end: 53 },
),
],
loc: { start: 0, end: 55 },
},
],
loc: { start: 0, end: 55 },
});
});

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

Expand Down Expand Up @@ -480,6 +639,7 @@ describe('Schema Parser', () => {
kind: 'InterfaceTypeDefinition',
name: nameNode('Hello', { start: 10, end: 15 }),
description: undefined,
interfaces: [],
directives: [],
fields: [
fieldNode(
Expand Down
6 changes: 6 additions & 0 deletions src/language/__tests__/schema-printer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ describe('Printer: SDL document', () => {
extend interface Bar @onInterface
interface Baz implements Bar {
one: Type
two(argument: InputType!): Type
four(argument: String = "string"): String
}
union Feed = Story | Article | Advert
union AnnotatedUnion @onUnion = A | B
Expand Down
2 changes: 2 additions & 0 deletions src/language/ast.js
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,7 @@ export type InterfaceTypeDefinitionNode = {
+loc?: Location,
+description?: StringValueNode,
+name: NameNode,
+interfaces?: $ReadOnlyArray<NamedTypeNode>,
+directives?: $ReadOnlyArray<DirectiveNode>,
+fields?: $ReadOnlyArray<FieldDefinitionNode>,
...
Expand Down Expand Up @@ -589,6 +590,7 @@ export type InterfaceTypeExtensionNode = {
+kind: 'InterfaceTypeExtension',
+loc?: Location,
+name: NameNode,
+interfaces?: $ReadOnlyArray<NamedTypeNode>,
+directives?: $ReadOnlyArray<DirectiveNode>,
+fields?: $ReadOnlyArray<FieldDefinitionNode>,
...
Expand Down
Loading

0 comments on commit 2570536

Please sign in to comment.