1.0.0
This commit is contained in:
parent
fa624e48df
commit
a059010428
13 changed files with 391 additions and 26 deletions
|
@ -2,9 +2,9 @@
|
|||
[![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)
|
||||
|
||||
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.
|
||||
|
||||
[Angel framework](https://angel-dart.github.io)
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import 'dart:async';
|
||||
import 'package:graphql_parser/src/language/language.dart';
|
||||
import 'package:graphql_parser/graphql_parser.dart';
|
||||
|
||||
final String INPUT = '''
|
||||
{
|
||||
|
@ -13,4 +12,15 @@ final String INPUT = '''
|
|||
main() {
|
||||
var tokens = scan(INPUT);
|
||||
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
50
example/visitor.dart
Normal 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];
|
||||
}
|
||||
}
|
|
@ -7,15 +7,14 @@ class AliasContext extends Node {
|
|||
|
||||
AliasContext(this.NAME1, this.COLON, this.NAME2);
|
||||
|
||||
/// The actual name of the value.
|
||||
String get name => NAME1.text;
|
||||
|
||||
/// 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
|
||||
SourceSpan get span =>
|
||||
new SourceSpan(NAME1.span?.start, NAME2.span?.end, toSource());
|
||||
SourceSpan get span => NAME1.span.union(COLON.span).union(NAME2.span);
|
||||
|
||||
@override
|
||||
String toSource() => '${NAME1.text}:${NAME2.text}';
|
||||
|
|
|
@ -7,7 +7,4 @@ abstract class Node {
|
|||
SourceLocation get end => span.end;
|
||||
|
||||
String toSource();
|
||||
|
||||
@override
|
||||
String toString() => '${runtimeType}: ${toSource()}';
|
||||
}
|
||||
|
|
|
@ -11,10 +11,10 @@ class OperationDefinitionContext extends DefinitionContext {
|
|||
final List<DirectiveContext> directives = [];
|
||||
final SelectionSetContext selectionSet;
|
||||
|
||||
bool get isMutation => TYPE.text == 'mutation';
|
||||
bool get isQuery => TYPE.text == 'query';
|
||||
bool get isMutation => TYPE?.text == 'mutation';
|
||||
bool get isQuery => TYPE?.text == 'query';
|
||||
|
||||
String get name => NAME.text;
|
||||
String get name => NAME?.text;
|
||||
|
||||
OperationDefinitionContext(
|
||||
this.TYPE, this.NAME, this.variableDefinitions, this.selectionSet) {
|
||||
|
|
|
@ -49,9 +49,69 @@ class Parser {
|
|||
DefinitionContext parseDefinition() =>
|
||||
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() {
|
||||
if (next(TokenType.ELLIPSIS)) {
|
||||
|
@ -164,7 +224,27 @@ class Parser {
|
|||
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() {
|
||||
var variable = parseVariable();
|
||||
|
|
125
lib/visitor.dart
Normal file
125
lib/visitor.dart
Normal 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) {}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
name: graphql_parser
|
||||
version: 0.0.0
|
||||
version: 1.0.0
|
||||
description: Parses GraphQL queries and schemas.
|
||||
author: Tobe O <thosakwe@gmail.com>
|
||||
homepage: https://github.com/thosakwe/graphql_parser
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:test/test.dart';
|
||||
import 'argument_test.dart' as argument;
|
||||
import 'directive_test.dart' as directive;
|
||||
import 'document_test.dart' as document;
|
||||
import 'field_test.dart' as field;
|
||||
import 'fragment_spread_test.dart' as fragment_spread;
|
||||
import 'inline_fragment_test.dart' as inline_fragment;
|
||||
|
@ -13,6 +14,7 @@ import 'variable_test.dart' as variable;
|
|||
main() {
|
||||
group('argument', argument.main);
|
||||
group('directive', directive.main);
|
||||
group('document', document.main);
|
||||
group('field', field.main);
|
||||
group('fragment spread', fragment_spread.main);
|
||||
group('inline fragment', inline_fragment.main);
|
||||
|
|
100
test/document_test.dart
Normal file
100
test/document_test.dart
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -109,22 +109,22 @@ class _IsField 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
|
||||
Description describe(Description description) {
|
||||
if (alias != null)
|
||||
return description.add('is field with name "$name" and alias "$alias"');
|
||||
if (realName != null)
|
||||
return description.add('is field with name "$name" and alias "$realName"');
|
||||
return description.add('is field with name "$name"');
|
||||
}
|
||||
|
||||
@override
|
||||
bool matches(item, Map matchState) {
|
||||
var fieldName = item is FieldNameContext ? item : parseFieldName(item);
|
||||
if (alias != null)
|
||||
return fieldName.alias?.name == name && fieldName.alias?.alias == alias;
|
||||
if (realName != null)
|
||||
return fieldName.alias?.alias == name && fieldName.alias?.name == realName;
|
||||
else
|
||||
return fieldName.name == name;
|
||||
}
|
||||
|
|
|
@ -21,6 +21,8 @@ main() {
|
|||
expect(() => parseVariableDefinition(r'$foo'), throwsSyntaxError);
|
||||
expect(() => parseVariableDefinition(r'$foo:'), throwsSyntaxError);
|
||||
expect(() => parseVariableDefinition(r'$foo: int ='), throwsSyntaxError);
|
||||
expect(() => parse(r'($foo: int = 2').parseVariableDefinitions(),
|
||||
throwsSyntaxError);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue