Skip to content

Commit

Permalink
Schema Coordinates
Browse files Browse the repository at this point in the history
Implements graphql/graphql-spec#794

Adds:

* DOT punctuator in lexer
* Improvements to lexer errors around misuse of `.`
* Minor improvement to parser core which simplified this addition
* `SchemaCoordinate` node and `isSchemaCoodinate()` predicate
* Support in `print()` and `visit()`
* Added function `parseSchemaCoordinate()` since it is a parser entry point.
* Added function `resolveSchemaCoordinate()` and `resolveASTSchemaCoordinate()` which implement the semantics (name mirrored from `buildASTSchema`) as well as the return type `GraphQLSchemaElement`
  • Loading branch information
leebyron committed May 28, 2021
1 parent 2f893d6 commit 3cec8c2
Show file tree
Hide file tree
Showing 17 changed files with 693 additions and 8 deletions.
6 changes: 6 additions & 0 deletions src/index.ts
Expand Up @@ -201,6 +201,7 @@ export {
parseValue,
parseConstValue,
parseType,
parseSchemaCoordinate,
/** Print */
print,
/** Visit */
Expand All @@ -221,6 +222,7 @@ export {
isTypeDefinitionNode,
isTypeSystemExtensionNode,
isTypeExtensionNode,
isSchemaCoordinateNode,
} from './language/index';

export type {
Expand Down Expand Up @@ -295,6 +297,7 @@ export type {
UnionTypeExtensionNode,
EnumTypeExtensionNode,
InputObjectTypeExtensionNode,
SchemaCoordinateNode,
} from './language/index';

/** Execute GraphQL queries. */
Expand Down Expand Up @@ -436,6 +439,8 @@ export {
DangerousChangeType,
findBreakingChanges,
findDangerousChanges,
resolveSchemaCoordinate,
resolveASTSchemaCoordinate,
} from './utilities/index';

export type {
Expand Down Expand Up @@ -465,4 +470,5 @@ export type {
BreakingChange,
DangerousChange,
TypedQueryDocumentNode,
GraphQLSchemaElement,
} from './utilities/index';
12 changes: 10 additions & 2 deletions src/language/__tests__/lexer-test.ts
Expand Up @@ -657,7 +657,8 @@ describe('Lexer', () => {
});

expectSyntaxError('.123').to.deep.equal({
message: 'Syntax Error: Unexpected character: ".".',
message:
'Syntax Error: Invalid number, expected digit before ".", did you mean "0.123"?',
locations: [{ line: 1, column: 1 }],
});

Expand Down Expand Up @@ -762,6 +763,13 @@ describe('Lexer', () => {
value: undefined,
});

expect(lexOne('.')).to.contain({
kind: TokenKind.DOT,
start: 0,
end: 1,
value: undefined,
});

expect(lexOne('...')).to.contain({
kind: TokenKind.SPREAD,
start: 0,
Expand Down Expand Up @@ -828,7 +836,7 @@ describe('Lexer', () => {

it('lex reports useful unknown character error', () => {
expectSyntaxError('..').to.deep.equal({
message: 'Syntax Error: Unexpected character: ".".',
message: 'Syntax Error: Unexpected "..", did you mean "..."?',
locations: [{ line: 1, column: 1 }],
});

Expand Down
133 changes: 132 additions & 1 deletion src/language/__tests__/parser-test.ts
Expand Up @@ -9,7 +9,13 @@ import { inspect } from '../../jsutils/inspect';
import { Kind } from '../kinds';
import { Source } from '../source';
import { TokenKind } from '../tokenKind';
import { parse, parseValue, parseConstValue, parseType } from '../parser';
import {
parse,
parseValue,
parseConstValue,
parseType,
parseSchemaCoordinate,
} from '../parser';

import { toJSONDeep } from './toJSONDeep';

Expand Down Expand Up @@ -619,4 +625,129 @@ describe('Parser', () => {
});
});
});

describe('parseSchemaCoordinate', () => {
it('parses Name', () => {
const result = parseSchemaCoordinate('MyType');
expect(toJSONDeep(result)).to.deep.equal({
kind: Kind.SCHEMA_COORDINATE,
loc: { start: 0, end: 6 },
isDirective: false,
name: {
kind: Kind.NAME,
loc: { start: 0, end: 6 },
value: 'MyType',
},
memberName: undefined,
argumentName: undefined,
});
});

it('parses Name . Name', () => {
const result = parseSchemaCoordinate('MyType.field');
expect(toJSONDeep(result)).to.deep.equal({
kind: Kind.SCHEMA_COORDINATE,
loc: { start: 0, end: 12 },
isDirective: false,
name: {
kind: Kind.NAME,
loc: { start: 0, end: 6 },
value: 'MyType',
},
memberName: {
kind: Kind.NAME,
loc: { start: 7, end: 12 },
value: 'field',
},
argumentName: undefined,
});
});

it('rejects Name . Name . Name', () => {
expect(() => parseSchemaCoordinate('MyType.field.deep'))
.to.throw()
.to.deep.equal({
message: 'Syntax Error: Expected <EOF>, found ".".',
locations: [{ line: 1, column: 13 }],
});
});

it('parses Name . Name ( Name : )', () => {
const result = parseSchemaCoordinate('MyType.field(arg:)');
expect(toJSONDeep(result)).to.deep.equal({
kind: Kind.SCHEMA_COORDINATE,
loc: { start: 0, end: 18 },
isDirective: false,
name: {
kind: Kind.NAME,
loc: { start: 0, end: 6 },
value: 'MyType',
},
memberName: {
kind: Kind.NAME,
loc: { start: 7, end: 12 },
value: 'field',
},
argumentName: {
kind: Kind.NAME,
loc: { start: 13, end: 16 },
value: 'arg',
},
});
});

it('rejects Name . Name ( Name : Name )', () => {
expect(() => parseSchemaCoordinate('MyType.field(arg: value)'))
.to.throw()
.to.deep.equal({
message: 'Syntax Error: Expected ")", found Name "value".',
locations: [{ line: 1, column: 19 }],
});
});

it('parses @ Name', () => {
const result = parseSchemaCoordinate('@myDirective');
expect(toJSONDeep(result)).to.deep.equal({
kind: Kind.SCHEMA_COORDINATE,
loc: { start: 0, end: 12 },
isDirective: true,
name: {
kind: Kind.NAME,
loc: { start: 1, end: 12 },
value: 'myDirective',
},
memberName: undefined,
argumentName: undefined,
});
});

it('parses @ Name ( Name : )', () => {
const result = parseSchemaCoordinate('@myDirective(arg:)');
expect(toJSONDeep(result)).to.deep.equal({
kind: Kind.SCHEMA_COORDINATE,
loc: { start: 0, end: 18 },
isDirective: true,
name: {
kind: Kind.NAME,
loc: { start: 1, end: 12 },
value: 'myDirective',
},
memberName: undefined,
argumentName: {
kind: Kind.NAME,
loc: { start: 13, end: 16 },
value: 'arg',
},
});
});

it('rejects @ Name . Name', () => {
expect(() => parseSchemaCoordinate('@myDirective.field'))
.to.throw()
.to.deep.equal({
message: 'Syntax Error: Expected <EOF>, found ".".',
locations: [{ line: 1, column: 13 }],
});
});
});
});
7 changes: 7 additions & 0 deletions src/language/__tests__/predicates-test.ts
Expand Up @@ -15,6 +15,7 @@ import {
isTypeDefinitionNode,
isTypeSystemExtensionNode,
isTypeExtensionNode,
isSchemaCoordinateNode,
} from '../predicates';

function filterNodes(predicate: (node: ASTNode) => boolean): Array<string> {
Expand Down Expand Up @@ -141,4 +142,10 @@ describe('AST node predicates', () => {
'InputObjectTypeExtension',
]);
});

it('isSchemaCoordinateNode', () => {
expect(filterNodes(isSchemaCoordinateNode)).to.deep.equal([
'SchemaCoordinate',
]);
});
});
16 changes: 15 additions & 1 deletion src/language/__tests__/printer-test.ts
Expand Up @@ -3,8 +3,8 @@ import { describe, it } from 'mocha';

import { dedent, dedentString } from '../../__testUtils__/dedent';
import { kitchenSinkQuery } from '../../__testUtils__/kitchenSinkQuery';
import { parseSchemaCoordinate, parse } from '../parser';

import { parse } from '../parser';
import { print } from '../printer';

describe('Printer: Query document', () => {
Expand Down Expand Up @@ -216,4 +216,18 @@ describe('Printer: Query document', () => {
`),
);
});

it('prints schema coordinates', () => {
expect(print(parseSchemaCoordinate(' Name '))).to.equal('Name');
expect(print(parseSchemaCoordinate(' Name . field '))).to.equal(
'Name.field',
);
expect(print(parseSchemaCoordinate(' Name . field ( arg: )'))).to.equal(
'Name.field(arg:)',
);
expect(print(parseSchemaCoordinate(' @ name '))).to.equal('@name');
expect(print(parseSchemaCoordinate(' @ name (arg:) '))).to.equal(
'@name(arg:)',
);
});
});
15 changes: 14 additions & 1 deletion src/language/ast.ts
Expand Up @@ -176,7 +176,8 @@ export type ASTNode =
| InterfaceTypeExtensionNode
| UnionTypeExtensionNode
| EnumTypeExtensionNode
| InputObjectTypeExtensionNode;
| InputObjectTypeExtensionNode
| SchemaCoordinateNode;

/**
* Utility type listing all nodes indexed by their kind.
Expand Down Expand Up @@ -225,6 +226,7 @@ export interface ASTKindToNode {
UnionTypeExtension: UnionTypeExtensionNode;
EnumTypeExtension: EnumTypeExtensionNode;
InputObjectTypeExtension: InputObjectTypeExtensionNode;
SchemaCoordinate: SchemaCoordinateNode;
}

/** Name */
Expand Down Expand Up @@ -670,3 +672,14 @@ export interface InputObjectTypeExtensionNode {
readonly directives?: ReadonlyArray<ConstDirectiveNode>;
readonly fields?: ReadonlyArray<InputValueDefinitionNode>;
}

// Schema Coordinates

export interface SchemaCoordinateNode {
readonly kind: 'SchemaCoordinate';
readonly loc?: Location;
readonly isDirective: boolean;
readonly name: NameNode;
readonly memberName?: NameNode;
readonly argumentName?: NameNode;
}
10 changes: 9 additions & 1 deletion src/language/index.ts
Expand Up @@ -13,7 +13,13 @@ export type { TokenKindEnum } from './tokenKind';

export { Lexer } from './lexer';

export { parse, parseValue, parseConstValue, parseType } from './parser';
export {
parse,
parseValue,
parseConstValue,
parseType,
parseSchemaCoordinate,
} from './parser';
export type { ParseOptions } from './parser';

export { print } from './printer';
Expand Down Expand Up @@ -85,6 +91,7 @@ export type {
UnionTypeExtensionNode,
EnumTypeExtensionNode,
InputObjectTypeExtensionNode,
SchemaCoordinateNode,
} from './ast';

export {
Expand All @@ -98,6 +105,7 @@ export {
isTypeDefinitionNode,
isTypeSystemExtensionNode,
isTypeExtensionNode,
isSchemaCoordinateNode,
} from './predicates';

export { DirectiveLocation } from './directiveLocation';
Expand Down
3 changes: 3 additions & 0 deletions src/language/kinds.ts
Expand Up @@ -66,6 +66,9 @@ export const Kind = Object.freeze({
UNION_TYPE_EXTENSION: 'UnionTypeExtension',
ENUM_TYPE_EXTENSION: 'EnumTypeExtension',
INPUT_OBJECT_TYPE_EXTENSION: 'InputObjectTypeExtension',

/** Schema Coordinates */
SCHEMA_COORDINATE: 'SchemaCoordinate',
} as const);

/**
Expand Down

0 comments on commit 3cec8c2

Please sign in to comment.