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
|
# Don't commit pubspec lock file
|
||||||
# (Library packages only! Remove pattern if developing an application package)
|
# (Library packages only! Remove pattern if developing an application package)
|
||||||
pubspec.lock
|
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
|
# 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.
|
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 'dart:async';
|
||||||
import 'package:graphql_parser/src/language/language.dart';
|
import 'package:graphql_parser/src/language/language.dart';
|
||||||
|
|
||||||
Stream<String> input() async* {
|
final String INPUT = '''
|
||||||
yield '''
|
|
||||||
{
|
{
|
||||||
project(name: "GraphQL") {
|
project(name: "GraphQL") {
|
||||||
tagline
|
tagline
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'''
|
'''.trim();
|
||||||
.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
var lexer = new Lexer(), parser = new Parser();
|
var tokens = scan(INPUT);
|
||||||
var stream = input().transform(lexer).asBroadcastStream();
|
var parser = new Parser(tokens);
|
||||||
stream
|
|
||||||
..forEach(print)
|
|
||||||
..pipe(parser);
|
|
||||||
parser.onNode.forEach(print);
|
|
||||||
}
|
}
|
||||||
|
|
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 =>
|
SourceSpan get span =>
|
||||||
new SourceSpan(LBRACKET.span?.end, RBRACKET.span?.end, toSource());
|
new SourceSpan(LBRACKET.span?.end, RBRACKET.span?.end, toSource());
|
||||||
|
|
||||||
|
@override
|
||||||
|
List get value => values.map((v) => v.value).toList();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toSource() {
|
String toSource() {
|
||||||
var buf = new StringBuffer('[');
|
var buf = new StringBuffer('[');
|
||||||
|
|
|
@ -3,13 +3,17 @@ import 'package:source_span/src/span.dart';
|
||||||
import 'value.dart';
|
import 'value.dart';
|
||||||
|
|
||||||
class BooleanValueContext extends ValueContext {
|
class BooleanValueContext extends ValueContext {
|
||||||
|
bool _valueCache;
|
||||||
final Token BOOLEAN;
|
final Token BOOLEAN;
|
||||||
|
|
||||||
BooleanValueContext(this.BOOLEAN) {
|
BooleanValueContext(this.BOOLEAN) {
|
||||||
assert(BOOLEAN?.text == 'true' || BOOLEAN?.text == 'false');
|
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
|
@override
|
||||||
SourceSpan get span => BOOLEAN.span;
|
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 '../token.dart';
|
||||||
import 'package:source_span/src/span.dart';
|
|
||||||
import 'value.dart';
|
import 'value.dart';
|
||||||
|
|
||||||
class NumberValueContext extends ValueContext {
|
class NumberValueContext extends ValueContext {
|
||||||
|
@ -7,6 +8,21 @@ class NumberValueContext extends ValueContext {
|
||||||
|
|
||||||
NumberValueContext(this.NUMBER);
|
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
|
@override
|
||||||
SourceSpan get span => NUMBER.span;
|
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 '../token.dart';
|
||||||
import 'package:source_span/src/span.dart';
|
|
||||||
import 'value.dart';
|
import 'value.dart';
|
||||||
|
|
||||||
class StringValueContext extends ValueContext {
|
class StringValueContext extends ValueContext {
|
||||||
|
@ -10,7 +13,61 @@ class StringValueContext extends ValueContext {
|
||||||
@override
|
@override
|
||||||
SourceSpan get span => STRING.span;
|
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
|
@override
|
||||||
String toSource() => STRING.text;
|
String toSource() => STRING.text;
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
import 'node.dart';
|
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 'lexer.dart';
|
||||||
export 'parser.dart';
|
export 'parser.dart';
|
||||||
|
export 'syntax_error.dart';
|
||||||
export 'token.dart';
|
export 'token.dart';
|
||||||
export 'token_type.dart';
|
export 'token_type.dart';
|
|
@ -1,4 +1,3 @@
|
||||||
import 'dart:async';
|
|
||||||
import 'package:string_scanner/string_scanner.dart';
|
import 'package:string_scanner/string_scanner.dart';
|
||||||
import 'syntax_error.dart';
|
import 'syntax_error.dart';
|
||||||
import 'token.dart';
|
import 'token.dart';
|
||||||
|
@ -35,43 +34,34 @@ final Map<Pattern, TokenType> _patterns = {
|
||||||
_name: TokenType.NAME
|
_name: TokenType.NAME
|
||||||
};
|
};
|
||||||
|
|
||||||
class Lexer implements StreamTransformer<String, Token> {
|
List<Token> scan(String text) {
|
||||||
@override
|
List<Token> out = [];
|
||||||
Stream<Token> bind(Stream<String> stream) {
|
var scanner = new SpanScanner(text);
|
||||||
var ctrl = new StreamController<Token>();
|
|
||||||
|
|
||||||
stream.listen((str) {
|
while (!scanner.isDone) {
|
||||||
var scanner = new SpanScanner(str);
|
List<Token> potential = [];
|
||||||
|
|
||||||
while (!scanner.isDone) {
|
if (scanner.scan(_whitespace)) continue;
|
||||||
List<Token> potential = [];
|
|
||||||
|
|
||||||
if (scanner.scan(_whitespace)) continue;
|
for (var pattern in _patterns.keys) {
|
||||||
|
if (scanner.matches(pattern)) {
|
||||||
for (var pattern in _patterns.keys) {
|
potential.add(new Token(_patterns[pattern], scanner.lastMatch[0], scanner.lastSpan));
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
..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;
|
library graphql_parser.language.parser;
|
||||||
|
|
||||||
import 'dart:async';
|
|
||||||
import 'ast/ast.dart';
|
import 'ast/ast.dart';
|
||||||
import 'stream_reader.dart';
|
|
||||||
import 'syntax_error.dart';
|
import 'syntax_error.dart';
|
||||||
import 'token.dart';
|
import 'token.dart';
|
||||||
import 'token_type.dart';
|
import 'token_type.dart';
|
||||||
part 'base_parser.dart';
|
|
||||||
|
|
||||||
class Parser extends BaseParser {
|
class Parser {
|
||||||
bool _closed = false;
|
Token _current;
|
||||||
final Completer _closer = new Completer();
|
|
||||||
final List<SyntaxError> _errors = [];
|
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);
|
List<SyntaxError> get errors => new List<SyntaxError>.unmodifiable(_errors);
|
||||||
|
|
||||||
Future _waterfall(List<Function> futures) async {
|
bool next(TokenType type) {
|
||||||
for (var f in futures) {
|
if (peek()?.type == type) {
|
||||||
var r = await f();
|
_current = tokens[++_index];
|
||||||
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();
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> nextIs(TokenType type) =>
|
Token peek() {
|
||||||
_reader.peek().then((t) => t?.type == type);
|
if (_index < tokens.length - 1) {
|
||||||
|
return tokens[_index + 1];
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<NumberValueContext> parseNumberValue() async {
|
DocumentContext parseDocument() {}
|
||||||
if (await nextIs(TokenType.NUMBER)) {
|
|
||||||
var result = new NumberValueContext(await _reader.consume());
|
|
||||||
_onNumberValue.add(result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
FragmentDefinitionContext parseFragmentDefinition() {}
|
||||||
}
|
|
||||||
|
|
||||||
Future<BooleanValueContext> parseBooleanValue() async {
|
FragmentSpreadContext parseFragmentSpread() {}
|
||||||
if (await nextIs(TokenType.BOOLEAN)) {
|
|
||||||
var result = new BooleanValueContext(await _reader.consume());
|
|
||||||
_onBooleanValue.add(result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
InlineFragmentContext parseInlineFragment() {}
|
||||||
}
|
|
||||||
|
|
||||||
Future<ArrayValueContext> parseArrayValue() async {
|
SelectionSetContext parseSelectionSet() {}
|
||||||
if (await nextIs(TokenType.LBRACKET)) {
|
|
||||||
ArrayValueContext result;
|
|
||||||
var LBRACKET = await _reader.consume();
|
|
||||||
List<ValueContext> values = [];
|
|
||||||
|
|
||||||
if (await nextIs(TokenType.RBRACKET)) {
|
SelectionContext parseSelection() {}
|
||||||
result = new ArrayValueContext(LBRACKET, await _reader.consume());
|
|
||||||
_onArrayValue.add(result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (!_reader.isDone) {
|
FieldContext parseField() {}
|
||||||
ValueContext value = await parseValue();
|
|
||||||
if (value == null) break;
|
|
||||||
|
|
||||||
values.add(value);
|
FieldNameContext parseFieldName() {}
|
||||||
|
|
||||||
if (await nextIs(TokenType.COMMA)) {
|
AliasContext parseAlias() {}
|
||||||
await _reader.consume();
|
|
||||||
continue;
|
|
||||||
} else if (await nextIs(TokenType.RBRACKET)) {
|
|
||||||
result = new ArrayValueContext(LBRACKET, await _reader.consume());
|
|
||||||
_onArrayValue.add(result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
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(
|
throw new SyntaxError.fromSourceLocation(
|
||||||
'Expected comma or right bracket in array',
|
'Expected name for directive.', ARROBA.span.end);
|
||||||
(await _reader.current())?.span?.start);
|
} 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(
|
if (next(TokenType.RBRACKET)) {
|
||||||
'Unterminated array literal.', LBRACKET.span?.start);
|
return new ArrayValueContext(LBRACKET, current)..values.addAll(values);
|
||||||
}
|
} else
|
||||||
|
throw new SyntaxError.fromSourceLocation(
|
||||||
if (await nextIs(TokenType.BOOLEAN)) {
|
'Unterminated array literal.', LBRACKET.span.end);
|
||||||
var result = new BooleanValueContext(await _reader.consume());
|
} else
|
||||||
_onBooleanValue.add(result);
|
return null;
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
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"
|
name: graphql_parser
|
||||||
description: "Parses GraphQL queries and schemas."
|
version: 0.0.0
|
||||||
homepage: "https://github.com/thosakwe/graphql_parser"
|
description: Parses GraphQL queries and schemas.
|
||||||
name: "graphql_parser"
|
author: Tobe O <thosakwe@gmail.com>
|
||||||
version: "0.0.0"
|
homepage: https://github.com/thosakwe/graphql_parser
|
||||||
|
environment:
|
||||||
|
sdk: ">=1.19.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
source_span: "^1.3.1"
|
source_span: ^1.0.0
|
||||||
string_scanner: "^1.0.1"
|
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