Whoo
This commit is contained in:
parent
15643e136b
commit
b99eaf1965
40 changed files with 928 additions and 2 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -5,6 +5,7 @@
|
||||||
.packages
|
.packages
|
||||||
.project
|
.project
|
||||||
.pub/
|
.pub/
|
||||||
|
.scripts-bin/
|
||||||
build/
|
build/
|
||||||
**/packages/
|
**/packages/
|
||||||
|
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
# graphql
|
# graphql_parser
|
||||||
Generates services and models from GraphQL.
|
Parses GraphQL queries and schemas.
|
||||||
|
|
17
example/basic.dart
Normal file
17
example/basic.dart
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'package:graphql_parser/src/language/language.dart';
|
||||||
|
|
||||||
|
Stream<String> input() async* {
|
||||||
|
yield '''
|
||||||
|
{
|
||||||
|
project(name: "GraphQL") {
|
||||||
|
tagline
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'''.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
main() async {
|
||||||
|
var lexer = new Lexer(), parser = new Parser();
|
||||||
|
await input().transform(lexer).forEach(print);
|
||||||
|
}
|
16
lib/src/language/ast/alias.dart
Normal file
16
lib/src/language/ast/alias.dart
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import '../token.dart';
|
||||||
|
import 'node.dart';
|
||||||
|
import 'package:source_span/src/span.dart';
|
||||||
|
|
||||||
|
class AliasContext extends Node {
|
||||||
|
final Token NAME1, COLON, NAME2;
|
||||||
|
|
||||||
|
AliasContext(this.NAME1, this.COLON, this.NAME2);
|
||||||
|
|
||||||
|
@override
|
||||||
|
SourceSpan get span =>
|
||||||
|
new SourceSpan(NAME1.span?.start, NAME2.span?.end, toSource());
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toSource() => '${NAME1.text}:${NAME2.text}';
|
||||||
|
}
|
18
lib/src/language/ast/argument.dart
Normal file
18
lib/src/language/ast/argument.dart
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import '../token.dart';
|
||||||
|
import 'node.dart';
|
||||||
|
import 'package:source_span/src/span.dart';
|
||||||
|
import 'value_or_variable.dart';
|
||||||
|
|
||||||
|
class ArgumentContext extends Node {
|
||||||
|
final Token NAME, COLON;
|
||||||
|
final ValueOrVariableContext valueOrVariable;
|
||||||
|
|
||||||
|
ArgumentContext(this.NAME, this.COLON, this.valueOrVariable);
|
||||||
|
|
||||||
|
@override
|
||||||
|
SourceSpan get span =>
|
||||||
|
new SourceSpan(NAME.span?.start, valueOrVariable.end, toSource());
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toSource() => '${NAME.text}:${valueOrVariable.toSource()}';
|
||||||
|
}
|
26
lib/src/language/ast/array_value.dart
Normal file
26
lib/src/language/ast/array_value.dart
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import '../token.dart';
|
||||||
|
import 'package:source_span/src/span.dart';
|
||||||
|
import 'value.dart';
|
||||||
|
|
||||||
|
class ArrayValueContext extends ValueContext {
|
||||||
|
final Token LBRACKET, RBRACKET;
|
||||||
|
final List<ValueContext> values = [];
|
||||||
|
|
||||||
|
ArrayValueContext(this.LBRACKET, this.RBRACKET);
|
||||||
|
|
||||||
|
@override
|
||||||
|
SourceSpan get span =>
|
||||||
|
new SourceSpan(LBRACKET.span?.end, RBRACKET.span?.end, toSource());
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toSource() {
|
||||||
|
var buf = new StringBuffer('[');
|
||||||
|
|
||||||
|
for (int i = 0; i < values.length; i++) {
|
||||||
|
if (i > 0) buf.write(',');
|
||||||
|
buf.write(values[i].toSource());
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.toString() + ']';
|
||||||
|
}
|
||||||
|
}
|
29
lib/src/language/ast/ast.dart
Normal file
29
lib/src/language/ast/ast.dart
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
library graphql_parser.language.ast;
|
||||||
|
|
||||||
|
export 'alias.dart';
|
||||||
|
export 'argument.dart';
|
||||||
|
export 'boolean_value.dart';
|
||||||
|
export 'default_value.dart';
|
||||||
|
export 'definition.dart';
|
||||||
|
export 'directive.dart';
|
||||||
|
export 'document.dart';
|
||||||
|
export 'field.dart';
|
||||||
|
export 'field_name.dart';
|
||||||
|
export 'fragment_definition.dart';
|
||||||
|
export 'fragment_spread.dart';
|
||||||
|
export 'inline_fragment.dart';
|
||||||
|
export 'list_type.dart';
|
||||||
|
export 'node.dart';
|
||||||
|
export 'number_value.dart';
|
||||||
|
export 'operation_definition.dart';
|
||||||
|
export 'selection.dart';
|
||||||
|
export 'selection_set.dart';
|
||||||
|
export 'string_value.dart';
|
||||||
|
export 'type.dart';
|
||||||
|
export 'type_condition.dart';
|
||||||
|
export 'type_name.dart';
|
||||||
|
export 'value.dart';
|
||||||
|
export 'value_or_variable.dart';
|
||||||
|
export 'variable.dart';
|
||||||
|
export 'variable_definition.dart';
|
||||||
|
export 'variable_definitions.dart';
|
15
lib/src/language/ast/boolean_value.dart
Normal file
15
lib/src/language/ast/boolean_value.dart
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import '../token.dart';
|
||||||
|
import 'package:source_span/src/span.dart';
|
||||||
|
import 'value.dart';
|
||||||
|
|
||||||
|
class BooleanValueContext extends ValueContext {
|
||||||
|
final Token BOOLEAN;
|
||||||
|
|
||||||
|
BooleanValueContext(this.BOOLEAN);
|
||||||
|
|
||||||
|
@override
|
||||||
|
SourceSpan get span => BOOLEAN.span;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toSource() => BOOLEAN.text;
|
||||||
|
}
|
18
lib/src/language/ast/default_value.dart
Normal file
18
lib/src/language/ast/default_value.dart
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import '../token.dart';
|
||||||
|
import 'node.dart';
|
||||||
|
import 'package:source_span/src/span.dart';
|
||||||
|
import 'value.dart';
|
||||||
|
|
||||||
|
class DefaultValueContext extends Node {
|
||||||
|
final Token EQUALS;
|
||||||
|
final ValueContext value;
|
||||||
|
|
||||||
|
DefaultValueContext(this.EQUALS, this.value);
|
||||||
|
|
||||||
|
@override
|
||||||
|
SourceSpan get span =>
|
||||||
|
new SourceSpan(EQUALS.span?.start, value.end, toSource());
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toSource() => '=${value.toSource()}';
|
||||||
|
}
|
5
lib/src/language/ast/definition.dart
Normal file
5
lib/src/language/ast/definition.dart
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import 'node.dart';
|
||||||
|
|
||||||
|
abstract class DefinitionContext extends Node {
|
||||||
|
|
||||||
|
}
|
37
lib/src/language/ast/directive.dart
Normal file
37
lib/src/language/ast/directive.dart
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import '../token.dart';
|
||||||
|
import 'argument.dart';
|
||||||
|
import 'node.dart';
|
||||||
|
import 'package:source_span/source_span.dart';
|
||||||
|
import 'value_or_variable.dart';
|
||||||
|
|
||||||
|
class DirectiveContext extends Node {
|
||||||
|
final Token ARROBA, NAME, COLON, LPAREN, RPAREN;
|
||||||
|
final ArgumentContext argument;
|
||||||
|
final ValueOrVariableContext valueOrVariable;
|
||||||
|
|
||||||
|
DirectiveContext(this.ARROBA, this.NAME, this.COLON, this.LPAREN, this.RPAREN,
|
||||||
|
this.argument, this.valueOrVariable) {
|
||||||
|
assert(NAME != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
SourceSpan get span {
|
||||||
|
SourceLocation end = NAME.span?.end;
|
||||||
|
|
||||||
|
if (valueOrVariable != null)
|
||||||
|
end = valueOrVariable.end;
|
||||||
|
else if (RPAREN != null) end = RPAREN.span?.end;
|
||||||
|
|
||||||
|
return new SourceSpan(ARROBA.span?.start, end, toSource());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toSource() {
|
||||||
|
if (valueOrVariable != null)
|
||||||
|
return '@${NAME.text}:${valueOrVariable.toSource()}';
|
||||||
|
else if (argument != null)
|
||||||
|
return '@${NAME.text}(${argument.toSource()})';
|
||||||
|
else
|
||||||
|
return '@${NAME.text}';
|
||||||
|
}
|
||||||
|
}
|
20
lib/src/language/ast/document.dart
Normal file
20
lib/src/language/ast/document.dart
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import 'definition.dart';
|
||||||
|
import 'node.dart';
|
||||||
|
import 'package:source_span/src/span.dart';
|
||||||
|
|
||||||
|
class DocumentContext extends Node {
|
||||||
|
final List<DefinitionContext> definitions = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
SourceSpan get span {
|
||||||
|
if (definitions.isEmpty) return null;
|
||||||
|
return new SourceSpan(
|
||||||
|
definitions.first.start, definitions.last.end, toSource());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toSource() {
|
||||||
|
if (definitions.isEmpty) return '(empty document)';
|
||||||
|
return definitions.map((d) => d.toSource()).join();
|
||||||
|
}
|
||||||
|
}
|
35
lib/src/language/ast/field.dart
Normal file
35
lib/src/language/ast/field.dart
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import 'argument.dart';
|
||||||
|
import 'directive.dart';
|
||||||
|
import 'field_name.dart';
|
||||||
|
import 'node.dart';
|
||||||
|
import 'package:source_span/source_span.dart';
|
||||||
|
import 'selection_set.dart';
|
||||||
|
|
||||||
|
class FieldContext extends Node {
|
||||||
|
final FieldNameContext fieldName;
|
||||||
|
final List<ArgumentContext> arguments = [];
|
||||||
|
final List<DirectiveContext> directives = [];
|
||||||
|
final SelectionSetContext selectionSet;
|
||||||
|
|
||||||
|
FieldContext(this.fieldName, [this.selectionSet]);
|
||||||
|
|
||||||
|
@override
|
||||||
|
SourceSpan get span {
|
||||||
|
SourceLocation end = fieldName.end;
|
||||||
|
|
||||||
|
if (selectionSet != null)
|
||||||
|
end = selectionSet.end;
|
||||||
|
else if (directives.isNotEmpty)
|
||||||
|
end = directives.last.end;
|
||||||
|
else if (arguments.isNotEmpty) end = arguments.last.end;
|
||||||
|
|
||||||
|
return new SourceSpan(fieldName.start, end, toSource());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toSource() =>
|
||||||
|
fieldName.toSource() +
|
||||||
|
arguments.map((a) => a.toSource()).join() +
|
||||||
|
directives.map((d) => d.toSource()).join() +
|
||||||
|
(selectionSet?.toSource() ?? '');
|
||||||
|
}
|
19
lib/src/language/ast/field_name.dart
Normal file
19
lib/src/language/ast/field_name.dart
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import '../token.dart';
|
||||||
|
import 'alias.dart';
|
||||||
|
import 'node.dart';
|
||||||
|
import 'package:source_span/src/span.dart';
|
||||||
|
|
||||||
|
class FieldNameContext extends Node {
|
||||||
|
final Token NAME;
|
||||||
|
final AliasContext alias;
|
||||||
|
|
||||||
|
FieldNameContext(this.NAME, [this.alias]) {
|
||||||
|
assert(NAME != null || alias != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
SourceSpan get span => alias?.span ?? NAME.span;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toSource() => alias?.toSource() ?? NAME.text;
|
||||||
|
}
|
29
lib/src/language/ast/fragment_definition.dart
Normal file
29
lib/src/language/ast/fragment_definition.dart
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import '../token.dart';
|
||||||
|
import 'definition.dart';
|
||||||
|
import 'directive.dart';
|
||||||
|
import 'package:source_span/src/span.dart';
|
||||||
|
import 'selection_set.dart';
|
||||||
|
import 'type_condition.dart';
|
||||||
|
|
||||||
|
class FragmentDefinitionContext extends DefinitionContext {
|
||||||
|
final Token FRAGMENT, NAME, ON;
|
||||||
|
final TypeConditionContext typeCondition;
|
||||||
|
final List<DirectiveContext> directives = [];
|
||||||
|
final SelectionSetContext selectionSet;
|
||||||
|
|
||||||
|
String get name => NAME.text;
|
||||||
|
|
||||||
|
FragmentDefinitionContext(
|
||||||
|
this.FRAGMENT, this.NAME, this.ON, this.typeCondition, this.selectionSet);
|
||||||
|
|
||||||
|
@override
|
||||||
|
SourceSpan get span =>
|
||||||
|
new SourceSpan(FRAGMENT.span?.start, selectionSet.end, toSource());
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toSource() =>
|
||||||
|
'fragment ${NAME.text} on ' +
|
||||||
|
typeCondition.toSource() +
|
||||||
|
directives.map((d) => d.toSource()).join() +
|
||||||
|
selectionSet.toSource();
|
||||||
|
}
|
22
lib/src/language/ast/fragment_spread.dart
Normal file
22
lib/src/language/ast/fragment_spread.dart
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import '../token.dart';
|
||||||
|
import 'directive.dart';
|
||||||
|
import 'node.dart';
|
||||||
|
import 'package:source_span/source_span.dart';
|
||||||
|
|
||||||
|
class FragmentSpreadContext extends Node {
|
||||||
|
final Token ELLIPSIS, NAME;
|
||||||
|
final List<DirectiveContext> directives = [];
|
||||||
|
|
||||||
|
FragmentSpreadContext(this.ELLIPSIS, this.NAME);
|
||||||
|
|
||||||
|
@override
|
||||||
|
SourceSpan get span {
|
||||||
|
SourceLocation end;
|
||||||
|
return new SourceSpan(ELLIPSIS.span?.start, end, toSource());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toSource() {
|
||||||
|
return '...${NAME.text}' + directives.map((d) => d.toSource()).join();
|
||||||
|
}
|
||||||
|
}
|
26
lib/src/language/ast/inline_fragment.dart
Normal file
26
lib/src/language/ast/inline_fragment.dart
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import '../token.dart';
|
||||||
|
import 'directive.dart';
|
||||||
|
import 'node.dart';
|
||||||
|
import 'package:source_span/src/span.dart';
|
||||||
|
import 'selection_set.dart';
|
||||||
|
import 'type_condition.dart';
|
||||||
|
|
||||||
|
class InlineFragmentContext extends Node {
|
||||||
|
final Token ELLIPSIS, ON;
|
||||||
|
final TypeConditionContext typeCondition;
|
||||||
|
final List<DirectiveContext> directives = [];
|
||||||
|
final SelectionSetContext selectionSet;
|
||||||
|
|
||||||
|
InlineFragmentContext(
|
||||||
|
this.ELLIPSIS, this.ON, this.typeCondition, this.selectionSet);
|
||||||
|
|
||||||
|
@override
|
||||||
|
SourceSpan get span =>
|
||||||
|
new SourceSpan(ELLIPSIS.span?.start, selectionSet.end, toSource());
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toSource() =>
|
||||||
|
'...on${typeCondition.toSource()}' +
|
||||||
|
directives.map((d) => d.toSource()).join() +
|
||||||
|
selectionSet.toSource();
|
||||||
|
}
|
18
lib/src/language/ast/list_type.dart
Normal file
18
lib/src/language/ast/list_type.dart
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import '../token.dart';
|
||||||
|
import 'node.dart';
|
||||||
|
import 'package:source_span/src/span.dart';
|
||||||
|
import 'type.dart';
|
||||||
|
|
||||||
|
class ListTypeContext extends Node {
|
||||||
|
final Token LBRACKET, RBRACKET;
|
||||||
|
final TypeContext type;
|
||||||
|
|
||||||
|
ListTypeContext(this.LBRACKET, this.type, this.RBRACKET);
|
||||||
|
|
||||||
|
@override
|
||||||
|
SourceSpan get span =>
|
||||||
|
new SourceSpan(LBRACKET.span?.end, RBRACKET.span?.end, toSource());
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toSource() => '[${type.toSource()}]';
|
||||||
|
}
|
13
lib/src/language/ast/node.dart
Normal file
13
lib/src/language/ast/node.dart
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import 'package:source_span/source_span.dart';
|
||||||
|
|
||||||
|
abstract class Node {
|
||||||
|
SourceSpan get span;
|
||||||
|
|
||||||
|
SourceLocation get start => span.start;
|
||||||
|
SourceLocation get end => span.end;
|
||||||
|
|
||||||
|
String toSource();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => '${runtimeType}: ${toSource()}';
|
||||||
|
}
|
15
lib/src/language/ast/number_value.dart
Normal file
15
lib/src/language/ast/number_value.dart
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import '../token.dart';
|
||||||
|
import 'package:source_span/src/span.dart';
|
||||||
|
import 'value.dart';
|
||||||
|
|
||||||
|
class NumberValueContext extends ValueContext {
|
||||||
|
final Token NUMBER;
|
||||||
|
|
||||||
|
NumberValueContext(this.NUMBER);
|
||||||
|
|
||||||
|
@override
|
||||||
|
SourceSpan get span => NUMBER.span;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toSource() => NUMBER.text;
|
||||||
|
}
|
37
lib/src/language/ast/operation_definition.dart
Normal file
37
lib/src/language/ast/operation_definition.dart
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import '../token.dart';
|
||||||
|
import 'definition.dart';
|
||||||
|
import 'directive.dart';
|
||||||
|
import 'package:source_span/src/span.dart';
|
||||||
|
import 'selection_set.dart';
|
||||||
|
import 'variable_definitions.dart';
|
||||||
|
|
||||||
|
class OperationDefinitionContext extends DefinitionContext {
|
||||||
|
final Token TYPE, NAME;
|
||||||
|
final VariableDefinitionsContext variableDefinitions;
|
||||||
|
final List<DirectiveContext> directives = [];
|
||||||
|
final SelectionSetContext selectionSet;
|
||||||
|
|
||||||
|
bool get isMutation => TYPE.text == 'mutation';
|
||||||
|
bool get isQuery => TYPE.text == 'query';
|
||||||
|
|
||||||
|
String get name => NAME.text;
|
||||||
|
|
||||||
|
OperationDefinitionContext(
|
||||||
|
this.TYPE, this.NAME, this.variableDefinitions, this.selectionSet) {
|
||||||
|
assert(TYPE == null || TYPE.text == 'query' || TYPE.text == 'mutation');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
SourceSpan get span {
|
||||||
|
if (TYPE == null) return selectionSet.span;
|
||||||
|
return new SourceSpan(TYPE.span?.start, selectionSet.end, toSource());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toSource() {
|
||||||
|
if (TYPE == null) return selectionSet.toSource();
|
||||||
|
return '${TYPE.text} ${NAME.text} ${variableDefinitions.toSource()} ' +
|
||||||
|
directives.map((d) => d.toSource()).join() +
|
||||||
|
' ${selectionSet.toSource()}';
|
||||||
|
}
|
||||||
|
}
|
25
lib/src/language/ast/selection.dart
Normal file
25
lib/src/language/ast/selection.dart
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import 'field.dart';
|
||||||
|
import 'fragment_spread.dart';
|
||||||
|
import 'inline_fragment.dart';
|
||||||
|
import 'node.dart';
|
||||||
|
import 'package:source_span/src/span.dart';
|
||||||
|
|
||||||
|
class SelectionContext extends Node {
|
||||||
|
final FieldContext field;
|
||||||
|
final FragmentSpreadContext fragmentSpread;
|
||||||
|
final InlineFragmentContext inlineFragment;
|
||||||
|
|
||||||
|
SelectionContext(this.field, [this.fragmentSpread, this.inlineFragment]) {
|
||||||
|
assert(field != null || fragmentSpread != null || inlineFragment != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
SourceSpan get span =>
|
||||||
|
field?.span ?? fragmentSpread?.span ?? inlineFragment?.span;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toSource() =>
|
||||||
|
field?.toSource() ??
|
||||||
|
fragmentSpread?.toSource() ??
|
||||||
|
inlineFragment?.toSource();
|
||||||
|
}
|
27
lib/src/language/ast/selection_set.dart
Normal file
27
lib/src/language/ast/selection_set.dart
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import '../token.dart';
|
||||||
|
import 'node.dart';
|
||||||
|
import 'package:source_span/src/span.dart';
|
||||||
|
import 'selection.dart';
|
||||||
|
|
||||||
|
class SelectionSetContext extends Node {
|
||||||
|
final Token LBRACE, RBRACE;
|
||||||
|
final List<SelectionContext> selections = [];
|
||||||
|
|
||||||
|
SelectionSetContext(this.LBRACE, this.RBRACE);
|
||||||
|
|
||||||
|
@override
|
||||||
|
SourceSpan get span =>
|
||||||
|
new SourceSpan(LBRACE.span?.start, RBRACE.span?.end, toSource());
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toSource() {
|
||||||
|
var buf = new StringBuffer('{');
|
||||||
|
|
||||||
|
for (int i = 0; i < selections.length; i++) {
|
||||||
|
if (i > 0) buf.write(',');
|
||||||
|
buf.write(selections[i].toSource());
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.toString() + '}';
|
||||||
|
}
|
||||||
|
}
|
15
lib/src/language/ast/string_value.dart
Normal file
15
lib/src/language/ast/string_value.dart
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import '../token.dart';
|
||||||
|
import 'package:source_span/src/span.dart';
|
||||||
|
import 'value.dart';
|
||||||
|
|
||||||
|
class StringValueContext extends ValueContext {
|
||||||
|
final Token STRING;
|
||||||
|
|
||||||
|
StringValueContext(this.STRING);
|
||||||
|
|
||||||
|
@override
|
||||||
|
SourceSpan get span => STRING.span;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toSource() => STRING.text;
|
||||||
|
}
|
49
lib/src/language/ast/type.dart
Normal file
49
lib/src/language/ast/type.dart
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import 'package:source_span/source_span.dart';
|
||||||
|
import '../token.dart';
|
||||||
|
import 'list_type.dart';
|
||||||
|
import 'node.dart';
|
||||||
|
import 'type_name.dart';
|
||||||
|
|
||||||
|
class TypeContext extends Node {
|
||||||
|
final Token EXCLAMATION;
|
||||||
|
final TypeNameContext typeName;
|
||||||
|
final ListTypeContext listType;
|
||||||
|
|
||||||
|
bool get nonNullType => EXCLAMATION != null;
|
||||||
|
|
||||||
|
TypeContext(this.typeName, this.listType, [this.EXCLAMATION]) {
|
||||||
|
assert(typeName != null || listType != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
SourceSpan get span {
|
||||||
|
SourceLocation start, end;
|
||||||
|
|
||||||
|
if (typeName != null) {
|
||||||
|
start = typeName.start;
|
||||||
|
end = typeName.end;
|
||||||
|
} else if (listType != null) {
|
||||||
|
start = listType.start;
|
||||||
|
end = listType.end;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (EXCLAMATION != null) end = EXCLAMATION.span?.end;
|
||||||
|
|
||||||
|
return new SourceSpan(start, end, toSource());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toSource() {
|
||||||
|
var buf = new StringBuffer();
|
||||||
|
|
||||||
|
if (typeName != null) {
|
||||||
|
buf.write(typeName.toSource());
|
||||||
|
} else if (listType != null) {
|
||||||
|
buf.write(listType.toSource());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nonNullType) buf.write('!');
|
||||||
|
|
||||||
|
return buf.toString();
|
||||||
|
}
|
||||||
|
}
|
15
lib/src/language/ast/type_condition.dart
Normal file
15
lib/src/language/ast/type_condition.dart
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import 'node.dart';
|
||||||
|
import 'package:source_span/src/span.dart';
|
||||||
|
import 'type_name.dart';
|
||||||
|
|
||||||
|
class TypeConditionContext extends Node {
|
||||||
|
final TypeNameContext typeName;
|
||||||
|
|
||||||
|
TypeConditionContext(this.typeName);
|
||||||
|
|
||||||
|
@override
|
||||||
|
SourceSpan get span => typeName.span;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toSource() => typeName.toSource();
|
||||||
|
}
|
15
lib/src/language/ast/type_name.dart
Normal file
15
lib/src/language/ast/type_name.dart
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import 'node.dart';
|
||||||
|
import 'package:source_span/src/span.dart';
|
||||||
|
import '../token.dart';
|
||||||
|
|
||||||
|
class TypeNameContext extends Node {
|
||||||
|
final Token NAME;
|
||||||
|
|
||||||
|
@override
|
||||||
|
SourceSpan get span => NAME.span;
|
||||||
|
|
||||||
|
TypeNameContext(this.NAME);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toSource() => NAME.text;
|
||||||
|
}
|
3
lib/src/language/ast/value.dart
Normal file
3
lib/src/language/ast/value.dart
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import 'node.dart';
|
||||||
|
|
||||||
|
abstract class ValueContext extends Node {}
|
19
lib/src/language/ast/value_or_variable.dart
Normal file
19
lib/src/language/ast/value_or_variable.dart
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import 'node.dart';
|
||||||
|
import 'package:source_span/src/span.dart';
|
||||||
|
import 'value.dart';
|
||||||
|
import 'variable.dart';
|
||||||
|
|
||||||
|
class ValueOrVariableContext extends Node {
|
||||||
|
final ValueContext value;
|
||||||
|
final VariableContext variable;
|
||||||
|
|
||||||
|
ValueOrVariableContext(this.value, this.variable) {
|
||||||
|
assert(value != null || variable != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
SourceSpan get span => value?.span ?? variable.span;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toSource() => '${value?.toSource() ?? variable.toSource()}';
|
||||||
|
}
|
16
lib/src/language/ast/variable.dart
Normal file
16
lib/src/language/ast/variable.dart
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import '../token.dart';
|
||||||
|
import 'node.dart';
|
||||||
|
import 'package:source_span/src/span.dart';
|
||||||
|
|
||||||
|
class VariableContext extends Node {
|
||||||
|
final Token DOLLAR, NAME;
|
||||||
|
|
||||||
|
VariableContext(this.DOLLAR, this.NAME);
|
||||||
|
|
||||||
|
@override
|
||||||
|
SourceSpan get span =>
|
||||||
|
new SourceSpan(DOLLAR?.span?.start, NAME?.span?.end, toSource());
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toSource() => '\$${NAME.text}';
|
||||||
|
}
|
24
lib/src/language/ast/variable_definition.dart
Normal file
24
lib/src/language/ast/variable_definition.dart
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import '../token.dart';
|
||||||
|
import 'node.dart';
|
||||||
|
import 'default_value.dart';
|
||||||
|
import 'package:source_span/src/span.dart';
|
||||||
|
import 'type.dart';
|
||||||
|
import 'variable.dart';
|
||||||
|
|
||||||
|
class VariableDefinitionContext extends Node {
|
||||||
|
final Token COLON;
|
||||||
|
final VariableContext variable;
|
||||||
|
final TypeContext type;
|
||||||
|
final DefaultValueContext defaultValue;
|
||||||
|
|
||||||
|
VariableDefinitionContext(this.variable, this.COLON, this.type,
|
||||||
|
[this.defaultValue]);
|
||||||
|
|
||||||
|
@override
|
||||||
|
SourceSpan get span =>
|
||||||
|
new SourceSpan(variable.start, defaultValue?.end ?? type.end, toSource());
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toSource() =>
|
||||||
|
'${variable.toSource()}:${type.toSource()}${defaultValue?.toSource() ?? ""}';
|
||||||
|
}
|
28
lib/src/language/ast/variable_definitions.dart
Normal file
28
lib/src/language/ast/variable_definitions.dart
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import '../token.dart';
|
||||||
|
import 'node.dart';
|
||||||
|
import 'package:source_span/src/span.dart';
|
||||||
|
import 'variable_definition.dart';
|
||||||
|
|
||||||
|
class VariableDefinitionsContext extends Node {
|
||||||
|
final Token LPAREN, RPAREN;
|
||||||
|
final List<VariableDefinitionContext> variableDefinitions = [];
|
||||||
|
|
||||||
|
VariableDefinitionsContext(this.LPAREN, this.RPAREN);
|
||||||
|
|
||||||
|
@override
|
||||||
|
SourceSpan get span =>
|
||||||
|
new SourceSpan(LPAREN.span?.end, RPAREN.span?.end, toSource());
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toSource() {
|
||||||
|
var buf = new StringBuffer('[');
|
||||||
|
|
||||||
|
for (int i = 0; i < variableDefinitions.length; i++) {
|
||||||
|
if (i > 0) buf.write(',');
|
||||||
|
buf.write(variableDefinitions[i].toSource());
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.write(']');
|
||||||
|
return buf.toString();
|
||||||
|
}
|
||||||
|
}
|
6
lib/src/language/language.dart
Normal file
6
lib/src/language/language.dart
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
library graphql_parser.language;
|
||||||
|
|
||||||
|
export 'lexer.dart';
|
||||||
|
export 'parser.dart';
|
||||||
|
export 'token.dart';
|
||||||
|
export 'token_type.dart';
|
93
lib/src/language/lexer.dart
Normal file
93
lib/src/language/lexer.dart
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'package:string_scanner/string_scanner.dart';
|
||||||
|
import 'package:source_span/source_span.dart';
|
||||||
|
import 'syntax_error.dart';
|
||||||
|
import 'token.dart';
|
||||||
|
import 'token_type.dart';
|
||||||
|
|
||||||
|
final RegExp _whitespace = new RegExp('[ \t\n\r]+');
|
||||||
|
final RegExp _boolean = new RegExp(r'true|false');
|
||||||
|
final RegExp _number = new RegExp(r'-?[0-9]+(\.[0-9]+)?(E|e(\+|-)?[0-9]+)?');
|
||||||
|
final RegExp _string = new RegExp(
|
||||||
|
r'"((\\(["\\/bfnrt]|(u[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])))|([^"\\]))*"');
|
||||||
|
final RegExp _name = new RegExp(r'[_A-Za-z][_0-9A-Za-z]*');
|
||||||
|
|
||||||
|
final Map<Pattern, TokenType> _patterns = {
|
||||||
|
'@': TokenType.ARROBA,
|
||||||
|
':': TokenType.COLON,
|
||||||
|
',': TokenType.COMMA,
|
||||||
|
r'$': TokenType.DOLLAR,
|
||||||
|
'...': TokenType.ELLIPSIS,
|
||||||
|
'=': TokenType.EQUALS,
|
||||||
|
'!': TokenType.EXCLAMATION,
|
||||||
|
'{': TokenType.LBRACE,
|
||||||
|
'}': TokenType.RBRACE,
|
||||||
|
'[': TokenType.LBRACKET,
|
||||||
|
']': TokenType.RBRACKET,
|
||||||
|
'(': TokenType.LPAREN,
|
||||||
|
')': TokenType.RPAREN,
|
||||||
|
'fragment': TokenType.FRAGMENT,
|
||||||
|
'mutation': TokenType.MUTATION,
|
||||||
|
'on': TokenType.ON,
|
||||||
|
'query': TokenType.QUERY,
|
||||||
|
_boolean: TokenType.BOOLEAN,
|
||||||
|
_number: TokenType.NUMBER,
|
||||||
|
_string: TokenType.STRING,
|
||||||
|
_name: TokenType.NAME
|
||||||
|
};
|
||||||
|
|
||||||
|
class Lexer implements StreamTransformer<String, Token> {
|
||||||
|
@override
|
||||||
|
Stream<Token> bind(Stream<String> stream) {
|
||||||
|
var ctrl = new StreamController<Token>();
|
||||||
|
|
||||||
|
stream.listen((str) {
|
||||||
|
var scanner = new StringScanner(str);
|
||||||
|
int line = 1, column = 1;
|
||||||
|
|
||||||
|
while (!scanner.isDone) {
|
||||||
|
List<Token> potential = [];
|
||||||
|
|
||||||
|
if (scanner.scan(_whitespace)) {
|
||||||
|
var text = scanner.lastMatch[0];
|
||||||
|
line += '\n'.allMatches(text).length;
|
||||||
|
var lastNewLine = text.lastIndexOf('\n');
|
||||||
|
|
||||||
|
if (lastNewLine != -1) {
|
||||||
|
int len = text.substring(lastNewLine + 1).length;
|
||||||
|
column = 1 + len;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var pattern in _patterns.keys) {
|
||||||
|
if (scanner.matches(pattern)) {
|
||||||
|
potential.add(new Token(_patterns[pattern], scanner.lastMatch[0]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (potential.isEmpty) {
|
||||||
|
var ch = new String.fromCharCode(scanner.readChar());
|
||||||
|
ctrl.addError(new SyntaxError("Unexpected token '$ch'.", line, column));
|
||||||
|
} else {
|
||||||
|
// Choose longest token
|
||||||
|
potential.sort((a, b) => a.text.length.compareTo(b.text.length));
|
||||||
|
var chosen = potential.first;
|
||||||
|
var start =
|
||||||
|
new SourceLocation(scanner.position, line: line, column: column);
|
||||||
|
ctrl.add(chosen);
|
||||||
|
scanner.position += chosen.text.length;
|
||||||
|
column += chosen.text.length;
|
||||||
|
var end =
|
||||||
|
new SourceLocation(scanner.position, line: line, column: column);
|
||||||
|
chosen.span = new SourceSpan(start, end, chosen.text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
..onDone(ctrl.close)
|
||||||
|
..onError(ctrl.addError);
|
||||||
|
|
||||||
|
return ctrl.stream;
|
||||||
|
}
|
||||||
|
}
|
26
lib/src/language/parser.dart
Normal file
26
lib/src/language/parser.dart
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'ast/ast.dart';
|
||||||
|
import 'stream_reader.dart';
|
||||||
|
import 'token.dart';
|
||||||
|
|
||||||
|
class Parser implements StreamConsumer<Token> {
|
||||||
|
bool _closed = false;
|
||||||
|
final StreamReader<Token> _reader = new StreamReader();
|
||||||
|
|
||||||
|
final StreamController<Node> _onNode = new StreamController<Node>();
|
||||||
|
|
||||||
|
Stream<Node> get onNode => _onNode.stream;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future addStream(Stream<Token> stream) async {
|
||||||
|
if (_closed) throw new StateError('Parser is already closed.');
|
||||||
|
stream.pipe(_reader);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future close() async {
|
||||||
|
_closed = true;
|
||||||
|
|
||||||
|
await _onNode.close();
|
||||||
|
}
|
||||||
|
}
|
79
lib/src/language/stream_reader.dart
Normal file
79
lib/src/language/stream_reader.dart
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:collection';
|
||||||
|
|
||||||
|
class StreamReader<T> implements StreamConsumer<T> {
|
||||||
|
final Queue<T> _buffer = new Queue();
|
||||||
|
bool _closed = false;
|
||||||
|
final Queue<Completer<T>> _nextQueue = new Queue();
|
||||||
|
final Queue<Completer<T>> _peekQueue = new Queue();
|
||||||
|
|
||||||
|
bool get isDone => _closed;
|
||||||
|
|
||||||
|
Future<T> peek() {
|
||||||
|
if (isDone) throw new StateError('Cannot read from closed stream.');
|
||||||
|
if (_buffer.isNotEmpty) return new Future.value(_buffer.first);
|
||||||
|
|
||||||
|
var c = new Completer<T>();
|
||||||
|
_peekQueue.addLast(c);
|
||||||
|
return c.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<T> next() {
|
||||||
|
if (isDone) throw new StateError('Cannot read from closed stream.');
|
||||||
|
if (_buffer.isNotEmpty) return new Future.value(_buffer.removeFirst());
|
||||||
|
|
||||||
|
var c = new Completer<T>();
|
||||||
|
_nextQueue.addLast(c);
|
||||||
|
return c.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future addStream(Stream<T> stream) {
|
||||||
|
if (_closed) throw new StateError('StreamReader has already been used.');
|
||||||
|
|
||||||
|
var c = new Completer();
|
||||||
|
|
||||||
|
stream.listen((data) {
|
||||||
|
if (_peekQueue.isNotEmpty || _nextQueue.isNotEmpty) {
|
||||||
|
if (_peekQueue.isNotEmpty) {
|
||||||
|
_peekQueue.removeFirst().complete(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_nextQueue.isNotEmpty) {
|
||||||
|
_nextQueue.removeFirst().complete(data);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_buffer.add(data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
..onDone(c.complete)
|
||||||
|
..onError(c.completeError);
|
||||||
|
|
||||||
|
return c.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future close() async {
|
||||||
|
_closed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _IteratorReader<T> {
|
||||||
|
final Iterator<T> _tokens;
|
||||||
|
|
||||||
|
T _current;
|
||||||
|
|
||||||
|
_IteratorReader(this._tokens) {
|
||||||
|
_tokens.moveNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
T advance() {
|
||||||
|
_current = _tokens.current;
|
||||||
|
_tokens.moveNext();
|
||||||
|
return _current;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get eof => _tokens.current == null;
|
||||||
|
|
||||||
|
T peek() => _tokens.current;
|
||||||
|
}
|
18
lib/src/language/syntax_error.dart
Normal file
18
lib/src/language/syntax_error.dart
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import 'package:source_span/source_span.dart';
|
||||||
|
import 'token.dart';
|
||||||
|
|
||||||
|
class SyntaxError implements Exception {
|
||||||
|
final String message;
|
||||||
|
final int line, column;
|
||||||
|
final Token offendingToken;
|
||||||
|
|
||||||
|
SyntaxError(this.message, this.line, this.column, [this.offendingToken]);
|
||||||
|
|
||||||
|
factory SyntaxError.fromSourceLocation(
|
||||||
|
String message, SourceLocation location,
|
||||||
|
[Token offendingToken]) =>
|
||||||
|
new SyntaxError(message, location.line, location.column, offendingToken);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'Syntax error at line $line, column $column: $message';
|
||||||
|
}
|
18
lib/src/language/token.dart
Normal file
18
lib/src/language/token.dart
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import 'package:source_span/source_span.dart';
|
||||||
|
import 'token_type.dart';
|
||||||
|
|
||||||
|
class Token {
|
||||||
|
final TokenType type;
|
||||||
|
final String text;
|
||||||
|
SourceSpan span;
|
||||||
|
|
||||||
|
Token(this.type, this.text, [this.span]);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
if (span == null)
|
||||||
|
return "'$text' -> $type";
|
||||||
|
else
|
||||||
|
return "(${span.start.line}:${span.start.column}) '$text' -> $type";
|
||||||
|
}
|
||||||
|
}
|
26
lib/src/language/token_type.dart
Normal file
26
lib/src/language/token_type.dart
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
enum TokenType {
|
||||||
|
ARROBA,
|
||||||
|
COLON,
|
||||||
|
COMMA,
|
||||||
|
DOLLAR,
|
||||||
|
ELLIPSIS,
|
||||||
|
EQUALS,
|
||||||
|
EXCLAMATION,
|
||||||
|
LBRACE,
|
||||||
|
RBRACE,
|
||||||
|
LBRACKET,
|
||||||
|
RBRACKET,
|
||||||
|
LPAREN,
|
||||||
|
RPAREN,
|
||||||
|
|
||||||
|
FRAGMENT,
|
||||||
|
MUTATION,
|
||||||
|
ON,
|
||||||
|
QUERY,
|
||||||
|
|
||||||
|
BOOLEAN,
|
||||||
|
NUMBER,
|
||||||
|
STRING,
|
||||||
|
|
||||||
|
NAME
|
||||||
|
}
|
8
pubspec.yaml
Normal file
8
pubspec.yaml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
author: "Tobe O"
|
||||||
|
description: "Parses GraphQL queries and schemas."
|
||||||
|
homepage: "https://github.com/thosakwe/graphql_parser"
|
||||||
|
name: "graphql_parser"
|
||||||
|
version: "0.0.0"
|
||||||
|
dependencies:
|
||||||
|
source_span: "^1.3.1"
|
||||||
|
string_scanner: "^1.0.1"
|
Loading…
Reference in a new issue