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
* Apply `package:pedantic`.

View file

@ -74,7 +74,7 @@ Future configureServer(Angel app) async {
convertDartType(Todo),
resolve: resolveViaServiceRead(todoService),
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.
```dart
app.all('/graphql', graphQLHttp(new GraphQL(schema)));
app.all('/graphql', graphQLHttp(GraphQL(schema)));
app.get('/graphiql', graphiQL());
```
@ -116,7 +116,7 @@ All that's left now is just to start the server!
```dart
var server = await http.startServer('127.0.0.1', 3000);
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');
print('Listening at $uri');
print('Access graphiql at $graphiqlUri');
@ -214,7 +214,7 @@ var queryType = objectType(
convertDartType(Todo),
resolve: resolveViaServiceRead(todoService),
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/graphql_http.dart';
export 'src/graphql_ws.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}) {
return (req, res) {
res
..contentType = new MediaType('text', 'html')
..contentType = MediaType('text', 'html')
..write(renderGraphiql(
graphqlEndpoint: graphQLEndpoint,
subscriptionsEndpoint: subscriptionsEndpoint))
@ -30,7 +30,7 @@ String renderGraphiql(
<script src="//unpkg.com/graphiql-subscriptions-fetcher@0.0.2/browser/client.js"></script>
''';
subscriptionsFetcher = '''
let subscriptionsClient = new window.SubscriptionsTransportWs.SubscriptionClient('$subscriptionsEndpoint', {
let subscriptionsClient = window.SubscriptionsTransportWs.SubscriptionClient('$subscriptionsEndpoint', {
reconnect: true
});
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_server/graphql_server.dart';
final ContentType graphQlContentType =
new ContentType('application', 'graphql');
final ContentType graphQlContentType = ContentType('application', 'graphql');
final Validator graphQlPostBody = new Validator({
final Validator graphQlPostBody = Validator({
'query*': isNonEmptyString,
'operation_name': isNonEmptyString,
'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.
///
/// Follows the guidelines listed here:
@ -80,30 +81,92 @@ RequestHandler graphQLHttp(GraphQL graphQL,
if (await validate(graphQlPostBody)(req, res) as bool) {
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 {
throw new AngelHttpException.badRequest();
throw AngelHttpException.badRequest();
}
} else {
throw new AngelHttpException.badRequest();
throw AngelHttpException.badRequest();
}
} on ValidationException catch (e) {
var errors = <GraphQLExceptionError>[
new GraphQLExceptionError(e.message)
];
var errors = <GraphQLExceptionError>[GraphQLExceptionError(e.message)];
errors
.addAll(e.errors.map((ee) => new GraphQLExceptionError(ee)).toList());
return new GraphQLException(errors).toJson();
errors.addAll(e.errors.map((ee) => GraphQLExceptionError(ee)).toList());
return GraphQLException(errors).toJson();
} on AngelHttpException catch (e) {
var errors = <GraphQLExceptionError>[
new GraphQLExceptionError(e.message)
];
var errors = <GraphQLExceptionError>[GraphQLExceptionError(e.message)];
errors
.addAll(e.errors.map((ee) => new GraphQLExceptionError(ee)).toList());
return new GraphQLException(errors).toJson();
errors.addAll(e.errors.map((ee) => GraphQLExceptionError(ee)).toList());
return GraphQLException(errors).toJson();
} on SyntaxError catch (e) {
return new GraphQLException.fromSourceSpan(e.message, e.span);
return GraphQLException.fromSourceSpan(e.message, e.span);
} on GraphQLException catch (e) {
return e.toJson();
} catch (e, st) {
@ -114,7 +177,7 @@ RequestHandler graphQLHttp(GraphQL graphQL,
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();
var socket = await WebSocketTransformer.upgrade(req.rawRequest,
protocolSelector: (protocols) {
if (protocols.contains('graphql-ws'))
if (protocols.contains('graphql-ws')) {
return 'graphql-ws';
else
} else {
throw AngelHttpException.badRequest(
message: 'Only the "graphql-ws" protocol is allowed.');
}
});
var channel = IOWebSocketChannel(socket);
var client = stw.RemoteClient(channel.cast<String>());
@ -69,6 +70,7 @@ class _GraphQLWSServer extends stw.Server {
operationName: operationName,
sourceUrl: 'input',
globalVariables: globalVariables,
variableValues: variables,
);
return stw.GraphQLResult(data);
} 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
* 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) {
var tokens = scan(text);
var parser = new Parser(tokens);
var parser = Parser(tokens);
if (parser.errors.isNotEmpty) {
// Handle errors...

View file

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

View file

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

View file

@ -1,19 +1,33 @@
import 'package:source_span/source_span.dart';
import '../token.dart';
import 'node.dart';
/// An alternate name for a field within a [SelectionSet].
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.
String get alias => NAME1.text;
String get alias => nameToken1.text;
/// The actual name of the value.
String get name => NAME2.text;
String get name => nameToken2.text;
@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 '../token.dart';
import 'node.dart';
import 'value_or_variable.dart';
import 'input_value.dart';
/// An argument passed to a [FieldContext].
class ArgumentContext extends Node {
final Token NAME, COLON;
final ValueOrVariableContext valueOrVariable;
/// The source tokens.
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
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 '../token.dart';
import 'value.dart';
import 'input_value.dart';
class ListValueContext extends ValueContext {
final Token LBRACKET, RBRACKET;
final List<ValueContext> values = [];
/// A GraphQL list value literal.
class ListValueContext extends InputValueContext {
/// 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
FileSpan get span {
var out = values.fold<FileSpan>(LBRACKET.span, (o, v) => o.expand(v.span));
return out.expand(RBRACKET.span);
var out =
values.fold<FileSpan>(lBracketToken.span, (o, v) => o.expand(v.span));
return out.expand(rBracketToken.span);
}
@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 'default_value.dart';
export 'definition.dart';
export 'deprecated_value.dart';
export 'directive.dart';
export 'document.dart';
export 'field.dart';
@ -13,6 +14,7 @@ export 'field_name.dart';
export 'fragment_definition.dart';
export 'fragment_spread.dart';
export 'inline_fragment.dart';
export 'input_value.dart';
export 'list_type.dart';
export 'misc_value.dart';
export 'node.dart';
@ -24,8 +26,6 @@ export 'string_value.dart';
export 'type.dart';
export 'type_condition.dart';
export 'type_name.dart';
export 'value.dart';
export 'value_or_variable.dart';
export 'variable.dart';
export 'variable_definition.dart';
export 'variable_definitions.dart';

View file

@ -1,20 +1,28 @@
import '../token.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;
final Token BOOLEAN;
BooleanValueContext(this.BOOLEAN) {
assert(BOOLEAN?.text == 'true' || BOOLEAN?.text == 'false');
/// The source token.
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
bool get value => booleanValue;
FileSpan get span => booleanToken.span;
@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 '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 {
final Token EQUALS;
final ValueContext value;
/// The source token.
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
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';
/// The base class for top-level GraphQL definitions.
abstract class DefinitionContext extends Node {}
/// An executable definition.
abstract class ExecutableDefinitionContext extends DefinitionContext {}
/// An ad-hoc type system declared in GraphQL.
abstract class TypeSystemDefinitionContext extends DefinitionContext {}
/// An extension to an existing ad-hoc type system.
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 'argument.dart';
import 'input_value.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 {
final Token ARROBA, NAME, COLON, LPAREN, RPAREN;
final ArgumentContext argument;
final ValueOrVariableContext valueOrVariable;
/// The source tokens.
final Token arrobaToken, nameToken, colonToken, lParenToken, rParenToken;
DirectiveContext(this.ARROBA, this.NAME, this.COLON, this.LPAREN, this.RPAREN,
this.argument, this.valueOrVariable) {
assert(NAME != null);
/// The argument being passed as the directive.
final ArgumentContext argument;
/// 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
FileSpan get span {
var out = ARROBA.span.expand(NAME.span);
var out = arrobaToken.span.expand(nameToken.span);
if (COLON != null) {
out = out.expand(COLON.span).expand(valueOrVariable.span);
} else if (LPAREN != null) {
out = out.expand(LPAREN.span).expand(argument.span).expand(RPAREN.span);
if (colonToken != null) {
out = out.expand(colonToken.span).expand(value.span);
} else if (lParenToken != null) {
out = out
.expand(lParenToken.span)
.expand(argument.span)
.expand(rParenToken.span);
}
return out;

View file

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

View file

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

View file

@ -3,16 +3,25 @@ import '../token.dart';
import 'alias.dart';
import 'node.dart';
/// The name of a GraphQL [FieldContext], which may or may not be [alias]ed.
class FieldNameContext extends Node {
final Token NAME;
/// The source token.
final Token nameToken;
/// An (optional) alias for the field.
final AliasContext alias;
FieldNameContext(this.NAME, [this.alias]) {
assert(NAME != null || alias != null);
FieldNameContext(this.nameToken, [this.alias]) {
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
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 'type_condition.dart';
/// A GraphQL query fragment definition.
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;
/// Any directives on the fragment.
final List<DirectiveContext> directives = [];
/// The selections to apply when the [typeCondition] is met.
final SelectionSetContext selectionSet;
String get name => NAME.text;
/// The [String] value of the [nameToken].
String get name => nameToken.text;
FragmentDefinitionContext(
this.FRAGMENT, this.NAME, this.ON, this.typeCondition, this.selectionSet);
FragmentDefinitionContext(this.fragmentToken, this.nameToken, this.onToken,
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
FileSpan get span {
var out = FRAGMENT.span
.expand(NAME.span)
.expand(ON.span)
var out = fragmentToken.span
.expand(nameToken.span)
.expand(onToken.span)
.expand(typeCondition.span);
out = directives.fold<FileSpan>(out, (o, d) => o.expand(d.span));
return out.expand(selectionSet.span);

View file

@ -3,17 +3,30 @@ import 'directive.dart';
import 'node.dart';
import 'package:source_span/source_span.dart';
/// A GraphQL fragment spread.
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 = [];
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
FileSpan get span {
var out = ELLIPSIS.span.expand(NAME.span);
var out = ellipsisToken.span.expand(nameToken.span);
if (directives.isEmpty) return out;
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 'type_condition.dart';
/// An inline fragment, which typically appears in a [SelectionSetContext].
class InlineFragmentContext extends Node {
final Token ELLIPSIS, ON;
/// The source tokens.
final Token ellipsisToken, onToken;
/// The type which this fragment matches.
final TypeConditionContext typeCondition;
/// Any directives affixed to this inline fragment.
final List<DirectiveContext> directives = [];
/// The selections applied when the [typeCondition] is met.
final SelectionSetContext selectionSet;
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
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));
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 'type.dart';
/// Represents a type that holds a list of another type.
class ListTypeContext extends Node {
final Token LBRACKET, RBRACKET;
final TypeContext type;
/// The source tokens.
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
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 '../token.dart';
import 'input_value.dart';
import 'node.dart';
import 'value.dart';
class NullValueContext extends ValueContext<Null> {
final Token NULL;
/// A GraphQL `null` literal.
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
FileSpan get span => NULL.span;
FileSpan get span => nullToken.span;
@override
Null get value => null;
Null computeValue(Map<String, dynamic> variables) => null;
}
class EnumValueContext extends ValueContext<String> {
final Token NAME;
/// A GraphQL enumeration literal.
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
FileSpan get span => NAME.span;
FileSpan get span => nameToken.span;
@override
String get value => NAME.span.text;
String computeValue(Map<String, dynamic> variables) => nameToken.span.text;
}
class ObjectValueContext extends ValueContext<Map<String, dynamic>> {
final Token LBRACE;
/// A GraphQL object literal.
class ObjectValueContext extends InputValueContext<Map<String, dynamic>> {
/// The source tokens.
final Token lBraceToken, rBraceToken;
/// The fields in the object.
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
FileSpan get span {
var left = LBRACE.span;
var left = lBraceToken.span;
for (var field in fields) {
left = left.expand(field.span);
}
return left.expand(RBRACE.span);
return left.expand(rBraceToken.span);
}
@override
Map<String, dynamic> get value {
Map<String, dynamic> computeValue(Map<String, dynamic> variables) {
if (fields.isEmpty) {
return <String, dynamic>{};
} else {
return fields.fold<Map<String, dynamic>>(<String, dynamic>{},
(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 {
final Token NAME;
final Token COLON;
final ValueContext value;
/// The source tokens.
final Token nameToken, colonToken;
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
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 'package:source_span/source_span.dart';
import '../token.dart';
import 'value.dart';
import 'input_value.dart';
class NumberValueContext extends ValueContext<num> {
final Token NUMBER;
/// A GraphQL number literal.
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 {
var text = NUMBER.text;
if (!text.contains('E') && !text.contains('e'))
var text = numberToken.text;
if (!text.contains('E') && !text.contains('e')) {
return num.parse(text);
else {
} else {
var split = text.split(text.contains('E') ? 'E' : 'e');
var base = num.parse(split[0]);
var exp = num.parse(split[1]);
@ -20,9 +23,13 @@ class NumberValueContext extends ValueContext<num> {
}
}
@override
num get value => numberValue;
/// Use [numberToken] instead.
@deprecated
Token get NUMBER => numberToken;
@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 '../token.dart';
import 'definition.dart';
import 'directive.dart';
import 'selection_set.dart';
import 'variable_definitions.dart';
/// An executable GraphQL operation definition.
class OperationDefinitionContext extends ExecutableDefinitionContext {
final Token TYPE, NAME;
/// The source tokens.
final Token typeToken, nameToken;
/// The variables defined in the operation.
final VariableDefinitionsContext variableDefinitions;
/// Any directives affixed to this operation.
final List<DirectiveContext> directives = [];
/// The selections to be applied to an object resolved in this operation.
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(
this.TYPE, this.NAME, this.variableDefinitions, this.selectionSet) {
assert(TYPE == null ||
TYPE.text == 'query' ||
TYPE.text == 'mutation' ||
TYPE.text == 'subscription');
/// Use [nameToken] instead.
@deprecated
Token get NAME => nameToken;
/// Use [typeToken] instead.
@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
FileSpan get span {
if (TYPE == null) return selectionSet.span;
var out = NAME == null ? TYPE.span : TYPE.span.expand(NAME.span);
if (typeToken == null) return selectionSet.span;
var out = nameToken == null
? typeToken.span
: typeToken.span.expand(nameToken.span);
out = directives.fold<FileSpan>(out, (o, d) => o.expand(d.span));
return out.expand(selectionSet.span);
}

View file

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

View file

@ -3,28 +3,37 @@ import 'package:source_span/source_span.dart';
import '../syntax_error.dart';
import '../token.dart';
import 'value.dart';
import 'input_value.dart';
class StringValueContext extends ValueContext {
final Token STRING;
/// A GraphQL string value literal.
class StringValueContext extends InputValueContext<String> {
/// The source token.
final Token stringToken;
/// Whether this is a block string.
final bool isBlockString;
StringValueContext(this.STRING, {this.isBlockString: false});
StringValueContext(this.stringToken, {this.isBlockString = false});
@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 text;
if (!isBlockString) {
text = STRING.text.substring(1, STRING.text.length - 1);
text = stringToken.text.substring(1, stringToken.text.length - 1);
} 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 buf = new StringBuffer();
var buf = StringBuffer();
for (int i = 0; i < codeUnits.length; i++) {
var ch = codeUnits[i];
@ -35,9 +44,9 @@ class StringValueContext extends ValueContext {
c2 = codeUnits[++i],
c3 = 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);
buf.write(new String.fromCharCode(hexNumber));
buf.write(String.fromCharCode(hexNumber));
continue;
}
@ -63,8 +72,9 @@ class StringValueContext extends ValueContext {
default:
buf.writeCharCode(next);
}
} else
throw new SyntaxError('Unexpected "\\" in string literal.', span);
} else {
throw SyntaxError('Unexpected "\\" in string literal.', span);
}
} else {
buf.writeCharCode(ch);
}
@ -74,5 +84,5 @@ class StringValueContext extends ValueContext {
}
@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 'type_name.dart';
/// A GraphQL type 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;
/// A list type that is being referenced.
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);
}
/// Use [exclamationToken] instead.
@deprecated
Token get EXCLAMATION => exclamationToken;
@override
FileSpan get 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 '../token.dart';
/// The name of a GraphQL type.
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
FileSpan get span => NAME.span;
TypeNameContext(this.NAME);
FileSpan get span => nameToken.span;
}

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 '../token.dart';
import 'input_value.dart';
class VariableContext extends Node {
final Token DOLLAR, NAME;
/// A variable reference in GraphQL.
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
FileSpan get span => DOLLAR.span.expand(NAME.span);
// new FileSpan(DOLLAR?.span?.start, NAME?.span?.end, toSource());
FileSpan get span => dollarToken.span.expand(nameToken.span);
@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 'variable.dart';
/// A single variable definition.
class VariableDefinitionContext extends Node {
final Token COLON;
/// The source token.
final Token colonToken;
/// The declared variable.
final VariableContext variable;
/// The type of the variable.
final TypeContext type;
/// The default value of the variable.
final DefaultValueContext defaultValue;
VariableDefinitionContext(this.variable, this.COLON, this.type,
VariableDefinitionContext(this.variable, this.colonToken, this.type,
[this.defaultValue]);
/// Use [colonToken] instead.
@deprecated
Token get COLON => colonToken;
@override
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 'variable_definition.dart';
/// A set of variable definitions in a GraphQL operation.
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 = [];
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
FileSpan get span {
var out = variableDefinitions.fold<FileSpan>(
LPAREN.span, (o, v) => o.expand(v.span));
return out.expand(RPAREN.span);
lParenToken.span, (o, v) => o.expand(v.span));
return out.expand(rParenToken.span);
}
}

View file

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

View file

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

View file

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

View file

@ -11,4 +11,5 @@ dependencies:
string_scanner: ^1.0.0
dev_dependencies:
matcher: any
pedantic: ^1.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) =>
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) =>
new _IsArgumentList(arguments);
Matcher isArgumentList(List<Matcher> arguments) => _IsArgumentList(arguments);
class _IsArgument extends Matcher {
final String name;
@ -53,11 +52,11 @@ class _IsArgument extends Matcher {
var arg = item is ArgumentContext ? item : parseArgument(item.toString());
if (arg == null) return false;
print(arg.span.highlight());
var v = arg.value;
return equals(name).matches(arg.name, matchState) &&
equals(value).matches(
arg.valueOrVariable.value?.value ??
arg.valueOrVariable.variable?.name,
matchState);
((v is VariableContext && equals(value).matches(v.name, matchState)) ||
equals(value).matches(arg.value.computeValue({}), matchState));
}
}

View file

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

View file

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

View file

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

View file

@ -47,7 +47,7 @@ InlineFragmentContext parseInlineFragment(String text) =>
Matcher isInlineFragment(String name,
{Matcher directives, Matcher selectionSet}) =>
new _IsInlineFragment(name, directives, selectionSet);
_IsInlineFragment(name, directives, selectionSet);
class _IsInlineFragment extends Matcher {
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) =>
parse(text).parseSelectionSet();
Matcher isSelectionSet(List<Matcher> selections) =>
new _IsSelectionSet(selections);
Matcher isSelectionSet(List<Matcher> selections) => _IsSelectionSet(selections);
class _IsSelectionSet extends Matcher {
final List<Matcher> selections;
@ -87,8 +86,9 @@ class _IsSelectionSet extends Matcher {
for (int i = 0; i < set.selections.length; i++) {
var sel = set.selections[i];
if (!selections[i].matches(
sel.field ?? sel.fragmentSpread ?? sel.inlineFragment, matchState))
sel.field ?? sel.fragmentSpread ?? sel.inlineFragment, matchState)) {
return false;
}
}
return true;

View file

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

View file

@ -64,7 +64,7 @@ main() {
test('exceptions', () {
var throwsSyntaxError = predicate((x) {
var parser = parse(x.toString())..parseValue();
var parser = parse(x.toString())..parseInputValue();
return parser.errors.isNotEmpty;
}, '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 {
final value;
@ -87,7 +87,7 @@ class _IsValue extends Matcher {
@override
bool matches(item, Map matchState) {
var v = item is ValueContext ? item : parseValue(item.toString());
return equals(value).matches(v.value, matchState);
var v = item is InputValueContext ? item : parseValue(item.toString());
return equals(value).matches(v.computeValue({}), matchState);
}
}

View file

@ -42,7 +42,7 @@ VariableDefinitionContext parseVariableDefinition(String text) =>
Matcher isVariableDefinition(String name,
{Matcher type, Matcher defaultValue}) =>
new _IsVariableDefinition(name, type, defaultValue);
_IsVariableDefinition(name, type, defaultValue);
class _IsVariableDefinition extends Matcher {
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 {
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
* Make field resolution asynchronous.
* Make introspection cycle-safe.

View file

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

View file

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

View file

@ -1,5 +1,5 @@
name: graphql_server
version: 1.0.3
version: 1.1.0
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.
homepage: https://github.com/angel-dart/graphql
@ -17,3 +17,6 @@ dependencies:
dev_dependencies:
pedantic: ^1.0.0
test: ">=0.12.0 <2.0.0"
dependency_overrides:
graphql_parser:
path: ../graphql_parser