This commit is contained in:
thosakwe 2017-07-04 15:27:47 -04:00
parent fa624e48df
commit a059010428
13 changed files with 391 additions and 26 deletions

View file

@ -2,9 +2,9 @@
[![Pub](https://img.shields.io/pub/v/graphql_parser.svg)](https://pub.dartlang.org/packages/graphql_parser) [![Pub](https://img.shields.io/pub/v/graphql_parser.svg)](https://pub.dartlang.org/packages/graphql_parser)
[![build status](https://travis-ci.org/thosakwe/graphql_parser.svg)](https://travis-ci.org/thosakwe/graphql_parser) [![build status](https://travis-ci.org/thosakwe/graphql_parser.svg)](https://travis-ci.org/thosakwe/graphql_parser)
Parses GraphQL queries and schemas. Parses GraphQL queries and schemas. Also includes a `GraphQLVisitor` class.
*This library is merely a parser*. Any sort of actual GraphQL API functionality must be implemented by you, *This library is merely a parser/visitor*. Any sort of actual GraphQL API functionality must be implemented by you,
or by a third-party package. or by a third-party package.
[Angel framework](https://angel-dart.github.io) [Angel framework](https://angel-dart.github.io)

View file

@ -1,5 +1,4 @@
import 'dart:async'; import 'package:graphql_parser/graphql_parser.dart';
import 'package:graphql_parser/src/language/language.dart';
final String INPUT = ''' final String INPUT = '''
{ {
@ -13,4 +12,15 @@ final String INPUT = '''
main() { main() {
var tokens = scan(INPUT); var tokens = scan(INPUT);
var parser = new Parser(tokens); var parser = new Parser(tokens);
var doc = parser.parseDocument();
var operation = doc.definitions.first as OperationDefinitionContext;
var projectField = operation.selectionSet.selections.first.field;
print(projectField.fieldName.name); // project
print(projectField.arguments.first.name); // name
print(projectField.arguments.first.valueOrVariable.value.value); // GraphQL
var taglineField = projectField.selectionSet.selections.first.field;
print(taglineField.fieldName.name); // tagline
} }

50
example/visitor.dart Normal file
View file

@ -0,0 +1,50 @@
import 'package:graphql_parser/graphql_parser.dart';
import 'package:graphql_parser/visitor.dart';
const String QUERY = '''
{
foo,
baz: bar
}
''';
const Map<String, dynamic> DATA = const {
'foo': 'hello',
'bar': 'world',
'quux': 'extraneous'
};
main() {
// Highly-simplified querying example...
var result = new MapQuerier(DATA).execute(QUERY);
print(result); // { foo: hello, baz: world }
print(result['foo']); // hello
print(result['baz']); // world
}
class MapQuerier extends GraphQLVisitor {
final Map<String, dynamic> data;
final Map<String, dynamic> result = {};
MapQuerier(this.data);
Map<String, dynamic> execute(String query) {
var doc = new Parser(scan(query)).parseDocument();
visitDocument(doc);
return result;
}
@override
visitField(FieldContext ctx) {
String realName, alias;
if (ctx.fieldName.alias == null)
realName = alias = ctx.fieldName.name;
else {
realName = ctx.fieldName.alias.name;
alias = ctx.fieldName.alias.alias;
}
// Set output field...
result[alias] = data[realName];
}
}

View file

@ -7,15 +7,14 @@ class AliasContext extends Node {
AliasContext(this.NAME1, this.COLON, this.NAME2); AliasContext(this.NAME1, this.COLON, this.NAME2);
/// The actual name of the value.
String get name => NAME1.text;
/// The aliased name of the value. /// The aliased name of the value.
String get alias => NAME2.text; String get alias => NAME1.text;
/// The actual name of the value.
String get name => NAME2.text;
@override @override
SourceSpan get span => SourceSpan get span => NAME1.span.union(COLON.span).union(NAME2.span);
new SourceSpan(NAME1.span?.start, NAME2.span?.end, toSource());
@override @override
String toSource() => '${NAME1.text}:${NAME2.text}'; String toSource() => '${NAME1.text}:${NAME2.text}';

View file

@ -7,7 +7,4 @@ abstract class Node {
SourceLocation get end => span.end; SourceLocation get end => span.end;
String toSource(); String toSource();
@override
String toString() => '${runtimeType}: ${toSource()}';
} }

View file

@ -11,10 +11,10 @@ class OperationDefinitionContext extends DefinitionContext {
final List<DirectiveContext> directives = []; final List<DirectiveContext> directives = [];
final SelectionSetContext selectionSet; final SelectionSetContext selectionSet;
bool get isMutation => TYPE.text == 'mutation'; bool get isMutation => TYPE?.text == 'mutation';
bool get isQuery => TYPE.text == 'query'; bool get isQuery => TYPE?.text == 'query';
String get name => NAME.text; String get name => NAME?.text;
OperationDefinitionContext( OperationDefinitionContext(
this.TYPE, this.NAME, this.variableDefinitions, this.selectionSet) { this.TYPE, this.NAME, this.variableDefinitions, this.selectionSet) {

View file

@ -49,9 +49,69 @@ class Parser {
DefinitionContext parseDefinition() => DefinitionContext parseDefinition() =>
parseOperationDefinition() ?? parseFragmentDefinition(); parseOperationDefinition() ?? parseFragmentDefinition();
OperationDefinitionContext parseOperationDefinition() {} OperationDefinitionContext parseOperationDefinition() {
var selectionSet = parseSelectionSet();
if (selectionSet != null)
return new OperationDefinitionContext(null, null, null, selectionSet);
else {
if (next(TokenType.MUTATION) || next(TokenType.QUERY)) {
var TYPE = current;
if (next(TokenType.NAME)) {
var NAME = current;
var variables = parseVariableDefinitions();
var dirs = parseDirectives();
var selectionSet = parseSelectionSet();
if (selectionSet != null)
return new OperationDefinitionContext(
TYPE, NAME, variables, selectionSet)
..directives.addAll(dirs);
else
throw new SyntaxError.fromSourceLocation(
'Expected selection set in fragment definition.',
NAME.span.end);
} else
throw new SyntaxError.fromSourceLocation(
'Expected name after operation type "${TYPE.text}" in operation definition.',
TYPE.span.end);
} else
return null;
}
}
FragmentDefinitionContext parseFragmentDefinition() {} FragmentDefinitionContext parseFragmentDefinition() {
if (next(TokenType.FRAGMENT)) {
var FRAGMENT = current;
if (next(TokenType.NAME)) {
var NAME = current;
if (next(TokenType.ON)) {
var ON = current;
var typeCondition = parseTypeCondition();
if (typeCondition != null) {
var dirs = parseDirectives();
var selectionSet = parseSelectionSet();
if (selectionSet != null)
return new FragmentDefinitionContext(
FRAGMENT, NAME, ON, typeCondition, selectionSet)
..directives.addAll(dirs);
else
throw new SyntaxError.fromSourceLocation(
'Expected selection set in fragment definition.',
typeCondition.span.end);
} else
throw new SyntaxError.fromSourceLocation(
'Expected type condition after "on" in fragment definition.',
ON.span.end);
} else
throw new SyntaxError.fromSourceLocation(
'Expected "on" after name "${NAME.text}" in fragment definition.',
NAME.span.end);
} else
throw new SyntaxError.fromSourceLocation(
'Expected name after "fragment" in fragment definition.',
FRAGMENT.span.end);
} else
return null;
}
FragmentSpreadContext parseFragmentSpread() { FragmentSpreadContext parseFragmentSpread() {
if (next(TokenType.ELLIPSIS)) { if (next(TokenType.ELLIPSIS)) {
@ -164,7 +224,27 @@ class Parser {
return null; return null;
} }
VariableDefinitionsContext parseVariableDefinitions() {} VariableDefinitionsContext parseVariableDefinitions() {
if (next(TokenType.LPAREN)) {
var LPAREN = current;
List<VariableDefinitionContext> defs = [];
VariableDefinitionContext def = parseVariableDefinition();
while (def != null) {
defs.add(def);
maybe(TokenType.COMMA);
def = parseVariableDefinition();
}
if (next(TokenType.RPAREN))
return new VariableDefinitionsContext(LPAREN, current)
..variableDefinitions.addAll(defs);
else
throw new SyntaxError.fromSourceLocation(
'Expected ")" after variable definitions.', LPAREN.span.end);
} else
return null;
}
VariableDefinitionContext parseVariableDefinition() { VariableDefinitionContext parseVariableDefinition() {
var variable = parseVariable(); var variable = parseVariable();

125
lib/visitor.dart Normal file
View file

@ -0,0 +1,125 @@
import 'graphql_parser.dart';
class GraphQLVisitor {
visitDocument(DocumentContext ctx) {
ctx.definitions.forEach(visitDefinition);
}
visitDefinition(DefinitionContext ctx) {
if (ctx is OperationDefinitionContext)
visitOperationDefinition(ctx);
else if (ctx is FragmentDefinitionContext) visitFragmentDefinition(ctx);
}
visitOperationDefinition(OperationDefinitionContext ctx) {
if (ctx.variableDefinitions != null)
visitVariableDefinitions(ctx.variableDefinitions);
ctx.directives.forEach(visitDirective);
visitSelectionSet(ctx.selectionSet);
}
visitFragmentDefinition(FragmentDefinitionContext ctx) {
visitTypeCondition(ctx.typeCondition);
ctx.directives.forEach(visitDirective);
visitSelectionSet(ctx.selectionSet);
}
visitSelectionSet(SelectionSetContext ctx) {
ctx.selections.forEach(visitSelection);
}
visitSelection(SelectionContext ctx) {
if (ctx.field != null) visitField(ctx.field);
if (ctx.fragmentSpread != null) visitFragmentSpread(ctx.fragmentSpread);
if (ctx.inlineFragment != null) visitInlineFragment(ctx.inlineFragment);
}
visitInlineFragment(InlineFragmentContext ctx) {
visitTypeCondition(ctx.typeCondition);
ctx.directives.forEach(visitDirective);
visitSelectionSet(ctx.selectionSet);
}
visitFragmentSpread(FragmentSpreadContext ctx) {
ctx.directives.forEach(visitDirective);
}
visitField(FieldContext ctx) {
visitFieldName(ctx.fieldName);
ctx.arguments.forEach(visitArgument);
ctx.directives.forEach(visitDirective);
if (ctx.selectionSet != null) ;
visitSelectionSet(ctx.selectionSet);
}
visitFieldName(FieldNameContext ctx) {
if (ctx.alias != null) visitAlias(ctx.alias);
}
visitAlias(AliasContext ctx) {}
visitDirective(DirectiveContext ctx) {
if (ctx.valueOrVariable != null) visitValueOrVariable(ctx.valueOrVariable);
if (ctx.argument != null) visitArgument(ctx.argument);
}
visitArgument(ArgumentContext ctx) {
visitValueOrVariable(ctx.valueOrVariable);
}
visitVariableDefinitions(VariableDefinitionsContext ctx) {
ctx.variableDefinitions.forEach(visitVariableDefinition);
}
visitVariableDefinition(VariableDefinitionContext ctx) {
visitVariable(ctx.variable);
visitType(ctx.type);
if (ctx.defaultValue != null) visitDefaultValue(ctx.defaultValue);
}
visitVariable(VariableContext ctx) {}
visitValueOrVariable(ValueOrVariableContext ctx) {
if (ctx.variable != null) visitVariable(ctx.variable);
if (ctx.value != null) visitValue(ctx.value);
}
visitDefaultValue(DefaultValueContext ctx) {
visitValue(ctx.value);
}
visitValue(ValueContext ctx) {
if (ctx is StringValueContext)
visitStringValue(ctx);
else if (ctx is NumberValueContext)
visitNumberValue(ctx);
else if (ctx is BooleanValueContext)
visitBooleanValue(ctx);
else if (ctx is ArrayValueContext) visitArrayValue(ctx);
}
visitStringValue(StringValueContext ctx) {}
visitBooleanValue(BooleanValueContext ctx) {}
visitNumberValue(NumberValueContext ctx) {}
visitArrayValue(ArrayValueContext ctx) {
ctx.values.forEach(visitValue);
}
visitTypeCondition(TypeConditionContext ctx) {
visitTypeName(ctx.typeName);
}
visitType(TypeContext ctx) {
if (ctx.typeName != null) visitTypeName(ctx.typeName);
if (ctx.listType != null) visitListType(ctx.listType);
}
visitListType(ListTypeContext ctx) {
visitType(ctx.type);
}
visitTypeName(TypeNameContext ctx) {}
}

View file

@ -1,5 +1,5 @@
name: graphql_parser name: graphql_parser
version: 0.0.0 version: 1.0.0
description: Parses GraphQL queries and schemas. description: Parses GraphQL queries and schemas.
author: Tobe O <thosakwe@gmail.com> author: Tobe O <thosakwe@gmail.com>
homepage: https://github.com/thosakwe/graphql_parser homepage: https://github.com/thosakwe/graphql_parser

View file

@ -1,6 +1,7 @@
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'argument_test.dart' as argument; import 'argument_test.dart' as argument;
import 'directive_test.dart' as directive; import 'directive_test.dart' as directive;
import 'document_test.dart' as document;
import 'field_test.dart' as field; import 'field_test.dart' as field;
import 'fragment_spread_test.dart' as fragment_spread; import 'fragment_spread_test.dart' as fragment_spread;
import 'inline_fragment_test.dart' as inline_fragment; import 'inline_fragment_test.dart' as inline_fragment;
@ -13,6 +14,7 @@ import 'variable_test.dart' as variable;
main() { main() {
group('argument', argument.main); group('argument', argument.main);
group('directive', directive.main); group('directive', directive.main);
group('document', document.main);
group('field', field.main); group('field', field.main);
group('fragment spread', fragment_spread.main); group('fragment spread', fragment_spread.main);
group('inline fragment', inline_fragment.main); group('inline fragment', inline_fragment.main);

100
test/document_test.dart Normal file
View file

@ -0,0 +1,100 @@
import 'package:graphql_parser/graphql_parser.dart';
import 'package:test/test.dart';
import 'common.dart';
import 'directive_test.dart';
import 'field_test.dart';
import 'selection_set_test.dart';
import 'type_test.dart';
import 'value_test.dart';
import 'variable_definition_test.dart';
main() {
test('fragment', () {
var fragment = parse('''
fragment PostInfo on Post {
description
comments {
id
}
}
''').parseFragmentDefinition();
expect(fragment, isNotNull);
expect(fragment.name, 'PostInfo');
expect(fragment.typeCondition.typeName.name, 'Post');
expect(
fragment.selectionSet,
isSelectionSet([
isField(fieldName: isFieldName('description')),
isField(
fieldName: isFieldName('comments'),
selectionSet:
isSelectionSet([isField(fieldName: isFieldName('id'))])),
]));
});
test('fragment exceptions', () {
expect(
() => parse('fragment').parseFragmentDefinition(), throwsSyntaxError);
expect(() => parse('fragment foo').parseFragmentDefinition(),
throwsSyntaxError);
expect(() => parse('fragment foo on').parseFragmentDefinition(),
throwsSyntaxError);
expect(() => parse('fragment foo on bar').parseFragmentDefinition(),
throwsSyntaxError);
});
group('operation', () {
test('with selection set', () {
var op = parse('{foo, bar: baz}').parseOperationDefinition();
expect(op.variableDefinitions, isNull);
expect(op.isQuery, isFalse);
expect(op.isMutation, isFalse);
expect(op.name, isNull);
expect(
op.selectionSet,
isSelectionSet([
isField(fieldName: isFieldName('foo')),
isField(fieldName: isFieldName('bar', alias: 'baz'))
]));
});
test('with operation type', () {
var doc =
parse(r'query foo ($one: [int] = 2) @foo @bar: 2 {foo, bar: baz}')
.parseDocument();
expect(doc.definitions, hasLength(1));
expect(doc.definitions.first,
const isInstanceOf<OperationDefinitionContext>());
var op = doc.definitions.first as OperationDefinitionContext;
expect(op.isMutation, isFalse);
expect(op.isQuery, isTrue);
expect(op.variableDefinitions.variableDefinitions, hasLength(1));
expect(
op.variableDefinitions.variableDefinitions.first,
isVariableDefinition('one',
type: isListType(isType('int'), isNullable: true),
defaultValue: isValue(2)));
expect(op.directives, hasLength(2));
expect(op.directives[0], isDirective('foo'));
expect(op.directives[1], isDirective('bar', valueOrVariable: equals(2)));
expect(op.selectionSet, isNotNull);
expect(
op.selectionSet,
isSelectionSet([
isField(fieldName: isFieldName('foo')),
isField(fieldName: isFieldName('bar', alias: 'baz'))
]));
});
test('exceptions', () {
expect(
() => parse('query').parseOperationDefinition(), throwsSyntaxError);
expect(() => parse('query foo()').parseOperationDefinition(),
throwsSyntaxError);
});
});
}

View file

@ -109,22 +109,22 @@ class _IsField extends Matcher {
} }
class _IsFieldName extends Matcher { class _IsFieldName extends Matcher {
final String name, alias; final String name, realName;
_IsFieldName(this.name, this.alias); _IsFieldName(this.name, this.realName);
@override @override
Description describe(Description description) { Description describe(Description description) {
if (alias != null) if (realName != null)
return description.add('is field with name "$name" and alias "$alias"'); return description.add('is field with name "$name" and alias "$realName"');
return description.add('is field with name "$name"'); return description.add('is field with name "$name"');
} }
@override @override
bool matches(item, Map matchState) { bool matches(item, Map matchState) {
var fieldName = item is FieldNameContext ? item : parseFieldName(item); var fieldName = item is FieldNameContext ? item : parseFieldName(item);
if (alias != null) if (realName != null)
return fieldName.alias?.name == name && fieldName.alias?.alias == alias; return fieldName.alias?.alias == name && fieldName.alias?.name == realName;
else else
return fieldName.name == name; return fieldName.name == name;
} }

View file

@ -21,6 +21,8 @@ main() {
expect(() => parseVariableDefinition(r'$foo'), throwsSyntaxError); expect(() => parseVariableDefinition(r'$foo'), throwsSyntaxError);
expect(() => parseVariableDefinition(r'$foo:'), throwsSyntaxError); expect(() => parseVariableDefinition(r'$foo:'), throwsSyntaxError);
expect(() => parseVariableDefinition(r'$foo: int ='), throwsSyntaxError); expect(() => parseVariableDefinition(r'$foo: int ='), throwsSyntaxError);
expect(() => parse(r'($foo: int = 2').parseVariableDefinitions(),
throwsSyntaxError);
}); });
} }