Values done
This commit is contained in:
parent
96aba898f3
commit
0f68a1c500
18 changed files with 447 additions and 351 deletions
65
.gitignore
vendored
65
.gitignore
vendored
|
@ -26,3 +26,68 @@ doc/api/
|
|||
# Don't commit pubspec lock file
|
||||
# (Library packages only! Remove pattern if developing an application package)
|
||||
pubspec.lock
|
||||
### Dart template
|
||||
# See https://www.dartlang.org/tools/private-files.html
|
||||
|
||||
# Files and directories created by pub
|
||||
|
||||
# SDK 1.20 and later (no longer creates packages directories)
|
||||
|
||||
# Older SDK versions
|
||||
# (Include if the minimum SDK version specified in pubsepc.yaml is earlier than 1.20)
|
||||
|
||||
|
||||
# Files created by dart2js
|
||||
# (Most Dart developers will use pub build to compile Dart, use/modify these
|
||||
# rules if you intend to use dart2js directly
|
||||
# Convention is to use extension '.dart.js' for Dart compiled to Javascript to
|
||||
# differentiate from explicit Javascript files)
|
||||
|
||||
# Directory created by dartdoc
|
||||
|
||||
# Don't commit pubspec lock file
|
||||
# (Library packages only! Remove pattern if developing an application package)
|
||||
### JetBrains template
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff:
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/dictionaries
|
||||
|
||||
# Sensitive or high-churn files:
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.xml
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
|
||||
# Gradle:
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Mongo Explorer plugin:
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
## File-based project format:
|
||||
*.iws
|
||||
|
||||
## Plugin-specific files:
|
||||
|
||||
# IntelliJ
|
||||
/out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
|
1
.travis.yml
Normal file
1
.travis.yml
Normal file
|
@ -0,0 +1 @@
|
|||
language: dart
|
37
README.md
37
README.md
|
@ -1,2 +1,39 @@
|
|||
# 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)
|
||||
|
||||
Parses GraphQL queries and schemas.
|
||||
|
||||
*This library is merely a parser*. 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)
|
||||
users should consider
|
||||
[`package:angel_graphql`](https://pub.dartlang.org/packages/angel_graphql)
|
||||
as a dead-simple way to add GraphQL functionality to their servers.
|
||||
|
||||
# Installation
|
||||
Add `graphql_parser` as a dependency in your `pubspec.yaml` file:
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
graphql_parser: ^1.0.0
|
||||
```
|
||||
|
||||
# Usage
|
||||
The AST featured in this library is directly based off this ANTLR4 grammar created by Joseph T. McBride:
|
||||
https://github.com/antlr/grammars-v4/blob/master/graphql/GraphQL.g4
|
||||
|
||||
```dart
|
||||
import 'package:graphql_parser/graphql_parser.dart';
|
||||
|
||||
doSomething(String text) {
|
||||
var tokens = scan(text);
|
||||
var parser = new Parser(tokens);
|
||||
|
||||
// Parse the GraphQL document using recursive descent
|
||||
var doc = parser.parseDocument();
|
||||
|
||||
// Do something with the parsed GraphQL document...
|
||||
}
|
||||
```
|
||||
|
|
|
@ -1,22 +1,15 @@
|
|||
import 'dart:async';
|
||||
import 'package:graphql_parser/src/language/language.dart';
|
||||
|
||||
Stream<String> input() async* {
|
||||
yield '''
|
||||
final String INPUT = '''
|
||||
{
|
||||
project(name: "GraphQL") {
|
||||
tagline
|
||||
}
|
||||
}
|
||||
'''
|
||||
.trim();
|
||||
}
|
||||
'''.trim();
|
||||
|
||||
main() {
|
||||
var lexer = new Lexer(), parser = new Parser();
|
||||
var stream = input().transform(lexer).asBroadcastStream();
|
||||
stream
|
||||
..forEach(print)
|
||||
..pipe(parser);
|
||||
parser.onNode.forEach(print);
|
||||
var tokens = scan(INPUT);
|
||||
var parser = new Parser(tokens);
|
||||
}
|
||||
|
|
2
lib/graphql_parser.dart
Normal file
2
lib/graphql_parser.dart
Normal file
|
@ -0,0 +1,2 @@
|
|||
export 'src/language/ast/ast.dart';
|
||||
export 'src/language/language.dart';
|
|
@ -12,6 +12,9 @@ class ArrayValueContext extends ValueContext {
|
|||
SourceSpan get span =>
|
||||
new SourceSpan(LBRACKET.span?.end, RBRACKET.span?.end, toSource());
|
||||
|
||||
@override
|
||||
List get value => values.map((v) => v.value).toList();
|
||||
|
||||
@override
|
||||
String toSource() {
|
||||
var buf = new StringBuffer('[');
|
||||
|
|
|
@ -3,13 +3,17 @@ import 'package:source_span/src/span.dart';
|
|||
import 'value.dart';
|
||||
|
||||
class BooleanValueContext extends ValueContext {
|
||||
bool _valueCache;
|
||||
final Token BOOLEAN;
|
||||
|
||||
BooleanValueContext(this.BOOLEAN) {
|
||||
assert(BOOLEAN?.text == 'true' || BOOLEAN?.text == 'false');
|
||||
}
|
||||
|
||||
bool get booleanValue => BOOLEAN.text == 'true';
|
||||
bool get booleanValue => _valueCache ??= BOOLEAN.text == 'true';
|
||||
|
||||
@override
|
||||
get value => booleanValue;
|
||||
|
||||
@override
|
||||
SourceSpan get span => BOOLEAN.span;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:math' as math;
|
||||
import 'package:source_span/source_span.dart';
|
||||
import '../token.dart';
|
||||
import 'package:source_span/src/span.dart';
|
||||
import 'value.dart';
|
||||
|
||||
class NumberValueContext extends ValueContext {
|
||||
|
@ -7,6 +8,21 @@ class NumberValueContext extends ValueContext {
|
|||
|
||||
NumberValueContext(this.NUMBER);
|
||||
|
||||
num get numberValue {
|
||||
var text = NUMBER.text;
|
||||
if (!text.contains('E') && !text.contains('e'))
|
||||
return num.parse(text);
|
||||
else {
|
||||
var split = text.split(text.contains('E') ? 'E' : 'e');
|
||||
var base = num.parse(split[0]);
|
||||
var exp = num.parse(split[1]);
|
||||
return base * math.pow(10, exp);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
get value => numberValue;
|
||||
|
||||
@override
|
||||
SourceSpan get span => NUMBER.span;
|
||||
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import 'dart:convert';
|
||||
import 'package:charcode/charcode.dart';
|
||||
import 'package:source_span/source_span.dart';
|
||||
import '../syntax_error.dart';
|
||||
import '../token.dart';
|
||||
import 'package:source_span/src/span.dart';
|
||||
import 'value.dart';
|
||||
|
||||
class StringValueContext extends ValueContext {
|
||||
|
@ -10,7 +13,61 @@ class StringValueContext extends ValueContext {
|
|||
@override
|
||||
SourceSpan get span => STRING.span;
|
||||
|
||||
String get stringValue => STRING.text.substring(0, STRING.text.length - 1);
|
||||
String get stringValue {
|
||||
var text = STRING.text.substring(1, STRING.text.length - 1);
|
||||
var codeUnits = text.codeUnits;
|
||||
var buf = new StringBuffer();
|
||||
|
||||
for (int i = 0; i < codeUnits.length; i++) {
|
||||
var ch = codeUnits[i];
|
||||
|
||||
if (ch == $backslash) {
|
||||
if (i < codeUnits.length - 5 && codeUnits[i + 1] == $u) {
|
||||
var c1 = codeUnits[i += 2],
|
||||
c2 = codeUnits[++i],
|
||||
c3 = codeUnits[++i],
|
||||
c4 = codeUnits[++i];
|
||||
var hexString = new String.fromCharCodes([c1, c2, c3, c4]);
|
||||
var hexNumber = int.parse(hexString, radix: 16);
|
||||
buf.write(new String.fromCharCode(hexNumber));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (i < codeUnits.length - 1) {
|
||||
var next = codeUnits[++i];
|
||||
|
||||
switch (next) {
|
||||
case $b:
|
||||
buf.write('\b');
|
||||
break;
|
||||
case $f:
|
||||
buf.write('\f');
|
||||
break;
|
||||
case $n:
|
||||
buf.writeCharCode($lf);
|
||||
break;
|
||||
case $r:
|
||||
buf.writeCharCode($cr);
|
||||
break;
|
||||
case $t:
|
||||
buf.writeCharCode($tab);
|
||||
break;
|
||||
default:
|
||||
buf.writeCharCode(next);
|
||||
}
|
||||
} else
|
||||
throw new SyntaxError.fromSourceLocation(
|
||||
'Unexpected "\\" in string literal.', span.start);
|
||||
} else {
|
||||
buf.writeCharCode(ch);
|
||||
}
|
||||
}
|
||||
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
get value => stringValue;
|
||||
|
||||
@override
|
||||
String toSource() => STRING.text;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import 'node.dart';
|
||||
|
||||
abstract class ValueContext extends Node {}
|
||||
abstract class ValueContext extends Node {
|
||||
get value;
|
||||
}
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
part of graphql_parser.language.parser;
|
||||
|
||||
abstract class BaseParser implements StreamConsumer<Token> {
|
||||
final StreamController<ArrayValueContext> _onArrayValue =
|
||||
new StreamController<ArrayValueContext>();
|
||||
final StreamController<BooleanValueContext> _onBooleanValue =
|
||||
new StreamController<BooleanValueContext>();
|
||||
final StreamController<DocumentContext> _onDocument =
|
||||
new StreamController<DocumentContext>();
|
||||
final StreamController<Node> _onNode = new StreamController<Node>();
|
||||
final StreamController<NumberValueContext> _onNumberValue =
|
||||
new StreamController<NumberValueContext>();
|
||||
final StreamController<StringValueContext> _onStringValue =
|
||||
new StreamController<StringValueContext>();
|
||||
final StreamController<ValueContext> _onValue =
|
||||
new StreamController<ValueContext>();
|
||||
|
||||
Stream<Node> get onArrayValue => _onArrayValue.stream;
|
||||
Stream<Node> get onBooleanValue => _onBooleanValue.stream;
|
||||
Stream<Node> get onDocument => _onDocument.stream;
|
||||
Stream<Node> get onNode => _onNode.stream;
|
||||
Stream<Node> get onNumberValue => _onNumberValue.stream;
|
||||
Stream<Node> get onStringValue => _onStringValue.stream;
|
||||
Stream<Node> get onValue => _onValue.stream;
|
||||
}
|
|
@ -2,5 +2,6 @@ library graphql_parser.language;
|
|||
|
||||
export 'lexer.dart';
|
||||
export 'parser.dart';
|
||||
export 'syntax_error.dart';
|
||||
export 'token.dart';
|
||||
export 'token_type.dart';
|
|
@ -1,4 +1,3 @@
|
|||
import 'dart:async';
|
||||
import 'package:string_scanner/string_scanner.dart';
|
||||
import 'syntax_error.dart';
|
||||
import 'token.dart';
|
||||
|
@ -35,43 +34,34 @@ final Map<Pattern, TokenType> _patterns = {
|
|||
_name: TokenType.NAME
|
||||
};
|
||||
|
||||
class Lexer implements StreamTransformer<String, Token> {
|
||||
@override
|
||||
Stream<Token> bind(Stream<String> stream) {
|
||||
var ctrl = new StreamController<Token>();
|
||||
List<Token> scan(String text) {
|
||||
List<Token> out = [];
|
||||
var scanner = new SpanScanner(text);
|
||||
|
||||
stream.listen((str) {
|
||||
var scanner = new SpanScanner(str);
|
||||
while (!scanner.isDone) {
|
||||
List<Token> potential = [];
|
||||
|
||||
while (!scanner.isDone) {
|
||||
List<Token> potential = [];
|
||||
if (scanner.scan(_whitespace)) continue;
|
||||
|
||||
if (scanner.scan(_whitespace)) 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'.",
|
||||
scanner.state.line, scanner.state.column));
|
||||
} else {
|
||||
// Choose longest token
|
||||
potential.sort((a, b) => b.text.length.compareTo(a.text.length));
|
||||
var chosen = potential.first;
|
||||
var start = scanner.state;
|
||||
ctrl.add(chosen);
|
||||
scanner.scan(chosen.text);
|
||||
chosen.span = scanner.spanFrom(start);
|
||||
}
|
||||
for (var pattern in _patterns.keys) {
|
||||
if (scanner.matches(pattern)) {
|
||||
potential.add(new Token(_patterns[pattern], scanner.lastMatch[0], scanner.lastSpan));
|
||||
}
|
||||
})
|
||||
..onDone(ctrl.close)
|
||||
..onError(ctrl.addError);
|
||||
}
|
||||
|
||||
return ctrl.stream;
|
||||
if (potential.isEmpty) {
|
||||
var ch = new String.fromCharCode(scanner.readChar());
|
||||
throw new SyntaxError(
|
||||
"Unexpected token '$ch'.", scanner.state.line, scanner.state.column);
|
||||
} else {
|
||||
// Choose longest token
|
||||
potential.sort((a, b) => b.text.length.compareTo(a.text.length));
|
||||
var chosen = potential.first;
|
||||
var start = scanner.state;
|
||||
out.add(chosen);
|
||||
scanner.scan(chosen.text);
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
|
|
@ -1,180 +1,187 @@
|
|||
library graphql_parser.language.parser;
|
||||
|
||||
import 'dart:async';
|
||||
import 'ast/ast.dart';
|
||||
import 'stream_reader.dart';
|
||||
import 'syntax_error.dart';
|
||||
import 'token.dart';
|
||||
import 'token_type.dart';
|
||||
part 'base_parser.dart';
|
||||
|
||||
class Parser extends BaseParser {
|
||||
bool _closed = false;
|
||||
final Completer _closer = new Completer();
|
||||
class Parser {
|
||||
Token _current;
|
||||
final List<SyntaxError> _errors = [];
|
||||
final StreamReader<Token> _reader = new StreamReader();
|
||||
int _index = -1;
|
||||
|
||||
final List<Token> tokens;
|
||||
|
||||
Parser(this.tokens);
|
||||
|
||||
Token get current => _current;
|
||||
|
||||
List<SyntaxError> get errors => new List<SyntaxError>.unmodifiable(_errors);
|
||||
|
||||
Future _waterfall(List<Function> futures) async {
|
||||
for (var f in futures) {
|
||||
var r = await f();
|
||||
if (r != null) return r;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future addStream(Stream<Token> stream) {
|
||||
if (_closed) throw new StateError('Parser is already closed.');
|
||||
|
||||
_closed = true;
|
||||
|
||||
_reader.onData
|
||||
.listen((data) => _waterfall([parseDocument, parseBooleanValue]))
|
||||
..onDone(() => Future.wait([
|
||||
_onBooleanValue.close(),
|
||||
_onDocument.close(),
|
||||
_onNode.close(),
|
||||
]))
|
||||
..onError(_closer.completeError);
|
||||
|
||||
return stream.pipe(_reader);
|
||||
}
|
||||
|
||||
@override
|
||||
Future close() {
|
||||
return _closer.future;
|
||||
}
|
||||
|
||||
Future<bool> expect(TokenType type) async {
|
||||
var peek = await _reader.peek();
|
||||
|
||||
if (peek?.type != type) {
|
||||
_errors.add(new SyntaxError.fromSourceLocation(
|
||||
"Expected $type, found '${peek?.text ?? 'empty text'}' instead.",
|
||||
peek?.span?.start));
|
||||
return false;
|
||||
} else {
|
||||
await _reader.consume();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> maybe(TokenType type) async {
|
||||
var peek = await _reader.peek();
|
||||
|
||||
if (peek?.type == type) {
|
||||
await _reader.consume();
|
||||
bool next(TokenType type) {
|
||||
if (peek()?.type == type) {
|
||||
_current = tokens[++_index];
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<bool> nextIs(TokenType type) =>
|
||||
_reader.peek().then((t) => t?.type == type);
|
||||
|
||||
Future<DocumentContext> parseDocument() async {
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<ValueContext> parseValue() async {
|
||||
ValueContext value;
|
||||
|
||||
var string = await parseStringValue();
|
||||
if (string != null)
|
||||
value = string;
|
||||
else {
|
||||
var number = await parseNumberValue();
|
||||
if (number != null)
|
||||
value = number;
|
||||
else {
|
||||
var boolean = await parseBooleanValue();
|
||||
if (boolean != null)
|
||||
value = boolean;
|
||||
else {
|
||||
var array = await parseArrayValue();
|
||||
if (array != null) value = array;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (value != null) _onValue.add(value);
|
||||
return value;
|
||||
}
|
||||
|
||||
Future<StringValueContext> parseStringValue() async {
|
||||
if (await nextIs(TokenType.STRING)) {
|
||||
var result = new StringValueContext(await _reader.consume());
|
||||
_onStringValue.add(result);
|
||||
return result;
|
||||
Token peek() {
|
||||
if (_index < tokens.length - 1) {
|
||||
return tokens[_index + 1];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<NumberValueContext> parseNumberValue() async {
|
||||
if (await nextIs(TokenType.NUMBER)) {
|
||||
var result = new NumberValueContext(await _reader.consume());
|
||||
_onNumberValue.add(result);
|
||||
return result;
|
||||
}
|
||||
DocumentContext parseDocument() {}
|
||||
|
||||
return null;
|
||||
}
|
||||
FragmentDefinitionContext parseFragmentDefinition() {}
|
||||
|
||||
Future<BooleanValueContext> parseBooleanValue() async {
|
||||
if (await nextIs(TokenType.BOOLEAN)) {
|
||||
var result = new BooleanValueContext(await _reader.consume());
|
||||
_onBooleanValue.add(result);
|
||||
return result;
|
||||
}
|
||||
FragmentSpreadContext parseFragmentSpread() {}
|
||||
|
||||
return null;
|
||||
}
|
||||
InlineFragmentContext parseInlineFragment() {}
|
||||
|
||||
Future<ArrayValueContext> parseArrayValue() async {
|
||||
if (await nextIs(TokenType.LBRACKET)) {
|
||||
ArrayValueContext result;
|
||||
var LBRACKET = await _reader.consume();
|
||||
List<ValueContext> values = [];
|
||||
SelectionSetContext parseSelectionSet() {}
|
||||
|
||||
if (await nextIs(TokenType.RBRACKET)) {
|
||||
result = new ArrayValueContext(LBRACKET, await _reader.consume());
|
||||
_onArrayValue.add(result);
|
||||
return result;
|
||||
}
|
||||
SelectionContext parseSelection() {}
|
||||
|
||||
while (!_reader.isDone) {
|
||||
ValueContext value = await parseValue();
|
||||
if (value == null) break;
|
||||
FieldContext parseField() {}
|
||||
|
||||
values.add(value);
|
||||
FieldNameContext parseFieldName() {}
|
||||
|
||||
if (await nextIs(TokenType.COMMA)) {
|
||||
await _reader.consume();
|
||||
continue;
|
||||
} else if (await nextIs(TokenType.RBRACKET)) {
|
||||
result = new ArrayValueContext(LBRACKET, await _reader.consume());
|
||||
_onArrayValue.add(result);
|
||||
return result;
|
||||
}
|
||||
AliasContext parseAlias() {}
|
||||
|
||||
VariableDefinitionsContext parseVariableDefinitions() {}
|
||||
|
||||
VariableDefinitionContext parseVariableDefinition() {}
|
||||
|
||||
List<DirectiveContext> parseDirectives() {}
|
||||
|
||||
DirectiveContext parseDirective() {
|
||||
if (next(TokenType.ARROBA)) {
|
||||
var ARROBA = current;
|
||||
if (next(TokenType.NAME)) {
|
||||
var NAME = current;
|
||||
|
||||
if (next(TokenType.COLON)) {
|
||||
var COLON = current;
|
||||
var val = parseValueOrVariable();
|
||||
if (val != null)
|
||||
return new DirectiveContext(
|
||||
ARROBA, NAME, COLON, null, null, null, val);
|
||||
else
|
||||
throw new SyntaxError.fromSourceLocation(
|
||||
'Expected value or variable in directive after colon.',
|
||||
COLON.span.end);
|
||||
} else if (next(TokenType.LPAREN)) {
|
||||
var LPAREN = current;
|
||||
var arg = parseArgument();
|
||||
if (arg != null) {
|
||||
if (next(TokenType.RPAREN)) {
|
||||
return new DirectiveContext(
|
||||
ARROBA, NAME, null, LPAREN, current, arg, null);
|
||||
} else
|
||||
throw new SyntaxError.fromSourceLocation(
|
||||
'Expected \'(\'', arg.valueOrVariable.span.end);
|
||||
} else
|
||||
throw new SyntaxError.fromSourceLocation(
|
||||
'Expected argument in directive.', LPAREN.span.end);
|
||||
} else
|
||||
return new DirectiveContext(
|
||||
ARROBA, NAME, null, null, null, null, null);
|
||||
} else
|
||||
throw new SyntaxError.fromSourceLocation(
|
||||
'Expected comma or right bracket in array',
|
||||
(await _reader.current())?.span?.start);
|
||||
'Expected name for directive.', ARROBA.span.end);
|
||||
} else
|
||||
return null;
|
||||
}
|
||||
|
||||
ArgumentContext parseArgument() {
|
||||
if (next(TokenType.NAME)) {
|
||||
var NAME = current;
|
||||
if (next(TokenType.COLON)) {
|
||||
var COLON = current;
|
||||
var val = parseValueOrVariable();
|
||||
if (val != null)
|
||||
return new ArgumentContext(NAME, COLON, val);
|
||||
else
|
||||
throw new SyntaxError.fromSourceLocation(
|
||||
'Expected value or variable in argument.', COLON.span.end);
|
||||
} else
|
||||
throw new SyntaxError.fromSourceLocation(
|
||||
'Expected colon after name in argument.', NAME.span.end);
|
||||
} else
|
||||
return null;
|
||||
}
|
||||
|
||||
ValueOrVariableContext parseValueOrVariable() {
|
||||
var value = parseValue();
|
||||
if (value != null)
|
||||
return new ValueOrVariableContext(value, null);
|
||||
else {
|
||||
var variable = parseVariable();
|
||||
if (variable != null)
|
||||
return new ValueOrVariableContext(null, variable);
|
||||
else
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
VariableContext parseVariable() {
|
||||
if (next(TokenType.DOLLAR)) {
|
||||
var DOLLAR = current;
|
||||
if (next(TokenType.NAME))
|
||||
return new VariableContext(DOLLAR, current);
|
||||
else
|
||||
throw new SyntaxError.fromSourceLocation(
|
||||
'Expected name for variable; found a lone "\$" instead.',
|
||||
DOLLAR.span.end);
|
||||
} else
|
||||
return null;
|
||||
}
|
||||
|
||||
DefaultValueContext parseDefaultValue() {}
|
||||
|
||||
TypeConditionContext parseTypeCondition() {}
|
||||
|
||||
ValueContext parseValue() {
|
||||
return parseStringValue() ??
|
||||
parseNumberValue() ??
|
||||
parseBooleanValue() ??
|
||||
parseArrayValue();
|
||||
}
|
||||
|
||||
StringValueContext parseStringValue() =>
|
||||
next(TokenType.STRING) ? new StringValueContext(current) : null;
|
||||
|
||||
NumberValueContext parseNumberValue() =>
|
||||
next(TokenType.NUMBER) ? new NumberValueContext(current) : null;
|
||||
|
||||
BooleanValueContext parseBooleanValue() =>
|
||||
next(TokenType.BOOLEAN) ? new BooleanValueContext(current) : null;
|
||||
|
||||
ArrayValueContext parseArrayValue() {
|
||||
if (next(TokenType.LBRACKET)) {
|
||||
var LBRACKET = current;
|
||||
List<ValueContext> values = [];
|
||||
ValueContext value = parseValue();
|
||||
|
||||
while (value != null) {
|
||||
values.add(value);
|
||||
if (next(TokenType.COMMA)) {
|
||||
value = parseValue();
|
||||
} else
|
||||
break;
|
||||
}
|
||||
|
||||
throw new SyntaxError.fromSourceLocation(
|
||||
'Unterminated array literal.', LBRACKET.span?.start);
|
||||
}
|
||||
|
||||
if (await nextIs(TokenType.BOOLEAN)) {
|
||||
var result = new BooleanValueContext(await _reader.consume());
|
||||
_onBooleanValue.add(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
if (next(TokenType.RBRACKET)) {
|
||||
return new ArrayValueContext(LBRACKET, current)..values.addAll(values);
|
||||
} else
|
||||
throw new SyntaxError.fromSourceLocation(
|
||||
'Unterminated array literal.', LBRACKET.span.end);
|
||||
} else
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,125 +0,0 @@
|
|||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
|
||||
class StreamReader<T> implements StreamConsumer<T> {
|
||||
final Queue<T> _buffer = new Queue();
|
||||
bool _closed = false;
|
||||
T _current;
|
||||
bool _onDataListening = false;
|
||||
final Queue<Completer<T>> _nextQueue = new Queue();
|
||||
final Queue<Completer<T>> _peekQueue = new Queue();
|
||||
|
||||
StreamController<T> _onData;
|
||||
|
||||
bool get isDone => _closed;
|
||||
Stream<T> get onData => _onData.stream;
|
||||
|
||||
_onListen() {
|
||||
_onDataListening = true;
|
||||
}
|
||||
|
||||
_onPause() {
|
||||
_onDataListening = false;
|
||||
}
|
||||
|
||||
StreamReader() {
|
||||
_onData = new StreamController<T>(
|
||||
onListen: _onListen,
|
||||
onResume: _onListen,
|
||||
onPause: _onPause,
|
||||
onCancel: _onPause);
|
||||
}
|
||||
|
||||
Future<T> current() {
|
||||
if (_current == null) {
|
||||
if (_nextQueue.isNotEmpty) return _nextQueue.first.future;
|
||||
return consume();
|
||||
}
|
||||
|
||||
return new Future.value(_current);
|
||||
}
|
||||
|
||||
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> consume() {
|
||||
if (isDone) throw new StateError('Cannot read from closed stream.');
|
||||
|
||||
if (_buffer.isNotEmpty) {
|
||||
_current = _buffer.removeFirst();
|
||||
return close().then((_) => new Future.value(_current));
|
||||
}
|
||||
|
||||
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 (_onDataListening) _onData.add(data);
|
||||
|
||||
if (_peekQueue.isNotEmpty || _nextQueue.isNotEmpty) {
|
||||
if (_peekQueue.isNotEmpty) {
|
||||
_peekQueue.removeFirst().complete(data);
|
||||
}
|
||||
|
||||
if (_nextQueue.isNotEmpty) {
|
||||
_nextQueue.removeFirst().complete(_current = data);
|
||||
}
|
||||
} else {
|
||||
_buffer.add(data);
|
||||
}
|
||||
})
|
||||
..onDone(() => close().then(c.complete))
|
||||
..onError(c.completeError);
|
||||
|
||||
return c.future;
|
||||
}
|
||||
|
||||
@override
|
||||
Future close() async {
|
||||
if (_buffer.isEmpty && _nextQueue.isEmpty && _peekQueue.isEmpty) {
|
||||
_closed = true;
|
||||
|
||||
kill(Completer c) {
|
||||
c.completeError(new StateError(
|
||||
'Reached end of stream, although more input was expected.'));
|
||||
}
|
||||
|
||||
_peekQueue.forEach(kill);
|
||||
_nextQueue.forEach(kill);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
pubspec.yaml
18
pubspec.yaml
|
@ -1,8 +1,12 @@
|
|||
author: "Tobe O"
|
||||
description: "Parses GraphQL queries and schemas."
|
||||
homepage: "https://github.com/thosakwe/graphql_parser"
|
||||
name: "graphql_parser"
|
||||
version: "0.0.0"
|
||||
name: graphql_parser
|
||||
version: 0.0.0
|
||||
description: Parses GraphQL queries and schemas.
|
||||
author: Tobe O <thosakwe@gmail.com>
|
||||
homepage: https://github.com/thosakwe/graphql_parser
|
||||
environment:
|
||||
sdk: ">=1.19.0"
|
||||
dependencies:
|
||||
source_span: "^1.3.1"
|
||||
string_scanner: "^1.0.1"
|
||||
source_span: ^1.0.0
|
||||
string_scanner: ^1.0.0
|
||||
dev_dependencies:
|
||||
test: ^0.12.0
|
||||
|
|
23
test/common.dart
Normal file
23
test/common.dart
Normal file
|
@ -0,0 +1,23 @@
|
|||
import 'package:graphql_parser/graphql_parser.dart';
|
||||
import 'package:matcher/matcher.dart';
|
||||
|
||||
Parser parse(String text) => new Parser(scan(text));
|
||||
|
||||
Matcher equalsParsed(value) => new _EqualsParsed(value);
|
||||
|
||||
class _EqualsParsed extends Matcher {
|
||||
final value;
|
||||
|
||||
_EqualsParsed(this.value);
|
||||
|
||||
@override
|
||||
Description describe(Description description)
|
||||
=> description.add('equals $value when parsed as a GraphQL value');
|
||||
|
||||
@override
|
||||
bool matches(String item, Map matchState) {
|
||||
var p = parse(item);
|
||||
var v = p.parseValue();
|
||||
return equals(value).matches(v.value, matchState);
|
||||
}
|
||||
}
|
41
test/value_test.dart
Normal file
41
test/value_test.dart
Normal file
|
@ -0,0 +1,41 @@
|
|||
import 'dart:math' as math;
|
||||
import 'package:test/test.dart';
|
||||
import 'common.dart';
|
||||
|
||||
main() {
|
||||
test('boolean', () {
|
||||
expect('true', equalsParsed(true));
|
||||
expect('false', equalsParsed(false));
|
||||
});
|
||||
|
||||
test('number', () {
|
||||
expect('1', equalsParsed(1));
|
||||
expect('1.0', equalsParsed(1.0));
|
||||
expect('-1', equalsParsed(-1));
|
||||
expect('-1.0', equalsParsed(-1.0));
|
||||
expect('6.26e-34', equalsParsed(6.26 * math.pow(10, -34)));
|
||||
expect('-6.26e-34', equalsParsed(-6.26 * math.pow(10, -34)));
|
||||
expect('-6.26e34', equalsParsed(-6.26 * math.pow(10, 34)));
|
||||
});
|
||||
|
||||
test('array', () {
|
||||
expect('[]', equalsParsed([]));
|
||||
expect('[1,2]', equalsParsed([1,2]));
|
||||
expect('[1,2, 3]', equalsParsed([1,2,3]));
|
||||
expect('["a"]', equalsParsed(['a']));
|
||||
});
|
||||
|
||||
test('string', () {
|
||||
expect('""', equalsParsed(''));
|
||||
expect('"a"', equalsParsed('a'));
|
||||
expect('"abc"', equalsParsed('abc'));
|
||||
expect('"\\""', equalsParsed('"'));
|
||||
expect('"\\b"', equalsParsed('\b'));
|
||||
expect('"\\f"', equalsParsed('\f'));
|
||||
expect('"\\n"', equalsParsed('\n'));
|
||||
expect('"\\r"', equalsParsed('\r'));
|
||||
expect('"\\t"', equalsParsed('\t'));
|
||||
expect('"\\u0123"', equalsParsed('\u0123'));
|
||||
expect('"\\u0123\\u4567"', equalsParsed('\u0123\u4567'));
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue