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
|
||||
.project
|
||||
.pub/
|
||||
.scripts-bin/
|
||||
build/
|
||||
**/packages/
|
||||
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
# graphql
|
||||
Generates services and models from GraphQL.
|
||||
# graphql_parser
|
||||
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