merge working branch in

This commit is contained in:
Tobe O 2020-02-03 15:34:31 -05:00
commit 935c06a51b
59 changed files with 1020 additions and 464 deletions

View file

@ -1,3 +1,6 @@
# 1.1.0
* Support the GraphQL multipart spec: https://github.com/jaydenseric/graphql-multipart-request-spec
# 1.0.0 # 1.0.0
* Apply `package:pedantic`. * Apply `package:pedantic`.

View file

@ -74,7 +74,7 @@ Future configureServer(Angel app) async {
convertDartType(Todo), convertDartType(Todo),
resolve: resolveViaServiceRead(todoService), resolve: resolveViaServiceRead(todoService),
inputs: [ inputs: [
new GraphQLFieldInput('id', graphQLId.nonNullable()), GraphQLFieldInput('id', graphQLId.nonNullable()),
], ],
), ),
], ],
@ -107,7 +107,7 @@ In *development*, it's also highly recommended to mount the
interface, for easy querying and feedback. interface, for easy querying and feedback.
```dart ```dart
app.all('/graphql', graphQLHttp(new GraphQL(schema))); app.all('/graphql', graphQLHttp(GraphQL(schema)));
app.get('/graphiql', graphiQL()); app.get('/graphiql', graphiQL());
``` ```
@ -116,7 +116,7 @@ All that's left now is just to start the server!
```dart ```dart
var server = await http.startServer('127.0.0.1', 3000); var server = await http.startServer('127.0.0.1', 3000);
var uri = var uri =
new Uri(scheme: 'http', host: server.address.address, port: server.port); Uri(scheme: 'http', host: server.address.address, port: server.port);
var graphiqlUri = uri.replace(path: 'graphiql'); var graphiqlUri = uri.replace(path: 'graphiql');
print('Listening at $uri'); print('Listening at $uri');
print('Access graphiql at $graphiqlUri'); print('Access graphiql at $graphiqlUri');
@ -214,7 +214,7 @@ var queryType = objectType(
convertDartType(Todo), convertDartType(Todo),
resolve: resolveViaServiceRead(todoService), resolve: resolveViaServiceRead(todoService),
inputs: [ inputs: [
new GraphQLFieldInput('id', graphQLId.nonNullable()), GraphQLFieldInput('id', graphQLId.nonNullable()),
], ],
), ),
], ],

View file

@ -1,4 +1,46 @@
import 'package:angel_framework/angel_framework.dart';
import 'package:graphql_schema/graphql_schema.dart';
export 'src/graphiql.dart'; export 'src/graphiql.dart';
export 'src/graphql_http.dart'; export 'src/graphql_http.dart';
export 'src/graphql_ws.dart'; export 'src/graphql_ws.dart';
export 'src/resolvers.dart'; export 'src/resolvers.dart';
/// The canonical [GraphQLUploadType] instance.
final GraphQLUploadType graphQLUpload = GraphQLUploadType();
/// A [GraphQLScalarType] that is used to read uploaded files from
/// `multipart/form-data` requests.
class GraphQLUploadType extends GraphQLScalarType<UploadedFile, UploadedFile> {
@override
String get name => 'Upload';
@override
String get description =>
'Represents a file that has been uploaded to the server.';
@override
GraphQLType<UploadedFile, UploadedFile> coerceToInputObject() => this;
@override
UploadedFile deserialize(UploadedFile serialized) => serialized;
@override
UploadedFile serialize(UploadedFile value) => value;
@override
ValidationResult<UploadedFile> validate(String key, UploadedFile input) {
if (input != null && input is! UploadedFile) {
return _Vr(false, errors: ['Expected "$key" to be a boolean.']);
}
return _Vr(true, value: input);
}
}
// TODO: Really need to make the validation result constructors *public*
class _Vr<T> implements ValidationResult<T> {
final bool successful;
final List<String> errors;
final T value;
_Vr(this.successful, {this.errors, this.value});
}

View file

@ -9,7 +9,7 @@ RequestHandler graphiQL(
{String graphQLEndpoint = '/graphql', String subscriptionsEndpoint}) { {String graphQLEndpoint = '/graphql', String subscriptionsEndpoint}) {
return (req, res) { return (req, res) {
res res
..contentType = new MediaType('text', 'html') ..contentType = MediaType('text', 'html')
..write(renderGraphiql( ..write(renderGraphiql(
graphqlEndpoint: graphQLEndpoint, graphqlEndpoint: graphQLEndpoint,
subscriptionsEndpoint: subscriptionsEndpoint)) subscriptionsEndpoint: subscriptionsEndpoint))
@ -30,7 +30,7 @@ String renderGraphiql(
<script src="//unpkg.com/graphiql-subscriptions-fetcher@0.0.2/browser/client.js"></script> <script src="//unpkg.com/graphiql-subscriptions-fetcher@0.0.2/browser/client.js"></script>
'''; ''';
subscriptionsFetcher = ''' subscriptionsFetcher = '''
let subscriptionsClient = new window.SubscriptionsTransportWs.SubscriptionClient('$subscriptionsEndpoint', { let subscriptionsClient = window.SubscriptionsTransportWs.SubscriptionClient('$subscriptionsEndpoint', {
reconnect: true reconnect: true
}); });
let $fetcherName = window.GraphiQLSubscriptionsFetcher.graphQLFetcher(subscriptionsClient, graphQLFetcher); let $fetcherName = window.GraphiQLSubscriptionsFetcher.graphQLFetcher(subscriptionsClient, graphQLFetcher);

View file

@ -7,15 +7,16 @@ import 'package:graphql_parser/graphql_parser.dart';
import 'package:graphql_schema/graphql_schema.dart'; import 'package:graphql_schema/graphql_schema.dart';
import 'package:graphql_server/graphql_server.dart'; import 'package:graphql_server/graphql_server.dart';
final ContentType graphQlContentType = final ContentType graphQlContentType = ContentType('application', 'graphql');
new ContentType('application', 'graphql');
final Validator graphQlPostBody = new Validator({ final Validator graphQlPostBody = Validator({
'query*': isNonEmptyString, 'query*': isNonEmptyString,
'operation_name': isNonEmptyString, 'operation_name': isNonEmptyString,
'variables': predicate((v) => v == null || v is String || v is Map), 'variables': predicate((v) => v == null || v is String || v is Map),
}); });
final RegExp _num = RegExp(r'^[0-9]+$');
/// A [RequestHandler] that serves a spec-compliant GraphQL backend. /// A [RequestHandler] that serves a spec-compliant GraphQL backend.
/// ///
/// Follows the guidelines listed here: /// Follows the guidelines listed here:
@ -80,30 +81,92 @@ RequestHandler graphQLHttp(GraphQL graphQL,
if (await validate(graphQlPostBody)(req, res) as bool) { if (await validate(graphQlPostBody)(req, res) as bool) {
return await executeMap(req.bodyAsMap); return await executeMap(req.bodyAsMap);
} }
} else if (req.headers.contentType?.mimeType == 'multipart/form-data') {
// TODO: Support file uploads in batch requests.
var fields = await req.parseBody().then((_) => req.bodyAsMap);
var operations = fields['operations'] as String;
if (operations == null) {
throw AngelHttpException.badRequest(
message: 'Missing "operations" field.');
}
var map = fields.containsKey('map')
? json.decode(fields['map'] as String)
: null;
if (map is! Map) {
throw AngelHttpException.badRequest(
message: '"map" field must decode to a JSON object.');
}
var variables = Map<String, dynamic>.from(globalVariables);
for (var entry in (map as Map).entries) {
var file = req.uploadedFiles
.firstWhere((f) => f.name == entry.key, orElse: () => null);
if (file == null) {
throw AngelHttpException.badRequest(
message:
'"map" contained key "${entry.key}", but no uploaded file '
'has that name.');
}
if (entry.value is! List) {
throw AngelHttpException.badRequest(
message:
'The value for "${entry.key}" in the "map" field was not a JSON array.');
}
var objectPaths = entry.value as List;
for (var objectPath in objectPaths) {
var subPaths = (objectPath as String).split('.');
if (subPaths[0] == 'variables') {
Object current = variables;
for (int i = 1; i < subPaths.length; i++) {
var name = subPaths[i];
var parent = subPaths.take(i).join('.');
if (_num.hasMatch(name)) {
if (current is! List) {
throw AngelHttpException.badRequest(
message:
'Object "$parent" is not a JSON array, but the '
'"map" field contained a mapping to $parent.$name.');
}
(current as List)[int.parse(name)] = file;
} else {
if (current is! Map) {
throw AngelHttpException.badRequest(
message:
'Object "$parent" is not a JSON object, but the '
'"map" field contained a mapping to $parent.$name.');
}
(current as Map)[name] = file;
}
}
} else {
throw AngelHttpException.badRequest(
message:
'All array values in the "map" field must begin with "variables.".');
}
}
}
return await sendGraphQLResponse(await graphQL.parseAndExecute(
operations,
sourceUrl: 'input',
globalVariables: variables,
));
} else { } else {
throw new AngelHttpException.badRequest(); throw AngelHttpException.badRequest();
} }
} else { } else {
throw new AngelHttpException.badRequest(); throw AngelHttpException.badRequest();
} }
} on ValidationException catch (e) { } on ValidationException catch (e) {
var errors = <GraphQLExceptionError>[ var errors = <GraphQLExceptionError>[GraphQLExceptionError(e.message)];
new GraphQLExceptionError(e.message)
];
errors errors.addAll(e.errors.map((ee) => GraphQLExceptionError(ee)).toList());
.addAll(e.errors.map((ee) => new GraphQLExceptionError(ee)).toList()); return GraphQLException(errors).toJson();
return new GraphQLException(errors).toJson();
} on AngelHttpException catch (e) { } on AngelHttpException catch (e) {
var errors = <GraphQLExceptionError>[ var errors = <GraphQLExceptionError>[GraphQLExceptionError(e.message)];
new GraphQLExceptionError(e.message)
];
errors errors.addAll(e.errors.map((ee) => GraphQLExceptionError(ee)).toList());
.addAll(e.errors.map((ee) => new GraphQLExceptionError(ee)).toList()); return GraphQLException(errors).toJson();
return new GraphQLException(errors).toJson();
} on SyntaxError catch (e) { } on SyntaxError catch (e) {
return new GraphQLException.fromSourceSpan(e.message, e.span); return GraphQLException.fromSourceSpan(e.message, e.span);
} on GraphQLException catch (e) { } on GraphQLException catch (e) {
return e.toJson(); return e.toJson();
} catch (e, st) { } catch (e, st) {
@ -114,7 +177,7 @@ RequestHandler graphQLHttp(GraphQL graphQL,
st); st);
} }
return new GraphQLException.fromMessage(e.toString()).toJson(); return GraphQLException.fromMessage(e.toString()).toJson();
} }
}; };
} }

View file

@ -22,11 +22,12 @@ RequestHandler graphQLWS(GraphQL graphQL, {Duration keepAliveInterval}) {
await res.detach(); await res.detach();
var socket = await WebSocketTransformer.upgrade(req.rawRequest, var socket = await WebSocketTransformer.upgrade(req.rawRequest,
protocolSelector: (protocols) { protocolSelector: (protocols) {
if (protocols.contains('graphql-ws')) if (protocols.contains('graphql-ws')) {
return 'graphql-ws'; return 'graphql-ws';
else } else {
throw AngelHttpException.badRequest( throw AngelHttpException.badRequest(
message: 'Only the "graphql-ws" protocol is allowed.'); message: 'Only the "graphql-ws" protocol is allowed.');
}
}); });
var channel = IOWebSocketChannel(socket); var channel = IOWebSocketChannel(socket);
var client = stw.RemoteClient(channel.cast<String>()); var client = stw.RemoteClient(channel.cast<String>());
@ -69,6 +70,7 @@ class _GraphQLWSServer extends stw.Server {
operationName: operationName, operationName: operationName,
sourceUrl: 'input', sourceUrl: 'input',
globalVariables: globalVariables, globalVariables: globalVariables,
variableValues: variables,
); );
return stw.GraphQLResult(data); return stw.GraphQLResult(data);
} on GraphQLException catch (e) { } on GraphQLException catch (e) {

View file

@ -1,3 +1,9 @@
# 1.2.0
* Combine `ValueContext` and `VariableContext` into a single `InputValueContext` supertype.
* Add `T computeValue(Map<String, dynamic> variables);`
* Resolve [#23](https://github.com/angel-dart/graphql/issues/23).
* Deprecate old `ValueOrVariable` class, and parser/AST methods related to it.
# 1.1.4 # 1.1.4
* Fix broken int variable parsing - https://github.com/angel-dart/graphql/pull/32 * Fix broken int variable parsing - https://github.com/angel-dart/graphql/pull/32

View file

@ -33,7 +33,7 @@ import 'package:graphql_parser/graphql_parser.dart';
doSomething(String text) { doSomething(String text) {
var tokens = scan(text); var tokens = scan(text);
var parser = new Parser(tokens); var parser = Parser(tokens);
if (parser.errors.isNotEmpty) { if (parser.errors.isNotEmpty) {
// Handle errors... // Handle errors...

View file

@ -1,3 +1,4 @@
include: package:pedantic/analysis_options.yaml
analyzer: analyzer:
strong-mode: strong-mode:
implicit-casts: false implicit-casts: false

View file

@ -11,7 +11,7 @@ final String text = '''
main() { main() {
var tokens = scan(text); var tokens = scan(text);
var parser = new Parser(tokens); var parser = Parser(tokens);
var doc = parser.parseDocument(); var doc = parser.parseDocument();
var operation = doc.definitions.first as OperationDefinitionContext; var operation = doc.definitions.first as OperationDefinitionContext;
@ -19,7 +19,7 @@ main() {
var projectField = operation.selectionSet.selections.first.field; var projectField = operation.selectionSet.selections.first.field;
print(projectField.fieldName.name); // project print(projectField.fieldName.name); // project
print(projectField.arguments.first.name); // name print(projectField.arguments.first.name); // name
print(projectField.arguments.first.valueOrVariable.value.value); // GraphQL print(projectField.arguments.first.value); // GraphQL
var taglineField = projectField.selectionSet.selections.first.field; var taglineField = projectField.selectionSet.selections.first.field;
print(taglineField.fieldName.name); // tagline print(taglineField.fieldName.name); // tagline

View file

@ -1,19 +1,33 @@
import 'package:source_span/source_span.dart'; import 'package:source_span/source_span.dart';
import '../token.dart'; import '../token.dart';
import 'node.dart'; import 'node.dart';
/// An alternate name for a field within a [SelectionSet].
class AliasContext extends Node { class AliasContext extends Node {
final Token NAME1, COLON, NAME2; /// The source tokens.
final Token nameToken1, colonToken, nameToken2;
AliasContext(this.NAME1, this.COLON, this.NAME2); AliasContext(this.nameToken1, this.colonToken, this.nameToken2);
/// Use [nameToken1] instead.
@deprecated
Token get NAME1 => nameToken1;
/// Use [colonToken] instead.
@deprecated
Token get COLON => colonToken;
/// Use [nameToken2] instead.
@deprecated
Token get NAME2 => nameToken2;
/// The aliased name of the value. /// The aliased name of the value.
String get alias => NAME1.text; String get alias => nameToken1.text;
/// The actual name of the value. /// The actual name of the value.
String get name => NAME2.text; String get name => nameToken2.text;
@override @override
FileSpan get span => NAME1.span.expand(COLON.span).expand(NAME2.span); FileSpan get span =>
nameToken1.span.expand(colonToken.span).expand(nameToken2.span);
} }

View file

@ -1,17 +1,34 @@
import 'package:source_span/source_span.dart'; import 'package:source_span/source_span.dart';
import '../token.dart'; import '../token.dart';
import 'node.dart'; import 'node.dart';
import 'value_or_variable.dart'; import 'input_value.dart';
/// An argument passed to a [FieldContext].
class ArgumentContext extends Node { class ArgumentContext extends Node {
final Token NAME, COLON; /// The source tokens.
final ValueOrVariableContext valueOrVariable; final Token nameToken, colonToken;
ArgumentContext(this.NAME, this.COLON, this.valueOrVariable); /// The value of the argument.
final InputValueContext value;
String get name => NAME.text; ArgumentContext(this.nameToken, this.colonToken, this.value);
/// Use [value] instead.
@deprecated
InputValueContext get valueOrVariable => value;
/// Use [nameToken] instead.
@deprecated
Token get NAME => nameToken;
/// Use [colonToken] instead.
@deprecated
Token get COLON => colonToken;
/// The name of the argument, as a [String].
String get name => nameToken.text;
@override @override
FileSpan get span => FileSpan get span =>
NAME.span.expand(COLON.span).expand(valueOrVariable.span); nameToken.span.expand(colonToken.span).expand(value.span);
} }

View file

@ -1,19 +1,34 @@
import 'package:source_span/source_span.dart'; import 'package:source_span/source_span.dart';
import '../token.dart'; import '../token.dart';
import 'value.dart'; import 'input_value.dart';
class ListValueContext extends ValueContext { /// A GraphQL list value literal.
final Token LBRACKET, RBRACKET; class ListValueContext extends InputValueContext {
final List<ValueContext> values = []; /// The source tokens.
final Token lBracketToken, rBracketToken;
ListValueContext(this.LBRACKET, this.RBRACKET); /// The child values.
final List<InputValueContext> values = [];
ListValueContext(this.lBracketToken, this.rBracketToken);
/// Use [lBracketToken] instead.
@deprecated
Token get LBRACKET => lBracketToken;
/// Use [rBracketToken] instead.
@deprecated
Token get RBRACKET => rBracketToken;
@override @override
FileSpan get span { FileSpan get span {
var out = values.fold<FileSpan>(LBRACKET.span, (o, v) => o.expand(v.span)); var out =
return out.expand(RBRACKET.span); values.fold<FileSpan>(lBracketToken.span, (o, v) => o.expand(v.span));
return out.expand(rBracketToken.span);
} }
@override @override
List get value => values.map((v) => v.value).toList(); computeValue(Map<String, dynamic> variables) {
return values.map((v) => v.computeValue(variables)).toList();
}
} }

View file

@ -6,6 +6,7 @@ export 'argument.dart';
export 'boolean_value.dart'; export 'boolean_value.dart';
export 'default_value.dart'; export 'default_value.dart';
export 'definition.dart'; export 'definition.dart';
export 'deprecated_value.dart';
export 'directive.dart'; export 'directive.dart';
export 'document.dart'; export 'document.dart';
export 'field.dart'; export 'field.dart';
@ -13,6 +14,7 @@ export 'field_name.dart';
export 'fragment_definition.dart'; export 'fragment_definition.dart';
export 'fragment_spread.dart'; export 'fragment_spread.dart';
export 'inline_fragment.dart'; export 'inline_fragment.dart';
export 'input_value.dart';
export 'list_type.dart'; export 'list_type.dart';
export 'misc_value.dart'; export 'misc_value.dart';
export 'node.dart'; export 'node.dart';
@ -24,8 +26,6 @@ export 'string_value.dart';
export 'type.dart'; export 'type.dart';
export 'type_condition.dart'; export 'type_condition.dart';
export 'type_name.dart'; export 'type_name.dart';
export 'value.dart';
export 'value_or_variable.dart';
export 'variable.dart'; export 'variable.dart';
export 'variable_definition.dart'; export 'variable_definition.dart';
export 'variable_definitions.dart'; export 'variable_definitions.dart';

View file

@ -1,20 +1,28 @@
import '../token.dart';
import 'package:source_span/source_span.dart'; import 'package:source_span/source_span.dart';
import 'value.dart'; import 'input_value.dart';
import '../token.dart';
class BooleanValueContext extends ValueContext<bool> { /// A GraphQL boolean value literal.
class BooleanValueContext extends InputValueContext<bool> {
bool _valueCache; bool _valueCache;
final Token BOOLEAN;
BooleanValueContext(this.BOOLEAN) { /// The source token.
assert(BOOLEAN?.text == 'true' || BOOLEAN?.text == 'false'); final Token booleanToken;
BooleanValueContext(this.booleanToken) {
assert(booleanToken?.text == 'true' || booleanToken?.text == 'false');
} }
bool get booleanValue => _valueCache ??= BOOLEAN.text == 'true'; /// The [bool] value of this literal.
bool get booleanValue => _valueCache ??= booleanToken.text == 'true';
/// Use [booleanToken] instead.
@deprecated
Token get BOOLEAN => booleanToken;
@override @override
bool get value => booleanValue; FileSpan get span => booleanToken.span;
@override @override
FileSpan get span => BOOLEAN.span; bool computeValue(Map<String, dynamic> variables) => booleanValue;
} }

View file

@ -1,14 +1,22 @@
import '../token.dart';
import 'node.dart';
import 'package:source_span/source_span.dart'; import 'package:source_span/source_span.dart';
import 'value.dart'; import '../token.dart';
import 'input_value.dart';
import 'node.dart';
/// The default value to be passed to an [ArgumentContext].
class DefaultValueContext extends Node { class DefaultValueContext extends Node {
final Token EQUALS; /// The source token.
final ValueContext value; final Token equalsToken;
DefaultValueContext(this.EQUALS, this.value); /// The default value for the argument.
final InputValueContext value;
DefaultValueContext(this.equalsToken, this.value);
/// Use [equalsToken] instead.
@deprecated
Token get EQUALS => equalsToken;
@override @override
FileSpan get span => EQUALS.span.expand(value.span); FileSpan get span => equalsToken.span.expand(value.span);
} }

View file

@ -1,9 +1,13 @@
import 'node.dart'; import 'node.dart';
/// The base class for top-level GraphQL definitions.
abstract class DefinitionContext extends Node {} abstract class DefinitionContext extends Node {}
/// An executable definition.
abstract class ExecutableDefinitionContext extends DefinitionContext {} abstract class ExecutableDefinitionContext extends DefinitionContext {}
/// An ad-hoc type system declared in GraphQL.
abstract class TypeSystemDefinitionContext extends DefinitionContext {} abstract class TypeSystemDefinitionContext extends DefinitionContext {}
/// An extension to an existing ad-hoc type system.
abstract class TypeSystemExtensionContext extends DefinitionContext {} abstract class TypeSystemExtensionContext extends DefinitionContext {}

View file

@ -0,0 +1,11 @@
import 'input_value.dart';
/// Use [ConstantContext] instead. This class remains solely for backwards compatibility.
@deprecated
abstract class ValueContext<T> extends InputValueContext<T> {
/// Return a constant value.
T get value;
@override
T computeValue(Map<String, dynamic> variables) => value;
}

View file

@ -1,27 +1,60 @@
import 'package:source_span/source_span.dart';
import '../token.dart'; import '../token.dart';
import 'argument.dart'; import 'argument.dart';
import 'input_value.dart';
import 'node.dart'; import 'node.dart';
import 'package:source_span/source_span.dart';
import 'value_or_variable.dart';
/// A GraphQL directive, which may or may not have runtime semantics.
class DirectiveContext extends Node { class DirectiveContext extends Node {
final Token ARROBA, NAME, COLON, LPAREN, RPAREN; /// The source tokens.
final ArgumentContext argument; final Token arrobaToken, nameToken, colonToken, lParenToken, rParenToken;
final ValueOrVariableContext valueOrVariable;
DirectiveContext(this.ARROBA, this.NAME, this.COLON, this.LPAREN, this.RPAREN, /// The argument being passed as the directive.
this.argument, this.valueOrVariable) { final ArgumentContext argument;
assert(NAME != null);
/// The (optional) value being passed with the directive.
final InputValueContext value;
DirectiveContext(this.arrobaToken, this.nameToken, this.colonToken,
this.lParenToken, this.rParenToken, this.argument, this.value) {
assert(nameToken != null);
} }
/// Use [value] instead.
@deprecated
InputValueContext get valueOrVariable => value;
/// Use [arrobaToken] instead.
@deprecated
Token get ARROBA => arrobaToken;
/// Use [nameToken] instead.
@deprecated
Token get NAME => nameToken;
/// Use [colonToken] instead.
@deprecated
Token get COLON => colonToken;
/// Use [lParenToken] instead.
@deprecated
Token get LPAREN => lParenToken;
/// Use [rParenToken] instead.
@deprecated
Token get RPAREN => rParenToken;
@override @override
FileSpan get span { FileSpan get span {
var out = ARROBA.span.expand(NAME.span); var out = arrobaToken.span.expand(nameToken.span);
if (COLON != null) { if (colonToken != null) {
out = out.expand(COLON.span).expand(valueOrVariable.span); out = out.expand(colonToken.span).expand(value.span);
} else if (LPAREN != null) { } else if (lParenToken != null) {
out = out.expand(LPAREN.span).expand(argument.span).expand(RPAREN.span); out = out
.expand(lParenToken.span)
.expand(argument.span)
.expand(rParenToken.span);
} }
return out; return out;

View file

@ -2,7 +2,9 @@ import 'package:source_span/source_span.dart';
import 'definition.dart'; import 'definition.dart';
import 'node.dart'; import 'node.dart';
/// A GraphQL document.
class DocumentContext extends Node { class DocumentContext extends Node {
/// The top-level definitions in the document.
final List<DefinitionContext> definitions = []; final List<DefinitionContext> definitions = [];
@override @override

View file

@ -5,25 +5,35 @@ import 'field_name.dart';
import 'node.dart'; import 'node.dart';
import 'selection_set.dart'; import 'selection_set.dart';
/// A field in a GraphQL [SelectionSet].
class FieldContext extends Node { class FieldContext extends Node {
/// The name of this field.
final FieldNameContext fieldName; final FieldNameContext fieldName;
/// Any arguments this field expects.
final List<ArgumentContext> arguments = []; final List<ArgumentContext> arguments = [];
/// Any directives affixed to this field.
final List<DirectiveContext> directives = []; final List<DirectiveContext> directives = [];
/// The list of selections to resolve on an object.
final SelectionSetContext selectionSet; final SelectionSetContext selectionSet;
FieldContext(this.fieldName, [this.selectionSet]); FieldContext(this.fieldName, [this.selectionSet]);
@override @override
FileSpan get span { FileSpan get span {
if (selectionSet != null) if (selectionSet != null) {
return fieldName.span.expand(selectionSet.span); return fieldName.span.expand(selectionSet.span);
else if (directives.isNotEmpty) } else if (directives.isNotEmpty) {
return directives.fold<FileSpan>( return directives.fold<FileSpan>(
fieldName.span, (out, d) => out.expand(d.span)); fieldName.span, (out, d) => out.expand(d.span));
if (arguments.isNotEmpty) }
if (arguments.isNotEmpty) {
return arguments.fold<FileSpan>( return arguments.fold<FileSpan>(
fieldName.span, (out, a) => out.expand(a.span)); fieldName.span, (out, a) => out.expand(a.span));
else } else {
return fieldName.span; return fieldName.span;
}
} }
} }

View file

@ -3,16 +3,25 @@ import '../token.dart';
import 'alias.dart'; import 'alias.dart';
import 'node.dart'; import 'node.dart';
/// The name of a GraphQL [FieldContext], which may or may not be [alias]ed.
class FieldNameContext extends Node { class FieldNameContext extends Node {
final Token NAME; /// The source token.
final Token nameToken;
/// An (optional) alias for the field.
final AliasContext alias; final AliasContext alias;
FieldNameContext(this.NAME, [this.alias]) { FieldNameContext(this.nameToken, [this.alias]) {
assert(NAME != null || alias != null); assert(nameToken != null || alias != null);
} }
String get name => NAME?.text; /// Use [nameToken] instead.
@deprecated
Token get NAME => nameToken;
/// The [String] value of the [nameToken], if any.
String get name => nameToken?.text;
@override @override
FileSpan get span => alias?.span ?? NAME.span; FileSpan get span => alias?.span ?? nameToken.span;
} }

View file

@ -5,22 +5,43 @@ import 'package:source_span/source_span.dart';
import 'selection_set.dart'; import 'selection_set.dart';
import 'type_condition.dart'; import 'type_condition.dart';
/// A GraphQL query fragment definition.
class FragmentDefinitionContext extends ExecutableDefinitionContext { class FragmentDefinitionContext extends ExecutableDefinitionContext {
final Token FRAGMENT, NAME, ON; /// The source tokens.
final Token fragmentToken, nameToken, onToken;
/// The type to which this fragment applies.
final TypeConditionContext typeCondition; final TypeConditionContext typeCondition;
/// Any directives on the fragment.
final List<DirectiveContext> directives = []; final List<DirectiveContext> directives = [];
/// The selections to apply when the [typeCondition] is met.
final SelectionSetContext selectionSet; final SelectionSetContext selectionSet;
String get name => NAME.text; /// The [String] value of the [nameToken].
String get name => nameToken.text;
FragmentDefinitionContext( FragmentDefinitionContext(this.fragmentToken, this.nameToken, this.onToken,
this.FRAGMENT, this.NAME, this.ON, this.typeCondition, this.selectionSet); this.typeCondition, this.selectionSet);
/// Use [fragmentToken] instead.
@deprecated
Token get FRAGMENT => fragmentToken;
/// Use [nameToken] instead.
@deprecated
Token get NAME => nameToken;
/// Use [onToken] instead.
@deprecated
Token get ON => onToken;
@override @override
FileSpan get span { FileSpan get span {
var out = FRAGMENT.span var out = fragmentToken.span
.expand(NAME.span) .expand(nameToken.span)
.expand(ON.span) .expand(onToken.span)
.expand(typeCondition.span); .expand(typeCondition.span);
out = directives.fold<FileSpan>(out, (o, d) => o.expand(d.span)); out = directives.fold<FileSpan>(out, (o, d) => o.expand(d.span));
return out.expand(selectionSet.span); return out.expand(selectionSet.span);

View file

@ -3,17 +3,30 @@ import 'directive.dart';
import 'node.dart'; import 'node.dart';
import 'package:source_span/source_span.dart'; import 'package:source_span/source_span.dart';
/// A GraphQL fragment spread.
class FragmentSpreadContext extends Node { class FragmentSpreadContext extends Node {
final Token ELLIPSIS, NAME; /// The source tokens.
final Token ellipsisToken, nameToken;
/// Any directives affixed to this fragment spread.
final List<DirectiveContext> directives = []; final List<DirectiveContext> directives = [];
FragmentSpreadContext(this.ELLIPSIS, this.NAME); FragmentSpreadContext(this.ellipsisToken, this.nameToken);
String get name => NAME.text; /// The [String] value of the [nameToken].
String get name => nameToken.text;
/// Use [ellipsisToken] instead.
@deprecated
Token get ELLIPSIS => ellipsisToken;
/// Use [nameToken] instead.
@deprecated
Token get NAME => nameToken;
@override @override
FileSpan get span { FileSpan get span {
var out = ELLIPSIS.span.expand(NAME.span); var out = ellipsisToken.span.expand(nameToken.span);
if (directives.isEmpty) return out; if (directives.isEmpty) return out;
return directives.fold<FileSpan>(out, (o, d) => o.expand(d.span)); return directives.fold<FileSpan>(out, (o, d) => o.expand(d.span));
} }

View file

@ -5,18 +5,35 @@ import 'package:source_span/source_span.dart';
import 'selection_set.dart'; import 'selection_set.dart';
import 'type_condition.dart'; import 'type_condition.dart';
/// An inline fragment, which typically appears in a [SelectionSetContext].
class InlineFragmentContext extends Node { class InlineFragmentContext extends Node {
final Token ELLIPSIS, ON; /// The source tokens.
final Token ellipsisToken, onToken;
/// The type which this fragment matches.
final TypeConditionContext typeCondition; final TypeConditionContext typeCondition;
/// Any directives affixed to this inline fragment.
final List<DirectiveContext> directives = []; final List<DirectiveContext> directives = [];
/// The selections applied when the [typeCondition] is met.
final SelectionSetContext selectionSet; final SelectionSetContext selectionSet;
InlineFragmentContext( InlineFragmentContext(
this.ELLIPSIS, this.ON, this.typeCondition, this.selectionSet); this.ellipsisToken, this.onToken, this.typeCondition, this.selectionSet);
/// Use [ellipsisToken] instead.
@deprecated
Token get ELLIPSIS => ellipsisToken;
/// Use [onToken] instead.
@deprecated
Token get ON => onToken;
@override @override
FileSpan get span { FileSpan get span {
var out = ELLIPSIS.span.expand(ON.span).expand(typeCondition.span); var out =
ellipsisToken.span.expand(onToken.span).expand(typeCondition.span);
out = directives.fold<FileSpan>(out, (o, d) => o.expand(d.span)); out = directives.fold<FileSpan>(out, (o, d) => o.expand(d.span));
return out.expand(selectionSet.span); return out.expand(selectionSet.span);
} }

View file

@ -0,0 +1,7 @@
import 'node.dart';
/// Represents a value in GraphQL.
abstract class InputValueContext<T> extends Node {
/// Computes the value, relative to some set of [variables].
T computeValue(Map<String, dynamic> variables);
}

View file

@ -3,12 +3,29 @@ import 'node.dart';
import 'package:source_span/source_span.dart'; import 'package:source_span/source_span.dart';
import 'type.dart'; import 'type.dart';
/// Represents a type that holds a list of another type.
class ListTypeContext extends Node { class ListTypeContext extends Node {
final Token LBRACKET, RBRACKET; /// The source tokens.
final TypeContext type; final Token lBracketToken, rBracketToken;
ListTypeContext(this.LBRACKET, this.type, this.RBRACKET); /// The inner type.
final TypeContext innerType;
ListTypeContext(this.lBracketToken, this.innerType, this.rBracketToken);
/// Use [innerType] instead.
@deprecated
TypeContext get type => innerType;
/// Use [lBracketToken] instead.
@deprecated
Token get LBRACKET => lBracketToken;
/// Use [rBracketToken] instead.
@deprecated
Token get RBRACKET => rBracketToken;
@override @override
FileSpan get span => LBRACKET.span.expand(type.span).expand(RBRACKET.span); FileSpan get span =>
lBracketToken.span.expand(innerType.span).expand(rBracketToken.span);
} }

View file

@ -1,71 +1,105 @@
import 'package:source_span/source_span.dart'; import 'package:source_span/source_span.dart';
import '../token.dart'; import '../token.dart';
import 'input_value.dart';
import 'node.dart'; import 'node.dart';
import 'value.dart';
class NullValueContext extends ValueContext<Null> { /// A GraphQL `null` literal.
final Token NULL; class NullValueContext extends InputValueContext<Null> {
/// The source token.
final Token nullToken;
NullValueContext(this.NULL); NullValueContext(this.nullToken);
/// Use [nullToken] instead.
@deprecated
Token get NULL => nullToken;
@override @override
FileSpan get span => NULL.span; FileSpan get span => nullToken.span;
@override @override
Null get value => null; Null computeValue(Map<String, dynamic> variables) => null;
} }
class EnumValueContext extends ValueContext<String> { /// A GraphQL enumeration literal.
final Token NAME; class EnumValueContext extends InputValueContext<String> {
/// The source token.
final Token nameToken;
EnumValueContext(this.NAME); EnumValueContext(this.nameToken);
/// Use [nameToken] instead.
@deprecated
Token get NAME => nameToken;
@override @override
FileSpan get span => NAME.span; FileSpan get span => nameToken.span;
@override @override
String get value => NAME.span.text; String computeValue(Map<String, dynamic> variables) => nameToken.span.text;
} }
class ObjectValueContext extends ValueContext<Map<String, dynamic>> { /// A GraphQL object literal.
final Token LBRACE; class ObjectValueContext extends InputValueContext<Map<String, dynamic>> {
/// The source tokens.
final Token lBraceToken, rBraceToken;
/// The fields in the object.
final List<ObjectFieldContext> fields; final List<ObjectFieldContext> fields;
final Token RBRACE;
ObjectValueContext(this.LBRACE, this.fields, this.RBRACE); ObjectValueContext(this.lBraceToken, this.fields, this.rBraceToken);
/// Use [lBraceToken] instead.
Token get LBRACE => lBraceToken;
/// Use [rBraceToken] instead.
@deprecated
Token get RBRACE => rBraceToken;
@override @override
FileSpan get span { FileSpan get span {
var left = LBRACE.span; var left = lBraceToken.span;
for (var field in fields) { for (var field in fields) {
left = left.expand(field.span); left = left.expand(field.span);
} }
return left.expand(RBRACE.span); return left.expand(rBraceToken.span);
} }
@override @override
Map<String, dynamic> get value { Map<String, dynamic> computeValue(Map<String, dynamic> variables) {
if (fields.isEmpty) { if (fields.isEmpty) {
return <String, dynamic>{}; return <String, dynamic>{};
} else { } else {
return fields.fold<Map<String, dynamic>>(<String, dynamic>{}, return fields.fold<Map<String, dynamic>>(<String, dynamic>{},
(map, field) { (map, field) {
return map..[field.NAME.text] = field.value.value; return map
..[field.nameToken.text] = field.value.computeValue(variables);
}); });
} }
} }
} }
/// A field within an [ObjectValueContext].
class ObjectFieldContext extends Node { class ObjectFieldContext extends Node {
final Token NAME; /// The source tokens.
final Token COLON; final Token nameToken, colonToken;
final ValueContext value;
ObjectFieldContext(this.NAME, this.COLON, this.value); /// The associated value.
final InputValueContext value;
ObjectFieldContext(this.nameToken, this.colonToken, this.value);
/// Use [nameToken] instead.
@deprecated
Token get NAME => nameToken;
/// Use [colonToken] instead.
@deprecated
Token get COLON => colonToken;
@override @override
FileSpan get span => NAME.span.expand(COLON.span).expand(value.span); FileSpan get span =>
nameToken.span.expand(colonToken.span).expand(value.span);
} }

View file

@ -1,18 +1,21 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:source_span/source_span.dart'; import 'package:source_span/source_span.dart';
import '../token.dart'; import '../token.dart';
import 'value.dart'; import 'input_value.dart';
class NumberValueContext extends ValueContext<num> { /// A GraphQL number literal.
final Token NUMBER; class NumberValueContext extends InputValueContext<num> {
/// The source token.
final Token numberToken;
NumberValueContext(this.NUMBER); NumberValueContext(this.numberToken);
/// The [num] value of the [numberToken].
num get numberValue { num get numberValue {
var text = NUMBER.text; var text = numberToken.text;
if (!text.contains('E') && !text.contains('e')) if (!text.contains('E') && !text.contains('e')) {
return num.parse(text); return num.parse(text);
else { } else {
var split = text.split(text.contains('E') ? 'E' : 'e'); var split = text.split(text.contains('E') ? 'E' : 'e');
var base = num.parse(split[0]); var base = num.parse(split[0]);
var exp = num.parse(split[1]); var exp = num.parse(split[1]);
@ -20,9 +23,13 @@ class NumberValueContext extends ValueContext<num> {
} }
} }
@override /// Use [numberToken] instead.
num get value => numberValue; @deprecated
Token get NUMBER => numberToken;
@override @override
FileSpan get span => NUMBER.span; FileSpan get span => numberToken.span;
@override
num computeValue(Map<String, dynamic> variables) => numberValue;
} }

View file

@ -1,37 +1,58 @@
import 'package:source_span/source_span.dart'; 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 'selection_set.dart'; import 'selection_set.dart';
import 'variable_definitions.dart'; import 'variable_definitions.dart';
/// An executable GraphQL operation definition.
class OperationDefinitionContext extends ExecutableDefinitionContext { class OperationDefinitionContext extends ExecutableDefinitionContext {
final Token TYPE, NAME; /// The source tokens.
final Token typeToken, nameToken;
/// The variables defined in the operation.
final VariableDefinitionsContext variableDefinitions; final VariableDefinitionsContext variableDefinitions;
/// Any directives affixed to this operation.
final List<DirectiveContext> directives = []; final List<DirectiveContext> directives = [];
/// The selections to be applied to an object resolved in this operation.
final SelectionSetContext selectionSet; final SelectionSetContext selectionSet;
bool get isMutation => TYPE?.text == 'mutation'; /// Whether this operation is a `mutation`.
bool get isMutation => typeToken?.text == 'mutation';
bool get isSubscription => TYPE?.text == 'subscription'; /// Whether this operation is a `subscription`.
bool get isSubscription => typeToken?.text == 'subscription';
bool get isQuery => TYPE?.text == 'query' || TYPE == null; /// Whether this operation is a `query`.
bool get isQuery => typeToken?.text == 'query' || typeToken == null;
String get name => NAME?.text; /// The [String] value of the [nameToken].
String get name => nameToken?.text;
OperationDefinitionContext( /// Use [nameToken] instead.
this.TYPE, this.NAME, this.variableDefinitions, this.selectionSet) { @deprecated
assert(TYPE == null || Token get NAME => nameToken;
TYPE.text == 'query' ||
TYPE.text == 'mutation' || /// Use [typeToken] instead.
TYPE.text == 'subscription'); @deprecated
Token get TYPE => typeToken;
OperationDefinitionContext(this.typeToken, this.nameToken,
this.variableDefinitions, this.selectionSet) {
assert(typeToken == null ||
typeToken.text == 'query' ||
typeToken.text == 'mutation' ||
typeToken.text == 'subscription');
} }
@override @override
FileSpan get span { FileSpan get span {
if (TYPE == null) return selectionSet.span; if (typeToken == null) return selectionSet.span;
var out = NAME == null ? TYPE.span : TYPE.span.expand(NAME.span); var out = nameToken == null
? typeToken.span
: typeToken.span.expand(nameToken.span);
out = directives.fold<FileSpan>(out, (o, d) => o.expand(d.span)); out = directives.fold<FileSpan>(out, (o, d) => o.expand(d.span));
return out.expand(selectionSet.span); return out.expand(selectionSet.span);
} }

View file

@ -4,20 +4,33 @@ import '../token.dart';
import 'node.dart'; import 'node.dart';
import 'selection.dart'; import 'selection.dart';
/// A set of GraphQL selections - fields, fragments, or inline fragments.
class SelectionSetContext extends Node { class SelectionSetContext extends Node {
final Token LBRACE, RBRACE; /// The source tokens.
final Token lBraceToken, rBraceToken;
/// The selections to be applied.
final List<SelectionContext> selections = []; final List<SelectionContext> selections = [];
SelectionSetContext(this.LBRACE, this.RBRACE); SelectionSetContext(this.lBraceToken, this.rBraceToken);
/// A synthetic [SelectionSetContext] produced from a set of [selections].
factory SelectionSetContext.merged(List<SelectionContext> selections) = factory SelectionSetContext.merged(List<SelectionContext> selections) =
_MergedSelectionSetContext; _MergedSelectionSetContext;
/// Use [lBraceToken] instead.
@deprecated
Token get LBRACE => lBraceToken;
/// Use [rBraceToken] instead.
@deprecated
Token get RBRACE => rBraceToken;
@override @override
FileSpan get span { FileSpan get span {
var out = var out = selections.fold<FileSpan>(
selections.fold<FileSpan>(LBRACE.span, (out, s) => out.expand(s.span)); lBraceToken.span, (out, s) => out.expand(s.span));
return out.expand(RBRACE.span); return out.expand(rBraceToken.span);
} }
} }

View file

@ -3,28 +3,37 @@ import 'package:source_span/source_span.dart';
import '../syntax_error.dart'; import '../syntax_error.dart';
import '../token.dart'; import '../token.dart';
import 'value.dart'; import 'input_value.dart';
class StringValueContext extends ValueContext { /// A GraphQL string value literal.
final Token STRING; class StringValueContext extends InputValueContext<String> {
/// The source token.
final Token stringToken;
/// Whether this is a block string.
final bool isBlockString; final bool isBlockString;
StringValueContext(this.STRING, {this.isBlockString: false}); StringValueContext(this.stringToken, {this.isBlockString = false});
@override @override
FileSpan get span => STRING.span; FileSpan get span => stringToken.span;
/// Use [stringToken] instead.
@deprecated
Token get STRING => stringToken;
/// The [String] value of the [stringToken].
String get stringValue { String get stringValue {
String text; String text;
if (!isBlockString) { if (!isBlockString) {
text = STRING.text.substring(1, STRING.text.length - 1); text = stringToken.text.substring(1, stringToken.text.length - 1);
} else { } else {
text = STRING.text.substring(3, STRING.text.length - 3).trim(); text = stringToken.text.substring(3, stringToken.text.length - 3).trim();
} }
var codeUnits = text.codeUnits; var codeUnits = text.codeUnits;
var buf = new StringBuffer(); var buf = StringBuffer();
for (int i = 0; i < codeUnits.length; i++) { for (int i = 0; i < codeUnits.length; i++) {
var ch = codeUnits[i]; var ch = codeUnits[i];
@ -35,9 +44,9 @@ class StringValueContext extends ValueContext {
c2 = codeUnits[++i], c2 = codeUnits[++i],
c3 = codeUnits[++i], c3 = codeUnits[++i],
c4 = codeUnits[++i]; c4 = codeUnits[++i];
var hexString = new String.fromCharCodes([c1, c2, c3, c4]); var hexString = String.fromCharCodes([c1, c2, c3, c4]);
var hexNumber = int.parse(hexString, radix: 16); var hexNumber = int.parse(hexString, radix: 16);
buf.write(new String.fromCharCode(hexNumber)); buf.write(String.fromCharCode(hexNumber));
continue; continue;
} }
@ -63,8 +72,9 @@ class StringValueContext extends ValueContext {
default: default:
buf.writeCharCode(next); buf.writeCharCode(next);
} }
} else } else {
throw new SyntaxError('Unexpected "\\" in string literal.', span); throw SyntaxError('Unexpected "\\" in string literal.', span);
}
} else { } else {
buf.writeCharCode(ch); buf.writeCharCode(ch);
} }
@ -74,5 +84,5 @@ class StringValueContext extends ValueContext {
} }
@override @override
get value => stringValue; String computeValue(Map<String, dynamic> variables) => stringValue;
} }

View file

@ -4,20 +4,31 @@ import 'list_type.dart';
import 'node.dart'; import 'node.dart';
import 'type_name.dart'; import 'type_name.dart';
/// A GraphQL type node.
class TypeContext extends Node { class TypeContext extends Node {
final Token EXCLAMATION; /// A source token, present in a nullable type literal.
final Token exclamationToken;
/// The name of the referenced type.
final TypeNameContext typeName; final TypeNameContext typeName;
/// A list type that is being referenced.
final ListTypeContext listType; final ListTypeContext listType;
bool get isNullable => EXCLAMATION == null; /// Whether the type is nullable.
bool get isNullable => exclamationToken == null;
TypeContext(this.typeName, this.listType, [this.EXCLAMATION]) { TypeContext(this.typeName, this.listType, [this.exclamationToken]) {
assert(typeName != null || listType != null); assert(typeName != null || listType != null);
} }
/// Use [exclamationToken] instead.
@deprecated
Token get EXCLAMATION => exclamationToken;
@override @override
FileSpan get span { FileSpan get span {
var out = typeName?.span ?? listType.span; var out = typeName?.span ?? listType.span;
return EXCLAMATION != null ? out.expand(EXCLAMATION.span) : out; return exclamationToken != null ? out.expand(exclamationToken.span) : out;
} }
} }

View file

@ -2,13 +2,20 @@ import 'node.dart';
import 'package:source_span/source_span.dart'; import 'package:source_span/source_span.dart';
import '../token.dart'; import '../token.dart';
/// The name of a GraphQL type.
class TypeNameContext extends Node { class TypeNameContext extends Node {
final Token NAME; /// The source token.
final Token nameToken;
String get name => NAME.text; TypeNameContext(this.nameToken);
/// Use [nameToken] instead.
@deprecated
Token get NAME => nameToken;
/// The [String] value of the [nameToken].
String get name => nameToken.text;
@override @override
FileSpan get span => NAME.span; FileSpan get span => nameToken.span;
TypeNameContext(this.NAME);
} }

View file

@ -1,5 +0,0 @@
import 'node.dart';
abstract class ValueContext<T> extends Node {
T get value;
}

View file

@ -1,16 +0,0 @@
import 'node.dart';
import 'package:source_span/source_span.dart';
import 'value.dart';
import 'variable.dart';
class ValueOrVariableContext extends Node {
final ValueContext value;
final VariableContext variable;
ValueOrVariableContext(this.value, this.variable) {
assert(value != null || variable != null);
}
@override
FileSpan get span => value?.span ?? variable.span;
}

View file

@ -1,15 +1,28 @@
import '../token.dart';
import 'node.dart';
import 'package:source_span/source_span.dart'; import 'package:source_span/source_span.dart';
import '../token.dart';
import 'input_value.dart';
class VariableContext extends Node { /// A variable reference in GraphQL.
final Token DOLLAR, NAME; class VariableContext extends InputValueContext<Object> {
/// The source tokens.
final Token dollarToken, nameToken;
VariableContext(this.DOLLAR, this.NAME); VariableContext(this.dollarToken, this.nameToken);
String get name => NAME.text; /// The [String] value of the [nameToken].
String get name => nameToken.text;
/// Use [dollarToken] instead.
@deprecated
Token get DOLLAR => dollarToken;
/// Use [nameToken] instead.
@deprecated
Token get NAME => nameToken;
@override @override
FileSpan get span => DOLLAR.span.expand(NAME.span); FileSpan get span => dollarToken.span.expand(nameToken.span);
// new FileSpan(DOLLAR?.span?.start, NAME?.span?.end, toSource());
@override
Object computeValue(Map<String, dynamic> variables) => variables[name];
} }

View file

@ -5,15 +5,27 @@ import 'package:source_span/source_span.dart';
import 'type.dart'; import 'type.dart';
import 'variable.dart'; import 'variable.dart';
/// A single variable definition.
class VariableDefinitionContext extends Node { class VariableDefinitionContext extends Node {
final Token COLON; /// The source token.
final Token colonToken;
/// The declared variable.
final VariableContext variable; final VariableContext variable;
/// The type of the variable.
final TypeContext type; final TypeContext type;
/// The default value of the variable.
final DefaultValueContext defaultValue; final DefaultValueContext defaultValue;
VariableDefinitionContext(this.variable, this.COLON, this.type, VariableDefinitionContext(this.variable, this.colonToken, this.type,
[this.defaultValue]); [this.defaultValue]);
/// Use [colonToken] instead.
@deprecated
Token get COLON => colonToken;
@override @override
FileSpan get span => variable.span.expand(defaultValue?.span ?? type.span); FileSpan get span => variable.span.expand(defaultValue?.span ?? type.span);
} }

View file

@ -3,16 +3,28 @@ import 'node.dart';
import 'package:source_span/source_span.dart'; import 'package:source_span/source_span.dart';
import 'variable_definition.dart'; import 'variable_definition.dart';
/// A set of variable definitions in a GraphQL operation.
class VariableDefinitionsContext extends Node { class VariableDefinitionsContext extends Node {
final Token LPAREN, RPAREN; /// The source tokens.
final Token lParenToken, rParenToken;
/// The variables defined in this node.
final List<VariableDefinitionContext> variableDefinitions = []; final List<VariableDefinitionContext> variableDefinitions = [];
VariableDefinitionsContext(this.LPAREN, this.RPAREN); VariableDefinitionsContext(this.lParenToken, this.rParenToken);
/// Use [lParenToken] instead.
@deprecated
Token get LPAREN => lParenToken;
/// Use [rParenToken] instead.
@deprecated
Token get RPAREN => rParenToken;
@override @override
FileSpan get span { FileSpan get span {
var out = variableDefinitions.fold<FileSpan>( var out = variableDefinitions.fold<FileSpan>(
LPAREN.span, (o, v) => o.expand(v.span)); lParenToken.span, (o, v) => o.expand(v.span));
return out.expand(RPAREN.span); return out.expand(rParenToken.span);
} }
} }

View file

@ -4,14 +4,14 @@ import 'syntax_error.dart';
import 'token.dart'; import 'token.dart';
import 'token_type.dart'; import 'token_type.dart';
final RegExp _comment = new RegExp(r'#[^\n]*'); final RegExp _comment = RegExp(r'#[^\n]*');
final RegExp _whitespace = new RegExp('[ \t\n\r]+'); final RegExp _whitespace = RegExp('[ \t\n\r]+');
// final RegExp _boolean = new RegExp(r'true|false'); // final RegExp _boolean = RegExp(r'true|false');
final RegExp _number = new RegExp(r'-?[0-9]+(\.[0-9]+)?(E|e(\+|-)?[0-9]+)?'); final RegExp _number = RegExp(r'-?[0-9]+(\.[0-9]+)?(E|e(\+|-)?[0-9]+)?');
final RegExp _string = new RegExp( final RegExp _string = RegExp(
r'"((\\(["\\/bfnrt]|(u[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])))|([^"\\]))*"'); r'"((\\(["\\/bfnrt]|(u[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])))|([^"\\]))*"');
final RegExp _blockString = new RegExp(r'"""(([^"])|(\\"""))*"""'); final RegExp _blockString = RegExp(r'"""(([^"])|(\\"""))*"""');
final RegExp _name = new RegExp(r'[_A-Za-z][_0-9A-Za-z]*'); final RegExp _name = RegExp(r'[_A-Za-z][_0-9A-Za-z]*');
final Map<Pattern, TokenType> _patterns = { final Map<Pattern, TokenType> _patterns = {
'@': TokenType.ARROBA, '@': TokenType.ARROBA,
@ -42,7 +42,7 @@ final Map<Pattern, TokenType> _patterns = {
List<Token> scan(String text, {sourceUrl}) { List<Token> scan(String text, {sourceUrl}) {
List<Token> out = []; List<Token> out = [];
var scanner = new SpanScanner(text, sourceUrl: sourceUrl); var scanner = SpanScanner(text, sourceUrl: sourceUrl);
while (!scanner.isDone) { while (!scanner.isDone) {
List<Token> potential = []; List<Token> potential = [];
@ -54,14 +54,14 @@ List<Token> scan(String text, {sourceUrl}) {
for (var pattern in _patterns.keys) { for (var pattern in _patterns.keys) {
if (scanner.matches(pattern)) { if (scanner.matches(pattern)) {
potential.add(new Token( potential.add(
_patterns[pattern], scanner.lastMatch[0], scanner.lastSpan)); Token(_patterns[pattern], scanner.lastMatch[0], scanner.lastSpan));
} }
} }
if (potential.isEmpty) { if (potential.isEmpty) {
var ch = new String.fromCharCode(scanner.readChar()); var ch = String.fromCharCode(scanner.readChar());
throw new SyntaxError("Unexpected token '$ch'.", scanner.emptySpan); throw SyntaxError("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

@ -60,7 +60,7 @@ class Parser {
def = parseDefinition(); def = parseDefinition();
} }
return new DocumentContext()..definitions.addAll(defs); return DocumentContext()..definitions.addAll(defs);
} }
DefinitionContext parseDefinition() => DefinitionContext parseDefinition() =>
@ -68,9 +68,9 @@ class Parser {
OperationDefinitionContext parseOperationDefinition() { OperationDefinitionContext parseOperationDefinition() {
var selectionSet = parseSelectionSet(); var selectionSet = parseSelectionSet();
if (selectionSet != null) if (selectionSet != null) {
return new OperationDefinitionContext(null, null, null, selectionSet); return OperationDefinitionContext(null, null, null, selectionSet);
else { } else {
if (nextName('mutation') || if (nextName('mutation') ||
nextName('query') || nextName('query') ||
nextName('subscription')) { nextName('subscription')) {
@ -79,18 +79,18 @@ class Parser {
var variables = parseVariableDefinitions(); var variables = parseVariableDefinitions();
var dirs = parseDirectives(); var dirs = parseDirectives();
var selectionSet = parseSelectionSet(); var selectionSet = parseSelectionSet();
if (selectionSet != null) if (selectionSet != null) {
return new OperationDefinitionContext( return OperationDefinitionContext(TYPE, NAME, variables, selectionSet)
TYPE, NAME, variables, selectionSet)
..directives.addAll(dirs); ..directives.addAll(dirs);
else { } else {
errors.add(new SyntaxError( errors.add(SyntaxError(
'Missing selection set in fragment definition.', 'Missing selection set in fragment definition.',
NAME?.span ?? TYPE.span)); NAME?.span ?? TYPE.span));
return null; return null;
} }
} else } else {
return null; return null;
}
} }
} }
@ -105,36 +105,37 @@ class Parser {
if (typeCondition != null) { if (typeCondition != null) {
var dirs = parseDirectives(); var dirs = parseDirectives();
var selectionSet = parseSelectionSet(); var selectionSet = parseSelectionSet();
if (selectionSet != null) if (selectionSet != null) {
return new FragmentDefinitionContext( return FragmentDefinitionContext(
FRAGMENT, NAME, ON, typeCondition, selectionSet) FRAGMENT, NAME, ON, typeCondition, selectionSet)
..directives.addAll(dirs); ..directives.addAll(dirs);
else { } else {
errors.add(new SyntaxError( errors.add(SyntaxError(
'Expected selection set in fragment definition.', 'Expected selection set in fragment definition.',
typeCondition.span)); typeCondition.span));
return null; return null;
} }
} else { } else {
errors.add(new SyntaxError( errors.add(SyntaxError(
'Expected type condition after "on" in fragment definition.', 'Expected type condition after "on" in fragment definition.',
ON.span)); ON.span));
return null; return null;
} }
} else { } else {
errors.add(new SyntaxError( errors.add(SyntaxError(
'Expected "on" after name "${NAME.text}" in fragment definition.', 'Expected "on" after name "${NAME.text}" in fragment definition.',
NAME.span)); NAME.span));
return null; return null;
} }
} else { } else {
errors.add(new SyntaxError( errors.add(SyntaxError(
'Expected name after "fragment" in fragment definition.', 'Expected name after "fragment" in fragment definition.',
FRAGMENT.span)); FRAGMENT.span));
return null; return null;
} }
} else } else {
return null; return null;
}
} }
FragmentSpreadContext parseFragmentSpread() { FragmentSpreadContext parseFragmentSpread() {
@ -142,14 +143,15 @@ class Parser {
var ELLIPSIS = current; var ELLIPSIS = current;
if (next(TokenType.NAME, exclude: ['on'])) { if (next(TokenType.NAME, exclude: ['on'])) {
var NAME = current; var NAME = current;
return new FragmentSpreadContext(ELLIPSIS, NAME) return FragmentSpreadContext(ELLIPSIS, NAME)
..directives.addAll(parseDirectives()); ..directives.addAll(parseDirectives());
} else { } else {
_index--; _index--;
return null; return null;
} }
} else } else {
return null; return null;
}
} }
InlineFragmentContext parseInlineFragment() { InlineFragmentContext parseInlineFragment() {
@ -162,11 +164,11 @@ class Parser {
var directives = parseDirectives(); var directives = parseDirectives();
var selectionSet = parseSelectionSet(); var selectionSet = parseSelectionSet();
if (selectionSet != null) { if (selectionSet != null) {
return new InlineFragmentContext( return InlineFragmentContext(
ELLIPSIS, ON, typeCondition, selectionSet) ELLIPSIS, ON, typeCondition, selectionSet)
..directives.addAll(directives); ..directives.addAll(directives);
} else { } else {
errors.add(new SyntaxError( errors.add(SyntaxError(
'Missing selection set in inline fragment.', 'Missing selection set in inline fragment.',
directives.isEmpty directives.isEmpty
? typeCondition.span ? typeCondition.span
@ -174,18 +176,19 @@ class Parser {
return null; return null;
} }
} else { } else {
errors.add(new SyntaxError( errors.add(SyntaxError(
'Missing type condition after "on" in inline fragment.', 'Missing type condition after "on" in inline fragment.',
ON.span)); ON.span));
return null; return null;
} }
} else { } else {
errors.add(new SyntaxError( errors.add(SyntaxError(
'Missing "on" after "..." in inline fragment.', ELLIPSIS.span)); 'Missing "on" after "..." in inline fragment.', ELLIPSIS.span));
return null; return null;
} }
} else } else {
return null; return null;
}
} }
SelectionSetContext parseSelectionSet() { SelectionSetContext parseSelectionSet() {
@ -201,29 +204,30 @@ class Parser {
} }
eatCommas(); eatCommas();
if (next(TokenType.RBRACE)) if (next(TokenType.RBRACE)) {
return new SelectionSetContext(LBRACE, current) return SelectionSetContext(LBRACE, current)
..selections.addAll(selections); ..selections.addAll(selections);
else { } else {
errors.add(new SyntaxError('Missing "}" after selection set.', errors.add(SyntaxError('Missing "}" after selection set.',
selections.isEmpty ? LBRACE.span : selections.last.span)); selections.isEmpty ? LBRACE.span : selections.last.span));
return null; return null;
} }
} else } else {
return null; return null;
}
} }
SelectionContext parseSelection() { SelectionContext parseSelection() {
var field = parseField(); var field = parseField();
if (field != null) return new SelectionContext(field); if (field != null) return SelectionContext(field);
var fragmentSpread = parseFragmentSpread(); var fragmentSpread = parseFragmentSpread();
if (fragmentSpread != null) if (fragmentSpread != null) return SelectionContext(null, fragmentSpread);
return new SelectionContext(null, fragmentSpread);
var inlineFragment = parseInlineFragment(); var inlineFragment = parseInlineFragment();
if (inlineFragment != null) if (inlineFragment != null) {
return new SelectionContext(null, null, inlineFragment); return SelectionContext(null, null, inlineFragment);
else } else {
return null; return null;
}
} }
FieldContext parseField() { FieldContext parseField() {
@ -232,11 +236,12 @@ class Parser {
var args = parseArguments(); var args = parseArguments();
var directives = parseDirectives(); var directives = parseDirectives();
var selectionSet = parseSelectionSet(); var selectionSet = parseSelectionSet();
return new FieldContext(fieldName, selectionSet) return FieldContext(fieldName, selectionSet)
..arguments.addAll(args ?? <ArgumentContext>[]) ..arguments.addAll(args ?? <ArgumentContext>[])
..directives.addAll(directives); ..directives.addAll(directives);
} else } else {
return null; return null;
}
} }
FieldNameContext parseFieldName() { FieldNameContext parseFieldName() {
@ -244,18 +249,19 @@ class Parser {
var NAME1 = current; var NAME1 = current;
if (next(TokenType.COLON)) { if (next(TokenType.COLON)) {
var COLON = current; var COLON = current;
if (next(TokenType.NAME)) if (next(TokenType.NAME)) {
return new FieldNameContext( return FieldNameContext(null, AliasContext(NAME1, COLON, current));
null, new AliasContext(NAME1, COLON, current)); } else {
else { errors.add(
errors.add(new SyntaxError( SyntaxError('Missing name after colon in alias.', COLON.span));
'Missing name after colon in alias.', COLON.span));
return null; return null;
} }
} else } else {
return new FieldNameContext(NAME1); return FieldNameContext(NAME1);
} else }
} else {
return null; return null;
}
} }
VariableDefinitionsContext parseVariableDefinitions() { VariableDefinitionsContext parseVariableDefinitions() {
@ -270,16 +276,17 @@ class Parser {
def = parseVariableDefinition(); def = parseVariableDefinition();
} }
if (next(TokenType.RPAREN)) if (next(TokenType.RPAREN)) {
return new VariableDefinitionsContext(LPAREN, current) return VariableDefinitionsContext(LPAREN, current)
..variableDefinitions.addAll(defs); ..variableDefinitions.addAll(defs);
else { } else {
errors.add(new SyntaxError( errors.add(SyntaxError(
'Missing ")" after variable definitions.', LPAREN.span)); 'Missing ")" after variable definitions.', LPAREN.span));
return null; return null;
} }
} else } else {
return null; return null;
}
} }
VariableDefinitionContext parseVariableDefinition() { VariableDefinitionContext parseVariableDefinition() {
@ -290,32 +297,33 @@ class Parser {
var type = parseType(); var type = parseType();
if (type != null) { if (type != null) {
var defaultValue = parseDefaultValue(); var defaultValue = parseDefaultValue();
return new VariableDefinitionContext( return VariableDefinitionContext(variable, COLON, type, defaultValue);
variable, COLON, type, defaultValue);
} else { } else {
errors.add(new SyntaxError( errors.add(
'Missing type in variable definition.', COLON.span)); SyntaxError('Missing type in variable definition.', COLON.span));
return null; return null;
} }
} else { } else {
errors.add(new SyntaxError( errors.add(
'Missing ":" in variable definition.', variable.span)); SyntaxError('Missing ":" in variable definition.', variable.span));
return null; return null;
} }
} else } else {
return null; return null;
}
} }
TypeContext parseType() { TypeContext parseType() {
var name = parseTypeName(); var name = parseTypeName();
if (name != null) { if (name != null) {
return new TypeContext(name, null, maybe(TokenType.EXCLAMATION)); return TypeContext(name, null, maybe(TokenType.EXCLAMATION));
} else { } else {
var listType = parseListType(); var listType = parseListType();
if (listType != null) { if (listType != null) {
return new TypeContext(null, listType, maybe(TokenType.EXCLAMATION)); return TypeContext(null, listType, maybe(TokenType.EXCLAMATION));
} else } else {
return null; return null;
}
} }
} }
@ -325,17 +333,18 @@ class Parser {
var type = parseType(); var type = parseType();
if (type != null) { if (type != null) {
if (next(TokenType.RBRACKET)) { if (next(TokenType.RBRACKET)) {
return new ListTypeContext(LBRACKET, type, current); return ListTypeContext(LBRACKET, type, current);
} else { } else {
errors.add(new SyntaxError('Missing "]" in list type.', type.span)); errors.add(SyntaxError('Missing "]" in list type.', type.span));
return null; return null;
} }
} else { } else {
errors.add(new SyntaxError('Missing type after "[".', LBRACKET.span)); errors.add(SyntaxError('Missing type after "[".', LBRACKET.span));
return null; return null;
} }
} else } else {
return null; return null;
}
} }
List<DirectiveContext> parseDirectives() { List<DirectiveContext> parseDirectives() {
@ -357,12 +366,11 @@ class Parser {
if (next(TokenType.COLON)) { if (next(TokenType.COLON)) {
var COLON = current; var COLON = current;
var val = parseValueOrVariable(); var val = parseInputValue();
if (val != null) if (val != null) {
return new DirectiveContext( return DirectiveContext(ARROBA, NAME, COLON, null, null, null, val);
ARROBA, NAME, COLON, null, null, null, val); } else {
else { errors.add(SyntaxError(
errors.add(new SyntaxError(
'Missing value or variable in directive after colon.', 'Missing value or variable in directive after colon.',
COLON.span)); COLON.span));
return null; return null;
@ -372,27 +380,27 @@ class Parser {
var arg = parseArgument(); var arg = parseArgument();
if (arg != null) { if (arg != null) {
if (next(TokenType.RPAREN)) { if (next(TokenType.RPAREN)) {
return new DirectiveContext( return DirectiveContext(
ARROBA, NAME, null, LPAREN, current, arg, null); ARROBA, NAME, null, LPAREN, current, arg, null);
} else { } else {
errors.add( errors.add(SyntaxError('Missing \')\'', arg.value.span));
new SyntaxError('Missing \')\'', arg.valueOrVariable.span));
return null; return null;
} }
} else { } else {
errors.add( errors.add(
new SyntaxError('Missing argument in directive.', LPAREN.span)); SyntaxError('Missing argument in directive.', LPAREN.span));
return null; return null;
} }
} else } else {
return new DirectiveContext( return DirectiveContext(ARROBA, NAME, null, null, null, null, null);
ARROBA, NAME, null, null, null, null, null); }
} else { } else {
errors.add(new SyntaxError('Missing name for directive.', ARROBA.span)); errors.add(SyntaxError('Missing name for directive.', ARROBA.span));
return null; return null;
} }
} else } else {
return null; return null;
}
} }
List<ArgumentContext> parseArguments() { List<ArgumentContext> parseArguments() {
@ -407,15 +415,15 @@ class Parser {
arg = parseArgument(); arg = parseArgument();
} }
if (next(TokenType.RPAREN)) if (next(TokenType.RPAREN)) {
return out; return out;
else { } else {
errors errors.add(SyntaxError('Missing ")" in argument list.', LPAREN.span));
.add(new SyntaxError('Missing ")" in argument list.', LPAREN.span));
return null; return null;
} }
} else } else {
return []; return [];
}
} }
ArgumentContext parseArgument() { ArgumentContext parseArgument() {
@ -423,134 +431,135 @@ class Parser {
var NAME = current; var NAME = current;
if (next(TokenType.COLON)) { if (next(TokenType.COLON)) {
var COLON = current; var COLON = current;
var val = parseValueOrVariable(); var val = parseInputValue();
if (val != null) if (val != null) {
return new ArgumentContext(NAME, COLON, val); return ArgumentContext(NAME, COLON, val);
else { } else {
errors.add(new SyntaxError( errors.add(SyntaxError(
'Missing value or variable in argument.', COLON.span)); 'Missing value or variable in argument.', COLON.span));
return null; return null;
} }
} else { } else {
errors.add(new SyntaxError( errors.add(
'Missing colon after name in argument.', NAME.span)); SyntaxError('Missing colon after name in argument.', NAME.span));
return null; return null;
} }
} else } else {
return null; 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;
} }
} }
/// Use [parseInputValue] instead.
@deprecated
InputValueContext parseValueOrVariable() => parseInputValue();
VariableContext parseVariable() { VariableContext parseVariable() {
if (next(TokenType.DOLLAR)) { if (next(TokenType.DOLLAR)) {
var DOLLAR = current; var DOLLAR = current;
if (next(TokenType.NAME)) if (next(TokenType.NAME)) {
return new VariableContext(DOLLAR, current); return VariableContext(DOLLAR, current);
else { } else {
errors.add(new SyntaxError( errors.add(SyntaxError(
'Missing name for variable; found a lone "\$" instead.', 'Missing name for variable; found a lone "\$" instead.',
DOLLAR.span)); DOLLAR.span));
return null; return null;
} }
} else } else {
return null; return null;
}
} }
DefaultValueContext parseDefaultValue() { DefaultValueContext parseDefaultValue() {
if (next(TokenType.EQUALS)) { if (next(TokenType.EQUALS)) {
var EQUALS = current; var EQUALS = current;
var value = parseValue(); var value = parseInputValue();
if (value != null) { if (value != null) {
return new DefaultValueContext(EQUALS, value); return DefaultValueContext(EQUALS, value);
} else { } else {
errors errors.add(SyntaxError('Missing value after "=" sign.', EQUALS.span));
.add(new SyntaxError('Missing value after "=" sign.', EQUALS.span));
return null; return null;
} }
} else } else {
return null; return null;
}
} }
TypeConditionContext parseTypeCondition() { TypeConditionContext parseTypeCondition() {
var name = parseTypeName(); var name = parseTypeName();
if (name != null) if (name != null) {
return new TypeConditionContext(name); return TypeConditionContext(name);
else } else {
return null; return null;
}
} }
TypeNameContext parseTypeName() { TypeNameContext parseTypeName() {
if (next(TokenType.NAME)) { if (next(TokenType.NAME)) {
return new TypeNameContext(current); return TypeNameContext(current);
} else } else {
return null; return null;
}
} }
ValueContext parseValue() { /// Use [parseInputValue] instead.
return (parseNumberValue() ?? @deprecated
InputValueContext parseValue() => parseInputValue();
InputValueContext parseInputValue() {
return (parseVariable() ??
parseNumberValue() ??
parseStringValue() ?? parseStringValue() ??
parseBooleanValue() ?? parseBooleanValue() ??
parseNullValue() ?? parseNullValue() ??
parseEnumValue() ?? parseEnumValue() ??
parseListValue() ?? parseListValue() ??
parseObjectValue()) as ValueContext; parseObjectValue()) as InputValueContext;
} }
StringValueContext parseStringValue() => next(TokenType.STRING) StringValueContext parseStringValue() => next(TokenType.STRING)
? new StringValueContext(current) ? StringValueContext(current)
: (next(TokenType.BLOCK_STRING) : (next(TokenType.BLOCK_STRING)
? new StringValueContext(current, isBlockString: true) ? StringValueContext(current, isBlockString: true)
: null); : null);
NumberValueContext parseNumberValue() => NumberValueContext parseNumberValue() =>
next(TokenType.NUMBER) ? new NumberValueContext(current) : null; next(TokenType.NUMBER) ? NumberValueContext(current) : null;
BooleanValueContext parseBooleanValue() => BooleanValueContext parseBooleanValue() =>
(nextName('true') || nextName('false')) (nextName('true') || nextName('false'))
? new BooleanValueContext(current) ? BooleanValueContext(current)
: null; : null;
EnumValueContext parseEnumValue() => EnumValueContext parseEnumValue() =>
next(TokenType.NAME) ? new EnumValueContext(current) : null; next(TokenType.NAME) ? EnumValueContext(current) : null;
NullValueContext parseNullValue() => NullValueContext parseNullValue() =>
nextName('null') ? new NullValueContext(current) : null; nextName('null') ? NullValueContext(current) : null;
ListValueContext parseListValue() { ListValueContext parseListValue() {
if (next(TokenType.LBRACKET)) { if (next(TokenType.LBRACKET)) {
var LBRACKET = current; var LBRACKET = current;
var lastSpan = LBRACKET.span; var lastSpan = LBRACKET.span;
List<ValueContext> values = []; List<InputValueContext> values = [];
ValueContext value = parseValue(); var value = parseInputValue();
while (value != null) { while (value != null) {
lastSpan = value.span; lastSpan = value.span;
values.add(value); values.add(value);
eatCommas(); eatCommas();
value = parseValue(); value = parseInputValue();
} }
eatCommas(); eatCommas();
if (next(TokenType.RBRACKET)) { if (next(TokenType.RBRACKET)) {
return new ListValueContext(LBRACKET, current)..values.addAll(values); return ListValueContext(LBRACKET, current)..values.addAll(values);
} else { } else {
errors.add(new SyntaxError('Unterminated list literal.', lastSpan)); errors.add(SyntaxError('Unterminated list literal.', lastSpan));
return null; return null;
} }
} else } else {
return null; return null;
}
} }
ObjectValueContext parseObjectValue() { ObjectValueContext parseObjectValue() {
@ -570,9 +579,9 @@ class Parser {
eatCommas(); eatCommas();
if (next(TokenType.RBRACE)) { if (next(TokenType.RBRACE)) {
return new ObjectValueContext(LBRACE, fields, current); return ObjectValueContext(LBRACE, fields, current);
} else { } else {
errors.add(new SyntaxError('Unterminated object literal.', lastSpan)); errors.add(SyntaxError('Unterminated object literal.', lastSpan));
return null; return null;
} }
} else { } else {
@ -586,16 +595,16 @@ class Parser {
if (next(TokenType.COLON)) { if (next(TokenType.COLON)) {
var COLON = current; var COLON = current;
var value = parseValue(); var value = parseInputValue();
if (value != null) { if (value != null) {
return new ObjectFieldContext(NAME, COLON, value); return ObjectFieldContext(NAME, COLON, value);
} else { } else {
errors.add(new SyntaxError('Missing value after ":".', COLON.span)); errors.add(SyntaxError('Missing value after ":".', COLON.span));
return null; return null;
} }
} else { } else {
errors.add(new SyntaxError( errors.add(SyntaxError(
'Missing ":" after name "${NAME.span.text}".', NAME.span)); 'Missing ":" after name "${NAME.span.text}".', NAME.span));
return null; return null;
} }

View file

@ -10,9 +10,10 @@ class Token {
@override @override
String toString() { String toString() {
if (span == null) if (span == null) {
return "'$text' -> $type"; return "'$text' -> $type";
else } else {
return "(${span.start.line}:${span.start.column}) '$text' -> $type"; return "(${span.start.line}:${span.start.column}) '$text' -> $type";
}
} }
} }

View file

@ -11,4 +11,5 @@ dependencies:
string_scanner: ^1.0.0 string_scanner: ^1.0.0
dev_dependencies: dev_dependencies:
matcher: any matcher: any
pedantic: ^1.0.0
test: ">=0.12.0 <2.0.0" test: ">=0.12.0 <2.0.0"

View file

@ -32,10 +32,9 @@ ArgumentContext parseArgument(String text) => parse(text).parseArgument();
List<ArgumentContext> parseArgumentList(String text) => List<ArgumentContext> parseArgumentList(String text) =>
parse(text).parseArguments(); parse(text).parseArguments();
Matcher isArgument(String name, value) => new _IsArgument(name, value); Matcher isArgument(String name, value) => _IsArgument(name, value);
Matcher isArgumentList(List<Matcher> arguments) => Matcher isArgumentList(List<Matcher> arguments) => _IsArgumentList(arguments);
new _IsArgumentList(arguments);
class _IsArgument extends Matcher { class _IsArgument extends Matcher {
final String name; final String name;
@ -53,11 +52,11 @@ class _IsArgument extends Matcher {
var arg = item is ArgumentContext ? item : parseArgument(item.toString()); var arg = item is ArgumentContext ? item : parseArgument(item.toString());
if (arg == null) return false; if (arg == null) return false;
print(arg.span.highlight()); print(arg.span.highlight());
var v = arg.value;
return equals(name).matches(arg.name, matchState) && return equals(name).matches(arg.name, matchState) &&
equals(value).matches( ((v is VariableContext && equals(value).matches(v.name, matchState)) ||
arg.valueOrVariable.value?.value ?? equals(value).matches(arg.value.computeValue({}), matchState));
arg.valueOrVariable.variable?.name,
matchState);
} }
} }

View file

@ -1,3 +1,3 @@
import 'package:graphql_parser/graphql_parser.dart'; import 'package:graphql_parser/graphql_parser.dart';
Parser parse(String text) => new Parser(scan(text)); Parser parse(String text) => Parser(scan(text));

View file

@ -37,11 +37,10 @@ main() {
DirectiveContext parseDirective(String text) => parse(text).parseDirective(); DirectiveContext parseDirective(String text) => parse(text).parseDirective();
Matcher isDirective(String name, {Matcher valueOrVariable, Matcher argument}) => Matcher isDirective(String name, {Matcher valueOrVariable, Matcher argument}) =>
new _IsDirective(name, _IsDirective(name, valueOrVariable: valueOrVariable, argument: argument);
valueOrVariable: valueOrVariable, argument: argument);
Matcher isDirectiveList(List<Matcher> directives) => Matcher isDirectiveList(List<Matcher> directives) =>
new _IsDirectiveList(directives); _IsDirectiveList(directives);
class _IsDirective extends Matcher { class _IsDirective extends Matcher {
final String name; final String name;
@ -57,8 +56,9 @@ class _IsDirective extends Matcher {
return valueOrVariable.describe(desc.add(' and ')); return valueOrVariable.describe(desc.add(' and '));
} else if (argument != null) { } else if (argument != null) {
return argument.describe(desc.add(' and ')); return argument.describe(desc.add(' and '));
} else } else {
return desc; return desc;
}
} }
@override @override
@ -67,20 +67,26 @@ class _IsDirective extends Matcher {
item is DirectiveContext ? item : parseDirective(item.toString()); item is DirectiveContext ? item : parseDirective(item.toString());
if (directive == null) return false; if (directive == null) return false;
if (valueOrVariable != null) { if (valueOrVariable != null) {
if (directive.valueOrVariable == null) if (directive.value == null) {
return false; return false;
else } else {
return valueOrVariable.matches( var v = directive.value;
directive.valueOrVariable.value?.value ?? if (v is VariableContext) {
directive.valueOrVariable.variable?.name, return valueOrVariable.matches(v.name, matchState);
matchState); } else {
return valueOrVariable.matches(
directive.value.computeValue({}), matchState);
}
}
} else if (argument != null) { } else if (argument != null) {
if (directive.argument == null) if (directive.argument == null) {
return false; return false;
else } else {
return argument.matches(directive.argument, matchState); return argument.matches(directive.argument, matchState);
} else }
} else {
return true; return true;
}
} }
} }

View file

@ -86,10 +86,9 @@ Matcher isField(
Matcher arguments, Matcher arguments,
Matcher directives, Matcher directives,
Matcher selectionSet}) => Matcher selectionSet}) =>
new _IsField(fieldName, arguments, directives, selectionSet); _IsField(fieldName, arguments, directives, selectionSet);
Matcher isFieldName(String name, {String alias}) => Matcher isFieldName(String name, {String alias}) => _IsFieldName(name, alias);
new _IsFieldName(name, alias);
class _IsField extends Matcher { class _IsField extends Matcher {
final Matcher fieldName, arguments, directives, selectionSet; final Matcher fieldName, arguments, directives, selectionSet;
@ -106,10 +105,12 @@ class _IsField extends Matcher {
bool matches(item, Map matchState) { bool matches(item, Map matchState) {
var field = item is FieldContext ? item : parseField(item.toString()); var field = item is FieldContext ? item : parseField(item.toString());
if (field == null) return false; if (field == null) return false;
if (fieldName != null && !fieldName.matches(field.fieldName, matchState)) if (fieldName != null && !fieldName.matches(field.fieldName, matchState)) {
return false; return false;
if (arguments != null && !arguments.matches(field.arguments, matchState)) }
if (arguments != null && !arguments.matches(field.arguments, matchState)) {
return false; return false;
}
return true; return true;
} }
} }
@ -121,9 +122,10 @@ class _IsFieldName extends Matcher {
@override @override
Description describe(Description description) { Description describe(Description description) {
if (realName != null) if (realName != null) {
return description return description
.add('is field with name "$name" and alias "$realName"'); .add('is field with name "$name" and alias "$realName"');
}
return description.add('is field with name "$name"'); return description.add('is field with name "$name"');
} }
@ -131,10 +133,11 @@ class _IsFieldName extends Matcher {
bool matches(item, Map matchState) { bool matches(item, Map matchState) {
var fieldName = var fieldName =
item is FieldNameContext ? item : parseFieldName(item.toString()); item is FieldNameContext ? item : parseFieldName(item.toString());
if (realName != null) if (realName != null) {
return fieldName.alias?.alias == name && return fieldName.alias?.alias == name &&
fieldName.alias?.name == realName; fieldName.alias?.name == realName;
else } else {
return fieldName.name == name; return fieldName.name == name;
}
} }
} }

View file

@ -25,7 +25,7 @@ FragmentSpreadContext parseFragmentSpread(String text) =>
parse(text).parseFragmentSpread(); parse(text).parseFragmentSpread();
Matcher isFragmentSpread(String name, {Matcher directives}) => Matcher isFragmentSpread(String name, {Matcher directives}) =>
new _IsFragmentSpread(name, directives); _IsFragmentSpread(name, directives);
class _IsFragmentSpread extends Matcher { class _IsFragmentSpread extends Matcher {
final String name; final String name;
@ -35,9 +35,10 @@ class _IsFragmentSpread extends Matcher {
@override @override
Description describe(Description description) { Description describe(Description description) {
if (directives != null) if (directives != null) {
return directives.describe( return directives.describe(
description.add('is a fragment spread named "$name" that also ')); description.add('is a fragment spread named "$name" that also '));
}
return description.add('is a fragment spread named "$name"'); return description.add('is a fragment spread named "$name"');
} }
@ -48,9 +49,10 @@ class _IsFragmentSpread extends Matcher {
: parseFragmentSpread(item.toString()); : parseFragmentSpread(item.toString());
if (spread == null) return false; if (spread == null) return false;
if (spread.name != name) return false; if (spread.name != name) return false;
if (directives != null) if (directives != null) {
return directives.matches(spread.directives, matchState); return directives.matches(spread.directives, matchState);
else } else {
return true; return true;
}
} }
} }

View file

@ -47,7 +47,7 @@ InlineFragmentContext parseInlineFragment(String text) =>
Matcher isInlineFragment(String name, Matcher isInlineFragment(String name,
{Matcher directives, Matcher selectionSet}) => {Matcher directives, Matcher selectionSet}) =>
new _IsInlineFragment(name, directives, selectionSet); _IsInlineFragment(name, directives, selectionSet);
class _IsInlineFragment extends Matcher { class _IsInlineFragment extends Matcher {
final String name; final String name;

View file

@ -0,0 +1,94 @@
import 'package:graphql_parser/graphql_parser.dart';
import 'package:test/test.dart';
/// This is an *extremely* verbose test, but basically it
/// parses both documents, and makes sure that $memberId has
/// a valid value.
///
/// Resolves https://github.com/angel-dart/graphql/issues/23.
void main() {
void testStr<T>(String name, String text) {
test('name', () {
final List<Token> tokens = scan(text);
final Parser parser = Parser(tokens);
if (parser.errors.isNotEmpty) {
print(parser.errors.toString());
}
expect(parser.errors, isEmpty);
// Parse the GraphQL document using recursive descent
final DocumentContext doc = parser.parseDocument();
expect(doc.definitions, isNotNull);
expect(doc.definitions, isNotEmpty);
// Sanity check
var queryDef = doc.definitions[0] as OperationDefinitionContext;
expect(queryDef.isQuery, true);
expect(queryDef.name, 'customerMemberAttributes');
expect(queryDef.variableDefinitions.variableDefinitions, hasLength(1));
var memberIdDef = queryDef.variableDefinitions.variableDefinitions[0];
expect(memberIdDef.variable.name, 'memberId');
// Find $memberId
var customerByCustomerId = queryDef.selectionSet.selections[0];
var customerMemberAttributesByCustomerId =
customerByCustomerId.field.selectionSet.selections[0];
var nodes0 =
customerMemberAttributesByCustomerId.field.selectionSet.selections[0];
var customerMemberAttributeId = nodes0.field.selectionSet.selections[0];
expect(customerMemberAttributeId.field.fieldName.name,
'customerMemberAttributeId');
var memberAttr = nodes0.field.selectionSet.selections[1];
expect(memberAttr.field.fieldName.name,
'memberAttributesByCustomerMemberAttributeId');
expect(memberAttr.field.arguments, hasLength(1));
var condition = memberAttr.field.arguments[0];
expect(condition.name, 'condition');
expect(condition.value, TypeMatcher<ObjectValueContext>());
var conditionValue = condition.value as ObjectValueContext;
var memberId = conditionValue.fields
.singleWhere((f) => f.nameToken.text == 'memberId');
expect(memberId.value, TypeMatcher<T>());
print('Found \$memberId: Instance of $T');
});
}
testStr<VariableContext>('member id as var', memberIdAsVar);
testStr<NumberValueContext>('member id as constant', memberIdAsConstant);
}
final String memberIdAsVar = r'''
query customerMemberAttributes($memberId: Int!){
customerByCustomerId(customerId: 7) {
customerMemberAttributesByCustomerId {
nodes {
customerMemberAttributeId
memberAttributesByCustomerMemberAttributeId(condition: {memberId: $memberId}) {
nodes {
memberAttributeId
}
}
}
}
}
}
''';
final String memberIdAsConstant = r'''
query customerMemberAttributes($memberId: Int!){
customerByCustomerId(customerId: 7) {
customerMemberAttributesByCustomerId {
nodes {
customerMemberAttributeId
memberAttributesByCustomerMemberAttributeId(condition: {memberId: 7}) {
nodes {
memberAttributeId
}
}
}
}
}
}
''';

View file

@ -55,8 +55,7 @@ main() {
SelectionSetContext parseSelectionSet(String text) => SelectionSetContext parseSelectionSet(String text) =>
parse(text).parseSelectionSet(); parse(text).parseSelectionSet();
Matcher isSelectionSet(List<Matcher> selections) => Matcher isSelectionSet(List<Matcher> selections) => _IsSelectionSet(selections);
new _IsSelectionSet(selections);
class _IsSelectionSet extends Matcher { class _IsSelectionSet extends Matcher {
final List<Matcher> selections; final List<Matcher> selections;
@ -87,8 +86,9 @@ class _IsSelectionSet extends Matcher {
for (int i = 0; i < set.selections.length; i++) { for (int i = 0; i < set.selections.length; i++) {
var sel = set.selections[i]; var sel = set.selections[i];
if (!selections[i].matches( if (!selections[i].matches(
sel.field ?? sel.fragmentSpread ?? sel.inlineFragment, matchState)) sel.field ?? sel.fragmentSpread ?? sel.inlineFragment, matchState)) {
return false; return false;
}
} }
return true; return true;

View file

@ -49,10 +49,10 @@ main() {
TypeContext parseType(String text) => parse(text).parseType(); TypeContext parseType(String text) => parse(text).parseType();
Matcher isListType(Matcher innerType, {bool isNullable}) => Matcher isListType(Matcher innerType, {bool isNullable}) =>
new _IsListType(innerType, isNullable: isNullable != false); _IsListType(innerType, isNullable: isNullable != false);
Matcher isType(String name, {bool isNullable}) => Matcher isType(String name, {bool isNullable}) =>
new _IsType(name, nonNull: isNullable != true); _IsType(name, nonNull: isNullable != true);
class _IsListType extends Matcher { class _IsListType extends Matcher {
final Matcher innerType; final Matcher innerType;
@ -72,7 +72,7 @@ class _IsListType extends Matcher {
var type = item is TypeContext ? item : parseType(item.toString()); var type = item is TypeContext ? item : parseType(item.toString());
if (type.listType == null) return false; if (type.listType == null) return false;
if (type.isNullable != (isNullable != false)) return false; if (type.isNullable != (isNullable != false)) return false;
return innerType.matches(type.listType.type, matchState); return innerType.matches(type.listType.innerType, matchState);
} }
} }
@ -84,10 +84,11 @@ class _IsType extends Matcher {
@override @override
Description describe(Description description) { Description describe(Description description) {
if (nonNull == true) if (nonNull == true) {
return description.add('is non-null type named "$name"'); return description.add('is non-null type named "$name"');
else } else {
return description.add('is nullable type named "$name"'); return description.add('is nullable type named "$name"');
}
} }
@override @override

View file

@ -64,7 +64,7 @@ main() {
test('exceptions', () { test('exceptions', () {
var throwsSyntaxError = predicate((x) { var throwsSyntaxError = predicate((x) {
var parser = parse(x.toString())..parseValue(); var parser = parse(x.toString())..parseInputValue();
return parser.errors.isNotEmpty; return parser.errors.isNotEmpty;
}, 'fails to parse value'); }, 'fails to parse value');
@ -72,9 +72,9 @@ main() {
}); });
} }
ValueContext parseValue(String text) => parse(text).parseValue(); InputValueContext parseValue(String text) => parse(text).parseInputValue();
Matcher isValue(value) => new _IsValue(value); Matcher isValue(value) => _IsValue(value);
class _IsValue extends Matcher { class _IsValue extends Matcher {
final value; final value;
@ -87,7 +87,7 @@ class _IsValue extends Matcher {
@override @override
bool matches(item, Map matchState) { bool matches(item, Map matchState) {
var v = item is ValueContext ? item : parseValue(item.toString()); var v = item is InputValueContext ? item : parseValue(item.toString());
return equals(value).matches(v.value, matchState); return equals(value).matches(v.computeValue({}), matchState);
} }
} }

View file

@ -42,7 +42,7 @@ VariableDefinitionContext parseVariableDefinition(String text) =>
Matcher isVariableDefinition(String name, Matcher isVariableDefinition(String name,
{Matcher type, Matcher defaultValue}) => {Matcher type, Matcher defaultValue}) =>
new _IsVariableDefinition(name, type, defaultValue); _IsVariableDefinition(name, type, defaultValue);
class _IsVariableDefinition extends Matcher { class _IsVariableDefinition extends Matcher {
final String name; final String name;

View file

@ -22,7 +22,7 @@ main() {
}); });
} }
Matcher isVariable(String name) => new _IsVariable(name); Matcher isVariable(String name) => _IsVariable(name);
class _IsVariable extends Matcher { class _IsVariable extends Matcher {
final String name; final String name;

View file

@ -1,3 +1,10 @@
# 1.1.0
* Updates for `package:graphql_parser@1.2.0`.
* Now that variables are `InputValueContext` descendants, handle them the
same way as other values in `coerceArgumentValues`. TLDR - Removed
now-obsolete, variable-specific logic in `coerceArgumentValues`.
* Pass `argumentName`, not `fieldName`, to type validations.
# 1.0.3 # 1.0.3
* Make field resolution asynchronous. * Make field resolution asynchronous.
* Make introspection cycle-safe. * Make introspection cycle-safe.

View file

@ -55,7 +55,7 @@ class GraphQL {
GraphQLType convertType(TypeContext ctx) { GraphQLType convertType(TypeContext ctx) {
if (ctx.listType != null) { if (ctx.listType != null) {
return GraphQLListType(convertType(ctx.listType.type)); return GraphQLListType(convertType(ctx.listType.innerType));
} else if (ctx.typeName != null) { } else if (ctx.typeName != null) {
switch (ctx.typeName.name) { switch (ctx.typeName.name) {
case 'Int': case 'Int':
@ -161,7 +161,8 @@ class GraphQL {
if (value == null) { if (value == null) {
if (defaultValue != null) { if (defaultValue != null) {
coercedValues[variableName] = defaultValue.value.value; coercedValues[variableName] =
defaultValue.value.computeValue(variableValues);
} else if (!variableType.isNullable) { } else if (!variableType.isNullable) {
throw GraphQLException.fromSourceSpan( throw GraphQLException.fromSourceSpan(
'Missing required variable "$variableName".', 'Missing required variable "$variableName".',
@ -402,25 +403,10 @@ class GraphQL {
var argumentType = argumentDefinition.type; var argumentType = argumentDefinition.type;
var defaultValue = argumentDefinition.defaultValue; var defaultValue = argumentDefinition.defaultValue;
var value = argumentValues.firstWhere((a) => a.name == argumentName, var argumentValue = argumentValues
orElse: () => null); .firstWhere((a) => a.name == argumentName, orElse: () => null);
if (value?.valueOrVariable?.variable != null) { if (argumentValue == null) {
var variableName = value.valueOrVariable.variable.name;
var variableValue = variableValues[variableName];
if (variableValues.containsKey(variableName)) {
coercedValues[argumentName] = variableValue;
} else if (defaultValue != null || argumentDefinition.defaultsToNull) {
coercedValues[argumentName] = defaultValue;
} else if (argumentType is GraphQLNonNullableType) {
throw GraphQLException.fromSourceSpan(
'Missing value for argument "$argumentName" of field "$fieldName".',
value.valueOrVariable.span);
} else {
continue;
}
} else if (value == null) {
if (defaultValue != null || argumentDefinition.defaultsToNull) { if (defaultValue != null || argumentDefinition.defaultsToNull) {
coercedValues[argumentName] = defaultValue; coercedValues[argumentName] = defaultValue;
} else if (argumentType is GraphQLNonNullableType) { } else if (argumentType is GraphQLNonNullableType) {
@ -432,7 +418,7 @@ class GraphQL {
} else { } else {
try { try {
var validation = argumentType.validate( var validation = argumentType.validate(
fieldName, value.valueOrVariable.value.value); argumentName, argumentValue.value.computeValue(variableValues));
if (!validation.successful) { if (!validation.successful) {
var errors = <GraphQLExceptionError>[ var errors = <GraphQLExceptionError>[
@ -440,7 +426,7 @@ class GraphQL {
'Type coercion error for value of argument "$argumentName" of field "$fieldName".', 'Type coercion error for value of argument "$argumentName" of field "$fieldName".',
locations: [ locations: [
GraphExceptionErrorLocation.fromSourceLocation( GraphExceptionErrorLocation.fromSourceLocation(
value.valueOrVariable.span.start) argumentValue.value.span.start)
], ],
) )
]; ];
@ -451,7 +437,7 @@ class GraphQL {
error, error,
locations: [ locations: [
GraphExceptionErrorLocation.fromSourceLocation( GraphExceptionErrorLocation.fromSourceLocation(
value.valueOrVariable.span.start) argumentValue.value.span.start)
], ],
), ),
); );
@ -468,14 +454,14 @@ class GraphQL {
'Type coercion error for value of argument "$argumentName" of field "$fieldName".', 'Type coercion error for value of argument "$argumentName" of field "$fieldName".',
locations: [ locations: [
GraphExceptionErrorLocation.fromSourceLocation( GraphExceptionErrorLocation.fromSourceLocation(
value.valueOrVariable.span.start) argumentValue.value.span.start)
], ],
), ),
GraphQLExceptionError( GraphQLExceptionError(
e.message.toString(), e.message.toString(),
locations: [ locations: [
GraphExceptionErrorLocation.fromSourceLocation( GraphExceptionErrorLocation.fromSourceLocation(
value.valueOrVariable.span.start) argumentValue.value.span.start)
], ],
), ),
]); ]);
@ -706,25 +692,27 @@ class GraphQL {
SelectionContext selection, Map<String, dynamic> variableValues) { SelectionContext selection, Map<String, dynamic> variableValues) {
if (selection.field == null) return null; if (selection.field == null) return null;
var directive = selection.field.directives.firstWhere((d) { var directive = selection.field.directives.firstWhere((d) {
var vv = d.valueOrVariable; var vv = d.value;
if (vv.value != null) return vv.value.value == name; if (vv is VariableContext) {
return vv.variable.name == name; return vv.name == name;
} else {
return vv.computeValue(variableValues) == name;
}
}, orElse: () => null); }, orElse: () => null);
if (directive == null) return null; if (directive == null) return null;
if (directive.argument?.name != argumentName) return null; if (directive.argument?.name != argumentName) return null;
var vv = directive.argument.valueOrVariable; var vv = directive.argument.value;
if (vv is VariableContext) {
if (vv.value != null) return vv.value.value; var vname = vv.name;
if (!variableValues.containsKey(vname)) {
var vname = vv.variable.name; throw GraphQLException.fromSourceSpan(
if (!variableValues.containsKey(vname)) { 'Unknown variable: "$vname"', vv.span);
throw GraphQLException.fromSourceSpan( }
'Unknown variable: "$vname"', vv.span); return variableValues[vname];
} }
return vv.computeValue(variableValues);
return variableValues[vname];
} }
bool doesFragmentTypeApply( bool doesFragmentTypeApply(

View file

@ -370,7 +370,7 @@ GraphQLObjectType _reflectDirectiveType() {
field( field(
'name', 'name',
graphQLString.nonNullable(), graphQLString.nonNullable(),
resolve: (obj, _) => (obj as DirectiveContext).NAME.span.text, resolve: (obj, _) => (obj as DirectiveContext).nameToken.span.text,
), ),
field( field(
'description', 'description',

View file

@ -1,5 +1,5 @@
name: graphql_server name: graphql_server
version: 1.0.3 version: 1.1.0
author: Tobe O <thosakwe@gmail.com> author: Tobe O <thosakwe@gmail.com>
description: Base package for implementing GraphQL servers. You might prefer `package:angel_graphql`, the fastest way to implement GraphQL backends in Dart. description: Base package for implementing GraphQL servers. You might prefer `package:angel_graphql`, the fastest way to implement GraphQL backends in Dart.
homepage: https://github.com/angel-dart/graphql homepage: https://github.com/angel-dart/graphql
@ -17,3 +17,6 @@ dependencies:
dev_dependencies: dev_dependencies:
pedantic: ^1.0.0 pedantic: ^1.0.0
test: ">=0.12.0 <2.0.0" test: ">=0.12.0 <2.0.0"
dependency_overrides:
graphql_parser:
path: ../graphql_parser