Basic query actually works

This commit is contained in:
Tobe O 2018-08-02 11:17:14 -04:00
parent 10e740b2b1
commit 39fd284c67
15 changed files with 177 additions and 95 deletions

View file

@ -0,0 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="tests in query_test.dart" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true" nameIsGenerated="true">
<option name="filePath" value="$PROJECT_DIR$/graphql_server/test/query_test.dart" />
<option name="testRunnerOptions" value="-j4" />
<method />
</configuration>
</component>

View file

@ -1,7 +1,8 @@
import 'package:source_span/source_span.dart';
import '../token.dart';
import 'definition.dart';
import 'directive.dart';
import 'package:source_span/source_span.dart';
import 'selection_set.dart';
import 'variable_definitions.dart';
@ -12,7 +13,8 @@ class OperationDefinitionContext extends DefinitionContext {
final SelectionSetContext selectionSet;
bool get isMutation => TYPE?.text == 'mutation';
bool get isQuery => TYPE?.text == 'query';
bool get isQuery => TYPE?.text == 'query' || TYPE == null;
String get name => NAME?.text;

View file

@ -55,8 +55,8 @@ class StringValueContext extends ValueContext {
buf.writeCharCode(next);
}
} else
throw new SyntaxError.fromSourceLocation(
'Unexpected "\\" in string literal.', span.start);
throw new SyntaxError(
'Unexpected "\\" in string literal.', span);
} else {
buf.writeCharCode(ch);
}

View file

@ -35,9 +35,9 @@ final Map<Pattern, TokenType> _patterns = {
_name: TokenType.NAME
};
List<Token> scan(String text) {
List<Token> scan(String text, {sourceUrl}) {
List<Token> out = [];
var scanner = new SpanScanner(text);
var scanner = new SpanScanner(text, sourceUrl: sourceUrl);
while (!scanner.isDone) {
List<Token> potential = [];
@ -54,7 +54,7 @@ List<Token> scan(String text) {
if (potential.isEmpty) {
var ch = new String.fromCharCode(scanner.readChar());
throw new SyntaxError(
"Unexpected token '$ch'.", scanner.state.line, scanner.state.column);
"Unexpected token '$ch'.", scanner.emptySpan);
} else {
// Choose longest token
potential.sort((a, b) => b.text.length.compareTo(a.text.length));

View file

@ -66,13 +66,13 @@ class Parser {
TYPE, NAME, variables, selectionSet)
..directives.addAll(dirs);
else
throw new SyntaxError.fromSourceLocation(
throw new SyntaxError(
'Expected selection set in fragment definition.',
NAME.span.end);
NAME.span);
} else
throw new SyntaxError.fromSourceLocation(
throw new SyntaxError(
'Expected name after operation type "${TYPE.text}" in operation definition.',
TYPE.span.end);
TYPE.span);
} else
return null;
}
@ -94,21 +94,21 @@ class Parser {
FRAGMENT, NAME, ON, typeCondition, selectionSet)
..directives.addAll(dirs);
else
throw new SyntaxError.fromSourceLocation(
throw new SyntaxError(
'Expected selection set in fragment definition.',
typeCondition.span.end);
typeCondition.span);
} else
throw new SyntaxError.fromSourceLocation(
throw new SyntaxError(
'Expected type condition after "on" in fragment definition.',
ON.span.end);
ON.span);
} else
throw new SyntaxError.fromSourceLocation(
throw new SyntaxError(
'Expected "on" after name "${NAME.text}" in fragment definition.',
NAME.span.end);
NAME.span);
} else
throw new SyntaxError.fromSourceLocation(
throw new SyntaxError(
'Expected name after "fragment" in fragment definition.',
FRAGMENT.span.end);
FRAGMENT.span);
} else
return null;
}
@ -142,18 +142,18 @@ class Parser {
ELLIPSIS, ON, typeCondition, selectionSet)
..directives.addAll(directives);
} else
throw new SyntaxError.fromSourceLocation(
throw new SyntaxError(
'Expected selection set in inline fragment.',
directives.isEmpty
? typeCondition.span.end
: directives.last.span.end);
? typeCondition.span
: directives.last.span);
} else
throw new SyntaxError.fromSourceLocation(
throw new SyntaxError(
'Expected type condition after "on" in inline fragment.',
ON.span.end);
ON.span);
} else
throw new SyntaxError.fromSourceLocation(
'Expected "on" after "..." in inline fragment.', ELLIPSIS.span.end);
throw new SyntaxError(
'Expected "on" after "..." in inline fragment.', ELLIPSIS.span);
} else
return null;
}
@ -174,9 +174,9 @@ class Parser {
return new SelectionSetContext(LBRACE, current)
..selections.addAll(selections);
else
throw new SyntaxError.fromSourceLocation(
throw new SyntaxError(
'Expected "}" after selection set.',
selections.isEmpty ? LBRACE.span.end : selections.last.span.end);
selections.isEmpty ? LBRACE.span : selections.last.span);
} else
return null;
}
@ -216,8 +216,8 @@ class Parser {
return new FieldNameContext(
null, new AliasContext(NAME1, COLON, current));
else
throw new SyntaxError.fromSourceLocation(
'Expected name after colon in alias.', COLON.span.end);
throw new SyntaxError(
'Expected name after colon in alias.', COLON.span);
} else
return new FieldNameContext(NAME1);
} else
@ -240,8 +240,8 @@ class Parser {
return new VariableDefinitionsContext(LPAREN, current)
..variableDefinitions.addAll(defs);
else
throw new SyntaxError.fromSourceLocation(
'Expected ")" after variable definitions.', LPAREN.span.end);
throw new SyntaxError(
'Expected ")" after variable definitions.', LPAREN.span);
} else
return null;
}
@ -257,11 +257,11 @@ class Parser {
return new VariableDefinitionContext(
variable, COLON, type, defaultValue);
} else
throw new SyntaxError.fromSourceLocation(
'Expected type in variable definition.', COLON.span.end);
throw new SyntaxError(
'Expected type in variable definition.', COLON.span);
} else
throw new SyntaxError.fromSourceLocation(
'Expected ":" in variable definition.', variable.span.end);
throw new SyntaxError(
'Expected ":" in variable definition.', variable.span);
} else
return null;
}
@ -287,11 +287,11 @@ class Parser {
if (next(TokenType.RBRACKET)) {
return new ListTypeContext(LBRACKET, type, current);
} else
throw new SyntaxError.fromSourceLocation(
'Expected "]" in list type.', type.span.end);
throw new SyntaxError(
'Expected "]" in list type.', type.span);
} else
throw new SyntaxError.fromSourceLocation(
'Expected type after "[".', LBRACKET.span.end);
throw new SyntaxError(
'Expected type after "[".', LBRACKET.span);
} else
return null;
}
@ -320,9 +320,9 @@ class Parser {
return new DirectiveContext(
ARROBA, NAME, COLON, null, null, null, val);
else
throw new SyntaxError.fromSourceLocation(
throw new SyntaxError(
'Expected value or variable in directive after colon.',
COLON.span.end);
COLON.span);
} else if (next(TokenType.LPAREN)) {
var LPAREN = current;
var arg = parseArgument();
@ -331,17 +331,17 @@ class Parser {
return new DirectiveContext(
ARROBA, NAME, null, LPAREN, current, arg, null);
} else
throw new SyntaxError.fromSourceLocation(
'Expected \')\'', arg.valueOrVariable.span.end);
throw new SyntaxError(
'Expected \')\'', arg.valueOrVariable.span);
} else
throw new SyntaxError.fromSourceLocation(
'Expected argument in directive.', LPAREN.span.end);
throw new SyntaxError(
'Expected argument in directive.', LPAREN.span);
} else
return new DirectiveContext(
ARROBA, NAME, null, null, null, null, null);
} else
throw new SyntaxError.fromSourceLocation(
'Expected name for directive.', ARROBA.span.end);
throw new SyntaxError(
'Expected name for directive.', ARROBA.span);
} else
return null;
}
@ -363,8 +363,8 @@ class Parser {
if (next(TokenType.RPAREN))
return out;
else
throw new SyntaxError.fromSourceLocation(
'Expected ")" in argument list.', LPAREN.span.end);
throw new SyntaxError(
'Expected ")" in argument list.', LPAREN.span);
} else
return [];
}
@ -378,11 +378,11 @@ class Parser {
if (val != null)
return new ArgumentContext(NAME, COLON, val);
else
throw new SyntaxError.fromSourceLocation(
'Expected value or variable in argument.', COLON.span.end);
throw new SyntaxError(
'Expected value or variable in argument.', COLON.span);
} else
throw new SyntaxError.fromSourceLocation(
'Expected colon after name in argument.', NAME.span.end);
throw new SyntaxError(
'Expected colon after name in argument.', NAME.span);
} else
return null;
}
@ -406,9 +406,9 @@ class Parser {
if (next(TokenType.NAME))
return new VariableContext(DOLLAR, current);
else
throw new SyntaxError.fromSourceLocation(
throw new SyntaxError(
'Expected name for variable; found a lone "\$" instead.',
DOLLAR.span.end);
DOLLAR.span);
} else
return null;
}
@ -420,8 +420,8 @@ class Parser {
if (value != null) {
return new DefaultValueContext(EQUALS, value);
} else
throw new SyntaxError.fromSourceLocation(
'Expected value after "=" sign.', EQUALS.span.end);
throw new SyntaxError(
'Expected value after "=" sign.', EQUALS.span);
} else
return null;
}
@ -474,8 +474,8 @@ class Parser {
if (next(TokenType.RBRACKET)) {
return new ArrayValueContext(LBRACKET, current)..values.addAll(values);
} else
throw new SyntaxError.fromSourceLocation(
'Unterminated array literal.', LBRACKET.span.end);
throw new SyntaxError(
'Unterminated array literal.', LBRACKET.span);
} else
return null;
}

View file

@ -1,18 +1,11 @@
import 'package:source_span/source_span.dart';
import 'token.dart';
class SyntaxError implements Exception {
final String message;
final int line, column;
final Token offendingToken;
final FileSpan span;
SyntaxError(this.message, this.line, this.column, [this.offendingToken]);
factory SyntaxError.fromSourceLocation(
String message, SourceLocation location,
[Token offendingToken]) =>
new SyntaxError(message, location.line, location.column, offendingToken);
SyntaxError(this.message, this.span);
@override
String toString() => 'Syntax error at line $line, column $column: $message';
String toString() => 'Syntax error at ${span.start.toolString}: $message\n${span.highlight()}';
}

View file

@ -4,9 +4,14 @@ final GraphQLSchema todoSchema = new GraphQLSchema(
query: objectType('Todo', [
field(
'text',
innerType: graphQLString.nonNullable(),
type: graphQLString.nonNullable(),
resolve: resolveToNull,
),
field(
'created_at',
type: graphQLDate,
resolve: resolveToNull,
),
field('created_at', innerType: graphQLDate),
]),
);
@ -14,7 +19,11 @@ main() {
// Validation
var validation = todoSchema.query.validate(
'@root',
{'foo': 'bar', 'text': null, 'created_at': 24,},
{
'foo': 'bar',
'text': null,
'created_at': 24,
},
);
if (validation.successful) {

View file

@ -11,7 +11,7 @@ class GraphQLField<Value, Serialized> {
GraphQLField(this.name,
{Iterable<GraphQLFieldArgument> arguments: const <GraphQLFieldArgument>[],
this.resolve,
@required this.resolve,
this.type}) {
this.arguments.addAll(arguments ?? <GraphQLFieldArgument>[]);
}

View file

@ -5,10 +5,9 @@ GraphQLObjectType objectType(String name,
new GraphQLObjectType(name)..fields.addAll(fields ?? []);
GraphQLField<T, Serialized> field<T, Serialized>(String name,
{Iterable<GraphQLFieldArgument<T, Serialized>> arguments:
const <GraphQLFieldArgument<T, Serialized>>[],
{Iterable<GraphQLFieldArgument<T, Serialized>> arguments: const [],
GraphQLFieldResolver<T, Serialized> resolve,
GraphQLType<T, Serialized> innerType}) {
GraphQLType<T, Serialized> type}) {
return new GraphQLField(name,
arguments: arguments, resolve: resolve, type: innerType);
arguments: arguments, resolve: resolve, type: type);
}

View file

@ -20,3 +20,6 @@ class GraphQLSchema {
GraphQLSchema graphQLSchema(
{@required GraphQLObjectType query, GraphQLObjectType mutation}) =>
new GraphQLSchema(query: query, mutation: mutation);
/// A default resolver that always returns `null`.
resolveToNull(_, __) => null;

View file

@ -5,5 +5,7 @@ author: Tobe O <thosakwe@gmail.com>
homepage: https://github.com/thosakwe/graphql_schema
environment:
sdk: ">=1.8.0 <3.0.0"
dependencies:
meta: ^1.0.0
dev_dependencies:
test: ^0.12.0

View file

@ -1,14 +1,14 @@
import 'package:graphql_schema/graphql_schema.dart';
final GraphQLObjectType pokemonType = objectType('Pokemon', [
field('species', innerType: graphQLString),
field('catch_date', innerType: graphQLDate)
field('species', type: graphQLString),
field('catch_date', type: graphQLDate)
]);
final GraphQLObjectType trainerType =
objectType('Trainer', [field('name', innerType: graphQLString)]);
objectType('Trainer', [field('name', type: graphQLString)]);
final GraphQLObjectType pokemonRegionType = objectType('PokemonRegion', [
field('trainer', innerType: trainerType),
field('pokemon_species', innerType: listType(pokemonType))
field('trainer', type: trainerType),
field('pokemon_species', type: listType(pokemonType))
]);

View file

@ -44,9 +44,25 @@ class GraphQL {
}
}
Future<Map<String, dynamic>> parseAndExecute(String text,
{String operationName,
sourceUrl,
Map<String, dynamic> variableValues: const {},
initialValue}) {
var tokens = scan(text, sourceUrl: sourceUrl);
var parser = new Parser(tokens);
var document = parser.parseDocument();
return executeRequest(schema, document,
operationName: operationName,
initialValue: initialValue,
variableValues: variableValues);
}
Future<Map<String, dynamic>> executeRequest(
GraphQLSchema schema, DocumentContext document, String operationName,
{Map<String, dynamic> variableValues: const {}, initialValue}) async {
GraphQLSchema schema, DocumentContext document,
{String operationName,
Map<String, dynamic> variableValues: const {},
initialValue}) async {
var operation = getOperation(document, operationName);
var coercedVariableValues =
coerceVariableValues(schema, operation, variableValues ?? {});
@ -62,17 +78,20 @@ class GraphQL {
OperationDefinitionContext getOperation(
DocumentContext document, String operationName) {
var ops = document.definitions.whereType<OperationDefinitionContext>();
var ops =
document.definitions.where((d) => d is OperationDefinitionContext);
if (operationName == null) {
return ops.length == 1
? ops.first
? ops.first as OperationDefinitionContext
: throw new GraphQLException(
'Missing required operation "$operationName".');
'This document does not define any operations.');
} else {
return ops.firstWhere((d) => d.name == operationName,
orElse: () => throw new GraphQLException(
'Missing required operation "$operationName".'));
return ops.firstWhere(
(d) => (d as OperationDefinitionContext).name == operationName,
orElse: () => throw new GraphQLException(
'Missing required operation "$operationName".'))
as OperationDefinitionContext;
}
}
@ -139,8 +158,9 @@ class GraphQL {
for (var field in fields) {
var fieldName = field.field.fieldName.name;
var fieldType =
objectType.fields.firstWhere((f) => f.name == fieldName)?.type;
var fieldType = objectType.fields
.firstWhere((f) => f.name == fieldName, orElse: () => null)
?.type;
if (fieldType == null) continue;
var responseValue = await executeField(document, fieldName, objectType,
objectValue, fields, fieldType, variableValues);
@ -211,7 +231,12 @@ class GraphQL {
Future<T> resolveFieldValue<T>(GraphQLObjectType objectType, T objectValue,
String fieldName, Map<String, dynamic> argumentValues) async {
var field = objectType.fields.firstWhere((f) => f.name == fieldName);
return await field.resolve(objectValue, argumentValues) as T;
if (field.resolve == null) {
return null;
} else {
return await field.resolve(objectValue, argumentValues) as T;
}
}
Future completeValue(
@ -375,4 +400,7 @@ class GraphQL {
class GraphQLException extends FormatException {
GraphQLException(String message) : super(message);
@override
String toString() => 'GraphQL exception: $message';
}

View file

@ -4,4 +4,5 @@ dependencies:
path: ../graphql_schema
graphql_parser:
path: ../graphql_parser
symbol_table: ^1.0.0
dev_dependencies:
test: ^0.12.0

View file

@ -0,0 +1,38 @@
import 'package:graphql_schema/graphql_schema.dart';
import 'package:graphql_server/graphql_server.dart';
import 'package:test/test.dart';
void main() {
test('todo', () async {
var schema = graphQLSchema(
query: objectType('todo', [
field(
'text',
type: graphQLString,
resolve: (obj, args) => obj['text'],
),
field(
'completed',
type: graphQLBoolean,
resolve: (obj, args) => obj['completed'],
),
]),
);
var graphql = new GraphQL(schema);
var result = await graphql.parseAndExecute('{ text }', initialValue: {
'text': 'Clean your room!',
'completed': false,
});
print(result);
expect(result, {'text': 'Clean your room!'});
});
}
class Todo {
final String text;
final bool completed;
Todo({this.text, this.completed});
}