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 `ResolvedSchemaElement`
  • Loading branch information
leebyron authored and yaacovCR committed Dec 29, 2022
1 parent 1bf71ee commit 08e459e
Show file tree
Hide file tree
Showing 16 changed files with 696 additions and 8 deletions.
6 changes: 6 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ export {
parseValue,
parseConstValue,
parseType,
parseSchemaCoordinate,
// Print
print,
// Visit
Expand All @@ -239,6 +240,7 @@ export {
isTypeDefinitionNode,
isTypeSystemExtensionNode,
isTypeExtensionNode,
isSchemaCoordinateNode,
} from './language/index.js';

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

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

export type {
Expand Down Expand Up @@ -488,4 +493,5 @@ export type {
BreakingChange,
DangerousChange,
TypedQueryDocumentNode,
ResolvedSchemaElement,
} from './utilities/index.js';
12 changes: 10 additions & 2 deletions src/language/__tests__/lexer-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -852,7 +852,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 @@ -964,6 +965,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 @@ -1030,7 +1038,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
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@ import { kitchenSinkQuery } from '../../__testUtils__/kitchenSinkQuery.js';
import { inspect } from '../../jsutils/inspect.js';

import { Kind } from '../kinds.js';
import { parse, parseConstValue, parseType, parseValue } from '../parser.js';
import {
parse,
parseConstValue,
parseSchemaCoordinate,
parseType,
parseValue,
} from '../parser.js';
import { Source } from '../source.js';
import { TokenKind } from '../tokenKind.js';

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

describe('parseSchemaCoordinate', () => {
it('parses Name', () => {
const result = parseSchemaCoordinate('MyType');
expectJSON(result).toDeepEqual({
kind: Kind.SCHEMA_COORDINATE,
loc: { start: 0, end: 6 },
ofDirective: 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');
expectJSON(result).toDeepEqual({
kind: Kind.SCHEMA_COORDINATE,
loc: { start: 0, end: 12 },
ofDirective: 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', () => {
expectToThrowJSON(() =>
parseSchemaCoordinate('MyType.field.deep'),
).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:)');
expectJSON(result).toDeepEqual({
kind: Kind.SCHEMA_COORDINATE,
loc: { start: 0, end: 18 },
ofDirective: 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 )', () => {
expectToThrowJSON(() =>
parseSchemaCoordinate('MyType.field(arg: value)'),
).to.deep.equal({
message: 'Syntax Error: Expected ")", found Name "value".',
locations: [{ line: 1, column: 19 }],
});
});

it('parses @ Name', () => {
const result = parseSchemaCoordinate('@myDirective');
expectJSON(result).toDeepEqual({
kind: Kind.SCHEMA_COORDINATE,
loc: { start: 0, end: 12 },
ofDirective: 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:)');
expectJSON(result).toDeepEqual({
kind: Kind.SCHEMA_COORDINATE,
loc: { start: 0, end: 18 },
ofDirective: 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', () => {
expectToThrowJSON(() =>
parseSchemaCoordinate('@myDirective.field'),
).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
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
isDefinitionNode,
isExecutableDefinitionNode,
isNullabilityAssertionNode,
isSchemaCoordinateNode,
isSelectionNode,
isTypeDefinitionNode,
isTypeExtensionNode,
Expand Down Expand Up @@ -150,4 +151,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
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { dedent, dedentString } from '../../__testUtils__/dedent.js';
import { kitchenSinkQuery } from '../../__testUtils__/kitchenSinkQuery.js';

import { Kind } from '../kinds.js';
import { parse } from '../parser.js';
import { parse, parseSchemaCoordinate } from '../parser.js';
import { print } from '../printer.js';

describe('Printer: Query document', () => {
Expand Down Expand Up @@ -232,4 +232,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:)',
);
});
});
16 changes: 15 additions & 1 deletion src/language/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,8 @@ export type ASTNode =
| InputObjectTypeExtensionNode
| NonNullAssertionNode
| ErrorBoundaryNode
| ListNullabilityOperatorNode;
| ListNullabilityOperatorNode
| SchemaCoordinateNode;

/**
* Utility type listing all nodes indexed by their kind.
Expand Down Expand Up @@ -295,6 +296,8 @@ export const QueryDocumentKeys: {
UnionTypeExtension: ['name', 'directives', 'types'],
EnumTypeExtension: ['name', 'directives', 'values'],
InputObjectTypeExtension: ['name', 'directives', 'fields'],

SchemaCoordinate: ['name', 'memberName', 'argumentName'],
};

const kindValues = new Set<string>(Object.keys(QueryDocumentKeys));
Expand Down Expand Up @@ -785,3 +788,14 @@ export interface InputObjectTypeExtensionNode {
readonly directives?: ReadonlyArray<ConstDirectiveNode> | undefined;
readonly fields?: ReadonlyArray<InputValueDefinitionNode> | undefined;
}

/** Schema Coordinates */

export interface SchemaCoordinateNode {
readonly kind: 'SchemaCoordinate';
readonly loc?: Location | undefined;
readonly ofDirective: boolean;
readonly name: NameNode;
readonly memberName?: NameNode | undefined;
readonly argumentName?: NameNode | undefined;
}
10 changes: 9 additions & 1 deletion src/language/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@ export { TokenKind } from './tokenKind.js';

export { Lexer } from './lexer.js';

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

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

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

export { DirectiveLocation } from './directiveLocation.js';
3 changes: 3 additions & 0 deletions src/language/kinds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,7 @@ export enum Kind {
UNION_TYPE_EXTENSION = 'UnionTypeExtension',
ENUM_TYPE_EXTENSION = 'EnumTypeExtension',
INPUT_OBJECT_TYPE_EXTENSION = 'InputObjectTypeExtension',

/** Schema Coordinates */
SCHEMA_COORDINATE = 'SchemaCoordinate',
}
Loading

0 comments on commit 08e459e

Please sign in to comment.