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

View file

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

View file

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

View file

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

View file

@ -1,18 +1,11 @@
import 'package:source_span/source_span.dart'; import 'package:source_span/source_span.dart';
import 'token.dart';
class SyntaxError implements Exception { class SyntaxError implements Exception {
final String message; final String message;
final int line, column; final FileSpan span;
final Token offendingToken;
SyntaxError(this.message, this.line, this.column, [this.offendingToken]); SyntaxError(this.message, this.span);
factory SyntaxError.fromSourceLocation(
String message, SourceLocation location,
[Token offendingToken]) =>
new SyntaxError(message, location.line, location.column, offendingToken);
@override @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', [ query: objectType('Todo', [
field( field(
'text', '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 // Validation
var validation = todoSchema.query.validate( var validation = todoSchema.query.validate(
'@root', '@root',
{'foo': 'bar', 'text': null, 'created_at': 24,}, {
'foo': 'bar',
'text': null,
'created_at': 24,
},
); );
if (validation.successful) { if (validation.successful) {

View file

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

View file

@ -5,10 +5,9 @@ GraphQLObjectType objectType(String name,
new GraphQLObjectType(name)..fields.addAll(fields ?? []); new GraphQLObjectType(name)..fields.addAll(fields ?? []);
GraphQLField<T, Serialized> field<T, Serialized>(String name, GraphQLField<T, Serialized> field<T, Serialized>(String name,
{Iterable<GraphQLFieldArgument<T, Serialized>> arguments: {Iterable<GraphQLFieldArgument<T, Serialized>> arguments: const [],
const <GraphQLFieldArgument<T, Serialized>>[],
GraphQLFieldResolver<T, Serialized> resolve, GraphQLFieldResolver<T, Serialized> resolve,
GraphQLType<T, Serialized> innerType}) { GraphQLType<T, Serialized> type}) {
return new GraphQLField(name, 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( GraphQLSchema graphQLSchema(
{@required GraphQLObjectType query, GraphQLObjectType mutation}) => {@required GraphQLObjectType query, GraphQLObjectType mutation}) =>
new GraphQLSchema(query: query, mutation: 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 homepage: https://github.com/thosakwe/graphql_schema
environment: environment:
sdk: ">=1.8.0 <3.0.0" sdk: ">=1.8.0 <3.0.0"
dependencies:
meta: ^1.0.0
dev_dependencies: dev_dependencies:
test: ^0.12.0 test: ^0.12.0

View file

@ -1,14 +1,14 @@
import 'package:graphql_schema/graphql_schema.dart'; import 'package:graphql_schema/graphql_schema.dart';
final GraphQLObjectType pokemonType = objectType('Pokemon', [ final GraphQLObjectType pokemonType = objectType('Pokemon', [
field('species', innerType: graphQLString), field('species', type: graphQLString),
field('catch_date', innerType: graphQLDate) field('catch_date', type: graphQLDate)
]); ]);
final GraphQLObjectType trainerType = final GraphQLObjectType trainerType =
objectType('Trainer', [field('name', innerType: graphQLString)]); objectType('Trainer', [field('name', type: graphQLString)]);
final GraphQLObjectType pokemonRegionType = objectType('PokemonRegion', [ final GraphQLObjectType pokemonRegionType = objectType('PokemonRegion', [
field('trainer', innerType: trainerType), field('trainer', type: trainerType),
field('pokemon_species', innerType: listType(pokemonType)) 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( Future<Map<String, dynamic>> executeRequest(
GraphQLSchema schema, DocumentContext document, String operationName, GraphQLSchema schema, DocumentContext document,
{Map<String, dynamic> variableValues: const {}, initialValue}) async { {String operationName,
Map<String, dynamic> variableValues: const {},
initialValue}) async {
var operation = getOperation(document, operationName); var operation = getOperation(document, operationName);
var coercedVariableValues = var coercedVariableValues =
coerceVariableValues(schema, operation, variableValues ?? {}); coerceVariableValues(schema, operation, variableValues ?? {});
@ -62,17 +78,20 @@ class GraphQL {
OperationDefinitionContext getOperation( OperationDefinitionContext getOperation(
DocumentContext document, String operationName) { DocumentContext document, String operationName) {
var ops = document.definitions.whereType<OperationDefinitionContext>(); var ops =
document.definitions.where((d) => d is OperationDefinitionContext);
if (operationName == null) { if (operationName == null) {
return ops.length == 1 return ops.length == 1
? ops.first ? ops.first as OperationDefinitionContext
: throw new GraphQLException( : throw new GraphQLException(
'Missing required operation "$operationName".'); 'This document does not define any operations.');
} else { } else {
return ops.firstWhere((d) => d.name == operationName, return ops.firstWhere(
orElse: () => throw new GraphQLException( (d) => (d as OperationDefinitionContext).name == operationName,
'Missing required operation "$operationName".')); orElse: () => throw new GraphQLException(
'Missing required operation "$operationName".'))
as OperationDefinitionContext;
} }
} }
@ -139,8 +158,9 @@ class GraphQL {
for (var field in fields) { for (var field in fields) {
var fieldName = field.field.fieldName.name; var fieldName = field.field.fieldName.name;
var fieldType = var fieldType = objectType.fields
objectType.fields.firstWhere((f) => f.name == fieldName)?.type; .firstWhere((f) => f.name == fieldName, orElse: () => null)
?.type;
if (fieldType == null) continue; if (fieldType == null) continue;
var responseValue = await executeField(document, fieldName, objectType, var responseValue = await executeField(document, fieldName, objectType,
objectValue, fields, fieldType, variableValues); objectValue, fields, fieldType, variableValues);
@ -211,7 +231,12 @@ class GraphQL {
Future<T> resolveFieldValue<T>(GraphQLObjectType objectType, T objectValue, Future<T> resolveFieldValue<T>(GraphQLObjectType objectType, T objectValue,
String fieldName, Map<String, dynamic> argumentValues) async { String fieldName, Map<String, dynamic> argumentValues) async {
var field = objectType.fields.firstWhere((f) => f.name == fieldName); 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( Future completeValue(
@ -375,4 +400,7 @@ class GraphQL {
class GraphQLException extends FormatException { class GraphQLException extends FormatException {
GraphQLException(String message) : super(message); GraphQLException(String message) : super(message);
@override
String toString() => 'GraphQL exception: $message';
} }

View file

@ -4,4 +4,5 @@ dependencies:
path: ../graphql_schema path: ../graphql_schema
graphql_parser: graphql_parser:
path: ../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});
}