diff --git a/.gitignore b/.gitignore index 427e911b..e822a056 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..de2210c9 --- /dev/null +++ b/.travis.yml @@ -0,0 +1 @@ +language: dart \ No newline at end of file diff --git a/README.md b/README.md index ebe20bb9..c69e0227 100644 --- a/README.md +++ b/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... +} +``` diff --git a/example/basic.dart b/example/basic.dart index d18507ec..661c7617 100644 --- a/example/basic.dart +++ b/example/basic.dart @@ -1,22 +1,15 @@ import 'dart:async'; import 'package:graphql_parser/src/language/language.dart'; -Stream 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); } diff --git a/lib/graphql_parser.dart b/lib/graphql_parser.dart new file mode 100644 index 00000000..a03df794 --- /dev/null +++ b/lib/graphql_parser.dart @@ -0,0 +1,2 @@ +export 'src/language/ast/ast.dart'; +export 'src/language/language.dart'; \ No newline at end of file diff --git a/lib/src/language/ast/array_value.dart b/lib/src/language/ast/array_value.dart index 265e2737..4cacda1a 100644 --- a/lib/src/language/ast/array_value.dart +++ b/lib/src/language/ast/array_value.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('['); diff --git a/lib/src/language/ast/boolean_value.dart b/lib/src/language/ast/boolean_value.dart index 3126b176..7f74ac08 100644 --- a/lib/src/language/ast/boolean_value.dart +++ b/lib/src/language/ast/boolean_value.dart @@ -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; diff --git a/lib/src/language/ast/number_value.dart b/lib/src/language/ast/number_value.dart index cc981c96..7d6db952 100644 --- a/lib/src/language/ast/number_value.dart +++ b/lib/src/language/ast/number_value.dart @@ -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; diff --git a/lib/src/language/ast/string_value.dart b/lib/src/language/ast/string_value.dart index 65809215..97ba0435 100644 --- a/lib/src/language/ast/string_value.dart +++ b/lib/src/language/ast/string_value.dart @@ -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; diff --git a/lib/src/language/ast/value.dart b/lib/src/language/ast/value.dart index 87b34bf7..e35e425d 100644 --- a/lib/src/language/ast/value.dart +++ b/lib/src/language/ast/value.dart @@ -1,3 +1,5 @@ import 'node.dart'; -abstract class ValueContext extends Node {} +abstract class ValueContext extends Node { + get value; +} diff --git a/lib/src/language/base_parser.dart b/lib/src/language/base_parser.dart deleted file mode 100644 index b0c2fd4c..00000000 --- a/lib/src/language/base_parser.dart +++ /dev/null @@ -1,25 +0,0 @@ -part of graphql_parser.language.parser; - -abstract class BaseParser implements StreamConsumer { - final StreamController _onArrayValue = - new StreamController(); - final StreamController _onBooleanValue = - new StreamController(); - final StreamController _onDocument = - new StreamController(); - final StreamController _onNode = new StreamController(); - final StreamController _onNumberValue = - new StreamController(); - final StreamController _onStringValue = - new StreamController(); - final StreamController _onValue = - new StreamController(); - - Stream get onArrayValue => _onArrayValue.stream; - Stream get onBooleanValue => _onBooleanValue.stream; - Stream get onDocument => _onDocument.stream; - Stream get onNode => _onNode.stream; - Stream get onNumberValue => _onNumberValue.stream; - Stream get onStringValue => _onStringValue.stream; - Stream get onValue => _onValue.stream; -} diff --git a/lib/src/language/language.dart b/lib/src/language/language.dart index bdf7665e..95db2ae3 100644 --- a/lib/src/language/language.dart +++ b/lib/src/language/language.dart @@ -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'; \ No newline at end of file diff --git a/lib/src/language/lexer.dart b/lib/src/language/lexer.dart index 44c3c44f..36395398 100644 --- a/lib/src/language/lexer.dart +++ b/lib/src/language/lexer.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 _patterns = { _name: TokenType.NAME }; -class Lexer implements StreamTransformer { - @override - Stream bind(Stream stream) { - var ctrl = new StreamController(); +List scan(String text) { + List out = []; + var scanner = new SpanScanner(text); - stream.listen((str) { - var scanner = new SpanScanner(str); + while (!scanner.isDone) { + List potential = []; - while (!scanner.isDone) { - List 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; } diff --git a/lib/src/language/parser.dart b/lib/src/language/parser.dart index e8011cb6..3d41b077 100644 --- a/lib/src/language/parser.dart +++ b/lib/src/language/parser.dart @@ -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 _errors = []; - final StreamReader _reader = new StreamReader(); + int _index = -1; + + final List tokens; + + Parser(this.tokens); + + Token get current => _current; List get errors => new List.unmodifiable(_errors); - Future _waterfall(List futures) async { - for (var f in futures) { - var r = await f(); - if (r != null) return r; - } - } - - @override - Future addStream(Stream 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 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 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 nextIs(TokenType type) => - _reader.peek().then((t) => t?.type == type); - - Future parseDocument() async { - return null; - } - - Future 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 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 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 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 parseArrayValue() async { - if (await nextIs(TokenType.LBRACKET)) { - ArrayValueContext result; - var LBRACKET = await _reader.consume(); - List 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 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 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; } } diff --git a/lib/src/language/stream_reader.dart b/lib/src/language/stream_reader.dart deleted file mode 100644 index ea7e75df..00000000 --- a/lib/src/language/stream_reader.dart +++ /dev/null @@ -1,125 +0,0 @@ -import 'dart:async'; -import 'dart:collection'; - -class StreamReader implements StreamConsumer { - final Queue _buffer = new Queue(); - bool _closed = false; - T _current; - bool _onDataListening = false; - final Queue> _nextQueue = new Queue(); - final Queue> _peekQueue = new Queue(); - - StreamController _onData; - - bool get isDone => _closed; - Stream get onData => _onData.stream; - - _onListen() { - _onDataListening = true; - } - - _onPause() { - _onDataListening = false; - } - - StreamReader() { - _onData = new StreamController( - onListen: _onListen, - onResume: _onListen, - onPause: _onPause, - onCancel: _onPause); - } - - Future current() { - if (_current == null) { - if (_nextQueue.isNotEmpty) return _nextQueue.first.future; - return consume(); - } - - return new Future.value(_current); - } - - Future peek() { - if (isDone) throw new StateError('Cannot read from closed stream.'); - if (_buffer.isNotEmpty) return new Future.value(_buffer.first); - - var c = new Completer(); - _peekQueue.addLast(c); - return c.future; - } - - Future 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(); - _nextQueue.addLast(c); - return c.future; - } - - @override - Future addStream(Stream 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 { - final Iterator _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; -} diff --git a/pubspec.yaml b/pubspec.yaml index b6356421..b25bcb5d 100644 --- a/pubspec.yaml +++ b/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 +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 diff --git a/test/common.dart b/test/common.dart new file mode 100644 index 00000000..a08dc700 --- /dev/null +++ b/test/common.dart @@ -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); + } +} \ No newline at end of file diff --git a/test/value_test.dart b/test/value_test.dart new file mode 100644 index 00000000..19e1c504 --- /dev/null +++ b/test/value_test.dart @@ -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')); + }); +}