Almost done with fields...

This commit is contained in:
thosakwe 2017-07-04 11:58:22 -04:00
parent 9e942d8221
commit 3be644b96f
13 changed files with 541 additions and 54 deletions

View file

@ -8,6 +8,7 @@
<excludeFolder url="file://$MODULE_DIR$/example/packages" />
<excludeFolder url="file://$MODULE_DIR$/packages" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/test/packages" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />

View file

@ -7,6 +7,12 @@ class AliasContext extends Node {
AliasContext(this.NAME1, this.COLON, this.NAME2);
/// The actual name of the value.
String get name => NAME1.text;
/// The aliased name of the value.
String get alias => NAME2.text;
@override
SourceSpan get span =>
new SourceSpan(NAME1.span?.start, NAME2.span?.end, toSource());

View file

@ -15,21 +15,18 @@ class FieldContext extends Node {
@override
SourceSpan get span {
SourceLocation end = fieldName.end;
if (selectionSet != null)
end = selectionSet.end;
return fieldName.span.union(selectionSet.span);
else if (directives.isNotEmpty)
end = directives.last.end;
else if (arguments.isNotEmpty) end = arguments.last.end;
return new SourceSpan(fieldName.start, end, toSource());
return directives.fold<SourceSpan>(
fieldName.span, (out, d) => out.union(d.span));
if (arguments.isNotEmpty)
return arguments.fold<SourceSpan>(
fieldName.span, (out, a) => out.union(a.span));
else
return fieldName.span;
}
@override
String toSource() =>
fieldName.toSource() +
arguments.map((a) => a.toSource()).join() +
directives.map((d) => d.toSource()).join() +
(selectionSet?.toSource() ?? '');
String toSource() => span.text;
}

View file

@ -11,6 +11,8 @@ class FieldNameContext extends Node {
assert(NAME != null || alias != null);
}
String get name => NAME?.text;
@override
SourceSpan get span => alias?.span ?? NAME.span;

View file

@ -9,7 +9,7 @@ class TypeContext extends Node {
final TypeNameContext typeName;
final ListTypeContext listType;
bool get nonNullType => EXCLAMATION != null;
bool get isNullable => EXCLAMATION == null;
TypeContext(this.typeName, this.listType, [this.EXCLAMATION]) {
assert(typeName != null || listType != null);
@ -42,7 +42,7 @@ class TypeContext extends Node {
buf.write(listType.toSource());
}
if (nonNullType) buf.write('!');
if (!isNullable) buf.write('!');
return buf.toString();
}

View file

@ -5,6 +5,8 @@ import '../token.dart';
class TypeNameContext extends Node {
final Token NAME;
String get name => NAME.text;
@override
SourceSpan get span => NAME.span;

View file

@ -10,8 +10,8 @@ class VariableContext extends Node {
String get name => NAME.text;
@override
SourceSpan get span =>
new SourceSpan(DOLLAR?.span?.start, NAME?.span?.end, toSource());
SourceSpan get span => DOLLAR.span.union(NAME.span);
// new SourceSpan(DOLLAR?.span?.start, NAME?.span?.end, toSource());
@override
String toSource() => '\$${NAME.text}';

View file

@ -32,6 +32,8 @@ class Parser {
return null;
}
Token maybe(TokenType type) => next(type) ? current : null;
DocumentContext parseDocument() {}
FragmentDefinitionContext parseFragmentDefinition() {}
@ -44,17 +46,98 @@ class Parser {
SelectionContext parseSelection() {}
FieldContext parseField() {}
FieldContext parseField() {
var fieldName = parseFieldName();
if (fieldName != null) {
var args = parseArguments();
var directives = parseDirectives();
var selectionSet = parseSelectionSet();
return new FieldContext(fieldName, selectionSet)
..arguments.addAll(args)
..directives.addAll(directives);
} else
return null;
}
FieldNameContext parseFieldName() {}
AliasContext parseAlias() {}
FieldNameContext parseFieldName() {
if (next(TokenType.NAME)) {
var NAME1 = current;
if (next(TokenType.COLON)) {
var COLON = current;
if (next(TokenType.NAME))
return new FieldNameContext(
null, new AliasContext(NAME1, COLON, current));
else
throw new SyntaxError.fromSourceLocation(
'Expected name after colon in alias.', COLON.span.end);
} else
return new FieldNameContext(NAME1);
} else
return null;
}
VariableDefinitionsContext parseVariableDefinitions() {}
VariableDefinitionContext parseVariableDefinition() {}
VariableDefinitionContext parseVariableDefinition() {
var variable = parseVariable();
if (variable != null) {
if (next(TokenType.COLON)) {
var COLON = current;
var type = parseType();
if (type != null) {
var defaultValue = parseDefaultValue();
return new VariableDefinitionContext(
variable, COLON, type, defaultValue);
} else
throw new SyntaxError.fromSourceLocation(
'Expected type in variable definition.', COLON.span.end);
} else
throw new SyntaxError.fromSourceLocation(
'Expected ":" in variable definition.', variable.span.end);
} else
return null;
}
List<DirectiveContext> parseDirectives() {}
TypeContext parseType() {
var name = parseTypeName();
if (name != null) {
return new TypeContext(name, null, maybe(TokenType.EXCLAMATION));
} else {
var listType = parseListType();
if (listType != null) {
return new TypeContext(null, listType, maybe(TokenType.EXCLAMATION));
} else
return null;
}
}
ListTypeContext parseListType() {
if (next(TokenType.LBRACKET)) {
var LBRACKET = current;
var type = parseType();
if (type != null) {
if (next(TokenType.RBRACKET)) {
return new ListTypeContext(LBRACKET, type, current);
} else
throw new SyntaxError.fromSourceLocation(
'Expected "]" in list type.', type.span.end);
} else
throw new SyntaxError.fromSourceLocation(
'Expected type after "[".', LBRACKET.span.end);
} else
return null;
}
List<DirectiveContext> parseDirectives() {
List<DirectiveContext> out = [];
DirectiveContext d = parseDirective();
while (d != null) {
out.add(d);
d = parseDirective();
}
return out;
}
DirectiveContext parseDirective() {
if (next(TokenType.ARROBA)) {
@ -95,6 +178,30 @@ class Parser {
return null;
}
List<ArgumentContext> parseArguments() {
if (next(TokenType.LPAREN)) {
var LPAREN = current;
List<ArgumentContext> out = [];
ArgumentContext arg = parseArgument();
while (arg != null) {
out.add(arg);
if (next(TokenType.COMMA))
arg = parseArgument();
else
break;
}
if (next(TokenType.RPAREN))
return out;
else
throw new SyntaxError.fromSourceLocation(
'Expected ")" in argument list.',
out.isEmpty ? LPAREN.span.end : out.last.span.end);
} else
return [];
}
ArgumentContext parseArgument() {
if (next(TokenType.NAME)) {
var NAME = current;
@ -139,9 +246,33 @@ class Parser {
return null;
}
DefaultValueContext parseDefaultValue() {}
DefaultValueContext parseDefaultValue() {
if (next(TokenType.EQUALS)) {
var EQUALS = current;
var value = parseValue();
if (value != null) {
return new DefaultValueContext(EQUALS, value);
} else
throw new SyntaxError.fromSourceLocation(
'Expected value after "=" sign.', EQUALS.span.end);
} else
return null;
}
TypeConditionContext parseTypeCondition() {}
TypeConditionContext parseTypeCondition() {
var name = parseTypeName();
if (name != null)
return new TypeConditionContext(name);
else
return null;
}
TypeNameContext parseTypeName() {
if (next(TokenType.NAME)) {
return new TypeNameContext(current);
} else
return null;
}
ValueContext parseValue() {
return parseStringValue() ??

View file

@ -1,12 +1,18 @@
import 'package:test/test.dart';
import 'argument_test.dart' as argument;
import 'directive_test.dart' as directive;
import 'field_test.dart' as field;
import 'type_test.dart' as type;
import 'value_test.dart' as value;
import 'variable_definition_test.dart' as variable_definition;
import 'variable_test.dart' as variable;
main() {
group('argument', argument.main);
group('directive', directive.main);
group('field', field.main);
group('type', type.main);
group('value', value.main);
group('variable', variable.main);
group('variable definition', variable_definition.main);
}

173
test/field_test.dart Normal file
View file

@ -0,0 +1,173 @@
import 'package:graphql_parser/graphql_parser.dart';
import 'package:test/test.dart';
import 'common.dart';
import 'argument_test.dart';
import 'directive_test.dart';
import 'value_test.dart';
main() {
group('field name', () {
test('plain field name', () {
expect('foo', isFieldName('foo'));
});
test('alias', () {
expect('foo: bar', isFieldName('foo', alias: 'bar'));
});
test('exceptions', () {
expect(() => parseFieldName('foo:'), throwsSyntaxError);
});
});
test('arguments', () {
expect('()', isArgumentList([]));
expect(r'(a: 2)', isArgumentList([isArgument('a', 2)]));
expect(r'(a: 2, b: $c)',
isArgumentList([isArgument('a', 2), isArgument('b', 'c')]));
});
group('field tests', () {
test('plain field name', () {
expect('foo', isField(fieldName: isFieldName('foo')));
});
test('aliased field name', () {
expect('foo: bar', isField(fieldName: isFieldName('foo', alias: 'bar')));
});
test('with arguments', () {
expect(
r'foo (a: 2, b: $c)',
isField(
fieldName: isFieldName('foo'),
arguments:
isArgumentList([isArgument('a', 2), isArgument('b', 'c')])));
});
test('with directives', () {
expect(
'foo: bar @bar @baz: 2 @quux (one: 1)',
isField(
fieldName: isFieldName('foo', alias: 'bar'),
directives: isDirectiveList([
isDirective('bar'),
isDirective('baz', valueOrVariable: isValue(2)),
isDirective('quux', argument: isArgument('one', 1))
])));
});
});
}
FieldContext parseField(String text) => parse(text).parseField();
FieldNameContext parseFieldName(String text) => parse(text).parseFieldName();
Matcher isArgumentList(List<Matcher> arguments) =>
new _IsArgumentList(arguments);
Matcher isDirectiveList(List<Matcher> directives) =>
new _IsDirectiveList(directives);
Matcher isField(
{Matcher fieldName,
Matcher arguments,
Matcher directives,
Matcher selectionSet}) =>
new _IsField(fieldName, arguments, directives, selectionSet);
Matcher isFieldName(String name, {String alias}) =>
new _IsFieldName(name, alias);
class _IsField extends Matcher {
final Matcher fieldName, arguments, directives, selectionSet;
_IsField(this.fieldName, this.arguments, this.directives, this.selectionSet);
@override
Description describe(Description description) {
// Too lazy to make a real description...
return description.add('is field');
}
@override
bool matches(item, Map matchState) {
var field = item is FieldContext ? item : parseField(item);
if (field == null) return false;
if (fieldName != null && !fieldName.matches(field.fieldName, matchState))
return false;
if (arguments != null && !arguments.matches(field.arguments, matchState))
return false;
return true;
}
}
class _IsFieldName extends Matcher {
final String name, alias;
_IsFieldName(this.name, this.alias);
@override
Description describe(Description description) {
if (alias != null)
return description.add('is field with name "$name" and alias "$alias"');
return description.add('is field with name "$name"');
}
@override
bool matches(item, Map matchState) {
var fieldName = item is FieldNameContext ? item : parseFieldName(item);
if (alias != null)
return fieldName.alias?.name == name && fieldName.alias?.alias == alias;
else
return fieldName.name == name;
}
}
class _IsArgumentList extends Matcher {
final List<Matcher> arguments;
_IsArgumentList(this.arguments);
@override
Description describe(Description description) {
return description.add('is list of ${arguments.length} argument(s)');
}
@override
bool matches(item, Map matchState) {
var args =
item is List<ArgumentContext> ? item : parse(item).parseArguments();
if (args.length != arguments.length) return false;
for (int i = 0; i < args.length; i++) {
if (!arguments[i].matches(args[i], matchState)) return false;
}
return true;
}
}
class _IsDirectiveList extends Matcher {
final List<Matcher> directives;
_IsDirectiveList(this.directives);
@override
Description describe(Description description) {
return description.add('is list of ${directives.length} directive(s)');
}
@override
bool matches(item, Map matchState) {
var args =
item is List<DirectiveContext> ? item : parse(item).parseDirectives();
if (args.length != directives.length) return false;
for (int i = 0; i < args.length; i++) {
if (!directives[i].matches(args[i], matchState)) return false;
}
return true;
}
}

95
test/type_test.dart Normal file
View file

@ -0,0 +1,95 @@
import 'package:graphql_parser/graphql_parser.dart';
import 'package:test/test.dart';
import 'common.dart';
main() {
test('nullable', () {
expect('foo', isType('foo', isNullable: true));
});
test('non-nullable', () {
expect('foo!', isType('foo', isNullable: false));
});
group('list type', () {
group('nullable list type', () {
test('with nullable', () {
expect('[foo]', isListType(isType('foo', isNullable: true)));
});
test('with non-nullable', () {
expect('[foo!]', isListType(isType('foo', isNullable: false)));
});
});
group('non-nullable list type', () {
test('with nullable', () {
expect('[foo]!',
isListType(isType('foo', isNullable: true), isNullable: false));
});
test('with non-nullable', () {
expect('[foo!]!',
isListType(isType('foo', isNullable: false), isNullable: false));
});
});
test('exceptions', () {
expect(() => parseType('[foo'), throwsSyntaxError);
expect(() => parseType('['), throwsSyntaxError);
});
});
}
TypeContext parseType(String text) => parse(text).parseType();
Matcher isListType(Matcher innerType, {bool isNullable}) =>
new _IsListType(innerType, isNullable: isNullable != false);
Matcher isType(String name, {bool isNullable}) =>
new _IsType(name, nonNull: isNullable != true);
class _IsListType extends Matcher {
final Matcher innerType;
final bool isNullable;
_IsListType(this.innerType, {this.isNullable});
@override
Description describe(Description description) {
var tok = isNullable != false ? 'nullable' : 'non-nullable';
var desc = description.add('is $tok list type with an inner type that ');
return innerType.describe(desc);
}
@override
bool matches(item, Map matchState) {
var type = item is TypeContext ? item : parseType(item);
if (type.listType == null) return false;
if (type.isNullable != (isNullable != false)) return false;
return innerType.matches(type.listType.type, matchState);
}
}
class _IsType extends Matcher {
final String name;
final bool nonNull;
_IsType(this.name, {this.nonNull});
@override
Description describe(Description description) {
if (nonNull == true)
return description.add('is non-null type named "$name"');
else
return description.add('is nullable type named "$name"');
}
@override
bool matches(item, Map matchState) {
var type = item is TypeContext ? item : parseType(item);
if (type.typeName == null) return false;
var result = type.typeName.name == name;
return result && type.isNullable == !(nonNull == true);
}
}

View file

@ -5,39 +5,39 @@ import 'common.dart';
main() {
test('boolean', () {
expect('true', equalsParsed(true));
expect('false', equalsParsed(false));
expect('true', isValue(true));
expect('false', isValue(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)));
expect('1', isValue(1));
expect('1.0', isValue(1.0));
expect('-1', isValue(-1));
expect('-1.0', isValue(-1.0));
expect('6.26e-34', isValue(6.26 * math.pow(10, -34)));
expect('-6.26e-34', isValue(-6.26 * math.pow(10, -34)));
expect('-6.26e34', isValue(-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']));
expect('[]', isValue([]));
expect('[1,2]', isValue([1, 2]));
expect('[1,2, 3]', isValue([1, 2, 3]));
expect('["a"]', isValue(['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'));
expect('""', isValue(''));
expect('"a"', isValue('a'));
expect('"abc"', isValue('abc'));
expect('"\\""', isValue('"'));
expect('"\\b"', isValue('\b'));
expect('"\\f"', isValue('\f'));
expect('"\\n"', isValue('\n'));
expect('"\\r"', isValue('\r'));
expect('"\\t"', isValue('\t'));
expect('"\\u0123"', isValue('\u0123'));
expect('"\\u0123\\u4567"', isValue('\u0123\u4567'));
});
test('exceptions', () {
@ -46,21 +46,20 @@ main() {
}
ValueContext parseValue(String text) => parse(text).parseValue();
Matcher equalsParsed(value) => new _EqualsParsed(value);
Matcher isValue(value) => new _IsValue(value);
class _EqualsParsed extends Matcher {
class _IsValue extends Matcher {
final value;
_EqualsParsed(this.value);
_IsValue(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();
bool matches(item, Map matchState) {
var v = item is ValueContext ? item : parseValue(item);
return equals(value).matches(v.value, matchState);
}
}

View file

@ -0,0 +1,75 @@
import 'package:graphql_parser/graphql_parser.dart';
import 'package:test/test.dart';
import 'common.dart';
import 'type_test.dart';
import 'value_test.dart';
main() {
test('no default value', () {
expect(r'$foo: bar',
isVariableDefinition('foo', type: isType('bar', isNullable: true)));
});
test('default value', () {
expect(
r'$foo: int! = 2',
isVariableDefinition('foo',
type: isType('int', isNullable: false), defaultValue: isValue(2)));
});
test('exceptions', () {
expect(() => parseVariableDefinition(r'$foo'), throwsSyntaxError);
expect(() => parseVariableDefinition(r'$foo:'), throwsSyntaxError);
expect(() => parseVariableDefinition(r'$foo: int ='), throwsSyntaxError);
});
}
VariableDefinitionContext parseVariableDefinition(String text) =>
parse(text).parseVariableDefinition();
Matcher isVariableDefinition(String name,
{Matcher type, Matcher defaultValue}) =>
new _IsVariableDefinition(name, type, defaultValue);
class _IsVariableDefinition extends Matcher {
final String name;
final Matcher type, defaultValue;
_IsVariableDefinition(this.name, this.type, this.defaultValue);
@override
Description describe(Description description) {
var desc = description.add('is variable definition with name "$name"');
if (type != null) {
desc = type.describe(desc.add(' with type that '));
}
if (defaultValue != null) {
desc = type.describe(desc.add(' with default value that '));
}
return desc;
}
@override
bool matches(item, Map matchState) {
var def = item is VariableDefinitionContext
? item
: parseVariableDefinition(item);
if (def == null) return false;
if (def.variable.name != name) return false;
bool result = true;
if (type != null) {
result == result && type.matches(def.type, matchState);
}
if (defaultValue != null) {
result =
result && defaultValue.matches(def.defaultValue.value, matchState);
}
return result;
}
}