diff --git a/angel_graphql/CHANGELOG.md b/angel_graphql/CHANGELOG.md index c26a73cd..faaed134 100644 --- a/angel_graphql/CHANGELOG.md +++ b/angel_graphql/CHANGELOG.md @@ -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`. diff --git a/angel_graphql/README.md b/angel_graphql/README.md index 74683163..bbb1f67c 100644 --- a/angel_graphql/README.md +++ b/angel_graphql/README.md @@ -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()), ], ), ], diff --git a/angel_graphql/lib/angel_graphql.dart b/angel_graphql/lib/angel_graphql.dart index 7673a86e..780cc100 100644 --- a/angel_graphql/lib/angel_graphql.dart +++ b/angel_graphql/lib/angel_graphql.dart @@ -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 { + @override + String get name => 'Upload'; + + @override + String get description => + 'Represents a file that has been uploaded to the server.'; + + @override + GraphQLType coerceToInputObject() => this; + + @override + UploadedFile deserialize(UploadedFile serialized) => serialized; + + @override + UploadedFile serialize(UploadedFile value) => value; + + @override + ValidationResult 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 implements ValidationResult { + final bool successful; + final List errors; + final T value; + + _Vr(this.successful, {this.errors, this.value}); +} diff --git a/angel_graphql/lib/src/graphiql.dart b/angel_graphql/lib/src/graphiql.dart index 6483a768..d64b8b4d 100644 --- a/angel_graphql/lib/src/graphiql.dart +++ b/angel_graphql/lib/src/graphiql.dart @@ -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( '''; subscriptionsFetcher = ''' - let subscriptionsClient = new window.SubscriptionsTransportWs.SubscriptionClient('$subscriptionsEndpoint', { + let subscriptionsClient = window.SubscriptionsTransportWs.SubscriptionClient('$subscriptionsEndpoint', { reconnect: true }); let $fetcherName = window.GraphiQLSubscriptionsFetcher.graphQLFetcher(subscriptionsClient, graphQLFetcher); diff --git a/angel_graphql/lib/src/graphql_http.dart b/angel_graphql/lib/src/graphql_http.dart index 61e11305..4607b3f9 100644 --- a/angel_graphql/lib/src/graphql_http.dart +++ b/angel_graphql/lib/src/graphql_http.dart @@ -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.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 = [ - new GraphQLExceptionError(e.message) - ]; + var errors = [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 = [ - new GraphQLExceptionError(e.message) - ]; + var errors = [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(); } }; } diff --git a/angel_graphql/lib/src/graphql_ws.dart b/angel_graphql/lib/src/graphql_ws.dart index 0698b65c..29f8ba2d 100644 --- a/angel_graphql/lib/src/graphql_ws.dart +++ b/angel_graphql/lib/src/graphql_ws.dart @@ -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()); @@ -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) { diff --git a/graphql_parser/CHANGELOG.md b/graphql_parser/CHANGELOG.md index 2dc0533a..7efd884c 100644 --- a/graphql_parser/CHANGELOG.md +++ b/graphql_parser/CHANGELOG.md @@ -1,3 +1,9 @@ +# 1.2.0 +* Combine `ValueContext` and `VariableContext` into a single `InputValueContext` supertype. + * Add `T computeValue(Map 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 diff --git a/graphql_parser/README.md b/graphql_parser/README.md index b90fa864..a91d3e49 100644 --- a/graphql_parser/README.md +++ b/graphql_parser/README.md @@ -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... diff --git a/graphql_parser/analysis_options.yaml b/graphql_parser/analysis_options.yaml index eae1e42a..c230cee7 100644 --- a/graphql_parser/analysis_options.yaml +++ b/graphql_parser/analysis_options.yaml @@ -1,3 +1,4 @@ +include: package:pedantic/analysis_options.yaml analyzer: strong-mode: implicit-casts: false \ No newline at end of file diff --git a/graphql_parser/example/example.dart b/graphql_parser/example/example.dart index 34566298..aabba873 100644 --- a/graphql_parser/example/example.dart +++ b/graphql_parser/example/example.dart @@ -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 diff --git a/graphql_parser/lib/src/language/ast/alias.dart b/graphql_parser/lib/src/language/ast/alias.dart index 986b4370..1c986bd2 100644 --- a/graphql_parser/lib/src/language/ast/alias.dart +++ b/graphql_parser/lib/src/language/ast/alias.dart @@ -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); } diff --git a/graphql_parser/lib/src/language/ast/argument.dart b/graphql_parser/lib/src/language/ast/argument.dart index 3a85a067..00382da8 100644 --- a/graphql_parser/lib/src/language/ast/argument.dart +++ b/graphql_parser/lib/src/language/ast/argument.dart @@ -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); } diff --git a/graphql_parser/lib/src/language/ast/array_value.dart b/graphql_parser/lib/src/language/ast/array_value.dart index 63633b5a..51afb3df 100644 --- a/graphql_parser/lib/src/language/ast/array_value.dart +++ b/graphql_parser/lib/src/language/ast/array_value.dart @@ -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 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 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(LBRACKET.span, (o, v) => o.expand(v.span)); - return out.expand(RBRACKET.span); + var out = + values.fold(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 variables) { + return values.map((v) => v.computeValue(variables)).toList(); + } } diff --git a/graphql_parser/lib/src/language/ast/ast.dart b/graphql_parser/lib/src/language/ast/ast.dart index 3fff886d..a274ade4 100644 --- a/graphql_parser/lib/src/language/ast/ast.dart +++ b/graphql_parser/lib/src/language/ast/ast.dart @@ -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'; diff --git a/graphql_parser/lib/src/language/ast/boolean_value.dart b/graphql_parser/lib/src/language/ast/boolean_value.dart index 528abfd4..0b8cfe8f 100644 --- a/graphql_parser/lib/src/language/ast/boolean_value.dart +++ b/graphql_parser/lib/src/language/ast/boolean_value.dart @@ -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 { +/// A GraphQL boolean value literal. +class BooleanValueContext extends InputValueContext { 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 variables) => booleanValue; } diff --git a/graphql_parser/lib/src/language/ast/default_value.dart b/graphql_parser/lib/src/language/ast/default_value.dart index 3395a688..d3b805b5 100644 --- a/graphql_parser/lib/src/language/ast/default_value.dart +++ b/graphql_parser/lib/src/language/ast/default_value.dart @@ -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); } diff --git a/graphql_parser/lib/src/language/ast/definition.dart b/graphql_parser/lib/src/language/ast/definition.dart index efc72ee0..d129d422 100644 --- a/graphql_parser/lib/src/language/ast/definition.dart +++ b/graphql_parser/lib/src/language/ast/definition.dart @@ -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 {} diff --git a/graphql_parser/lib/src/language/ast/deprecated_value.dart b/graphql_parser/lib/src/language/ast/deprecated_value.dart new file mode 100644 index 00000000..5dfca6ef --- /dev/null +++ b/graphql_parser/lib/src/language/ast/deprecated_value.dart @@ -0,0 +1,11 @@ +import 'input_value.dart'; + +/// Use [ConstantContext] instead. This class remains solely for backwards compatibility. +@deprecated +abstract class ValueContext extends InputValueContext { + /// Return a constant value. + T get value; + + @override + T computeValue(Map variables) => value; +} diff --git a/graphql_parser/lib/src/language/ast/directive.dart b/graphql_parser/lib/src/language/ast/directive.dart index a9f172a0..4e7200ed 100644 --- a/graphql_parser/lib/src/language/ast/directive.dart +++ b/graphql_parser/lib/src/language/ast/directive.dart @@ -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; diff --git a/graphql_parser/lib/src/language/ast/document.dart b/graphql_parser/lib/src/language/ast/document.dart index d705e0d6..278d4d9c 100644 --- a/graphql_parser/lib/src/language/ast/document.dart +++ b/graphql_parser/lib/src/language/ast/document.dart @@ -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 definitions = []; @override diff --git a/graphql_parser/lib/src/language/ast/field.dart b/graphql_parser/lib/src/language/ast/field.dart index 3ac2c965..2998d822 100644 --- a/graphql_parser/lib/src/language/ast/field.dart +++ b/graphql_parser/lib/src/language/ast/field.dart @@ -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 arguments = []; + + /// Any directives affixed to this field. final List 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( fieldName.span, (out, d) => out.expand(d.span)); - if (arguments.isNotEmpty) + } + if (arguments.isNotEmpty) { return arguments.fold( fieldName.span, (out, a) => out.expand(a.span)); - else + } else { return fieldName.span; + } } } diff --git a/graphql_parser/lib/src/language/ast/field_name.dart b/graphql_parser/lib/src/language/ast/field_name.dart index d6e80dea..4841d6ce 100644 --- a/graphql_parser/lib/src/language/ast/field_name.dart +++ b/graphql_parser/lib/src/language/ast/field_name.dart @@ -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; } diff --git a/graphql_parser/lib/src/language/ast/fragment_definition.dart b/graphql_parser/lib/src/language/ast/fragment_definition.dart index 23787e07..00204bf2 100644 --- a/graphql_parser/lib/src/language/ast/fragment_definition.dart +++ b/graphql_parser/lib/src/language/ast/fragment_definition.dart @@ -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 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(out, (o, d) => o.expand(d.span)); return out.expand(selectionSet.span); diff --git a/graphql_parser/lib/src/language/ast/fragment_spread.dart b/graphql_parser/lib/src/language/ast/fragment_spread.dart index dcac0070..36a68383 100644 --- a/graphql_parser/lib/src/language/ast/fragment_spread.dart +++ b/graphql_parser/lib/src/language/ast/fragment_spread.dart @@ -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 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(out, (o, d) => o.expand(d.span)); } diff --git a/graphql_parser/lib/src/language/ast/inline_fragment.dart b/graphql_parser/lib/src/language/ast/inline_fragment.dart index 7f616bc5..d73a8080 100644 --- a/graphql_parser/lib/src/language/ast/inline_fragment.dart +++ b/graphql_parser/lib/src/language/ast/inline_fragment.dart @@ -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 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(out, (o, d) => o.expand(d.span)); return out.expand(selectionSet.span); } diff --git a/graphql_parser/lib/src/language/ast/input_value.dart b/graphql_parser/lib/src/language/ast/input_value.dart new file mode 100644 index 00000000..213eb5dd --- /dev/null +++ b/graphql_parser/lib/src/language/ast/input_value.dart @@ -0,0 +1,7 @@ +import 'node.dart'; + +/// Represents a value in GraphQL. +abstract class InputValueContext extends Node { + /// Computes the value, relative to some set of [variables]. + T computeValue(Map variables); +} diff --git a/graphql_parser/lib/src/language/ast/list_type.dart b/graphql_parser/lib/src/language/ast/list_type.dart index ea8ef20c..ee770e00 100644 --- a/graphql_parser/lib/src/language/ast/list_type.dart +++ b/graphql_parser/lib/src/language/ast/list_type.dart @@ -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); } diff --git a/graphql_parser/lib/src/language/ast/misc_value.dart b/graphql_parser/lib/src/language/ast/misc_value.dart index d7f3a2ce..9307e4c1 100644 --- a/graphql_parser/lib/src/language/ast/misc_value.dart +++ b/graphql_parser/lib/src/language/ast/misc_value.dart @@ -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 { - final Token NULL; +/// A GraphQL `null` literal. +class NullValueContext extends InputValueContext { + /// 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 variables) => null; } -class EnumValueContext extends ValueContext { - final Token NAME; +/// A GraphQL enumeration literal. +class EnumValueContext extends InputValueContext { + /// 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 variables) => nameToken.span.text; } -class ObjectValueContext extends ValueContext> { - final Token LBRACE; +/// A GraphQL object literal. +class ObjectValueContext extends InputValueContext> { + /// The source tokens. + final Token lBraceToken, rBraceToken; + + /// The fields in the object. final List 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 get value { + Map computeValue(Map variables) { if (fields.isEmpty) { return {}; } else { return fields.fold>({}, (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); } diff --git a/graphql_parser/lib/src/language/ast/number_value.dart b/graphql_parser/lib/src/language/ast/number_value.dart index 8d5bb90c..733bf0e4 100644 --- a/graphql_parser/lib/src/language/ast/number_value.dart +++ b/graphql_parser/lib/src/language/ast/number_value.dart @@ -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 { - final Token NUMBER; +/// A GraphQL number literal. +class NumberValueContext extends InputValueContext { + /// 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 { } } - @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 variables) => numberValue; } diff --git a/graphql_parser/lib/src/language/ast/operation_definition.dart b/graphql_parser/lib/src/language/ast/operation_definition.dart index 06967ac7..d614fb6b 100644 --- a/graphql_parser/lib/src/language/ast/operation_definition.dart +++ b/graphql_parser/lib/src/language/ast/operation_definition.dart @@ -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 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(out, (o, d) => o.expand(d.span)); return out.expand(selectionSet.span); } diff --git a/graphql_parser/lib/src/language/ast/selection_set.dart b/graphql_parser/lib/src/language/ast/selection_set.dart index 36453c67..4097d94e 100644 --- a/graphql_parser/lib/src/language/ast/selection_set.dart +++ b/graphql_parser/lib/src/language/ast/selection_set.dart @@ -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 selections = []; - SelectionSetContext(this.LBRACE, this.RBRACE); + SelectionSetContext(this.lBraceToken, this.rBraceToken); + /// A synthetic [SelectionSetContext] produced from a set of [selections]. factory SelectionSetContext.merged(List 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(LBRACE.span, (out, s) => out.expand(s.span)); - return out.expand(RBRACE.span); + var out = selections.fold( + lBraceToken.span, (out, s) => out.expand(s.span)); + return out.expand(rBraceToken.span); } } diff --git a/graphql_parser/lib/src/language/ast/string_value.dart b/graphql_parser/lib/src/language/ast/string_value.dart index e5eef5cc..29bc71af 100644 --- a/graphql_parser/lib/src/language/ast/string_value.dart +++ b/graphql_parser/lib/src/language/ast/string_value.dart @@ -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 { + /// 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 variables) => stringValue; } diff --git a/graphql_parser/lib/src/language/ast/type.dart b/graphql_parser/lib/src/language/ast/type.dart index 48b4bcda..d82a97e3 100644 --- a/graphql_parser/lib/src/language/ast/type.dart +++ b/graphql_parser/lib/src/language/ast/type.dart @@ -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; } } diff --git a/graphql_parser/lib/src/language/ast/type_name.dart b/graphql_parser/lib/src/language/ast/type_name.dart index d7d9675a..209de19b 100644 --- a/graphql_parser/lib/src/language/ast/type_name.dart +++ b/graphql_parser/lib/src/language/ast/type_name.dart @@ -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; } diff --git a/graphql_parser/lib/src/language/ast/value.dart b/graphql_parser/lib/src/language/ast/value.dart deleted file mode 100644 index edc4e368..00000000 --- a/graphql_parser/lib/src/language/ast/value.dart +++ /dev/null @@ -1,5 +0,0 @@ -import 'node.dart'; - -abstract class ValueContext extends Node { - T get value; -} diff --git a/graphql_parser/lib/src/language/ast/value_or_variable.dart b/graphql_parser/lib/src/language/ast/value_or_variable.dart deleted file mode 100644 index 7b28b1e4..00000000 --- a/graphql_parser/lib/src/language/ast/value_or_variable.dart +++ /dev/null @@ -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; -} diff --git a/graphql_parser/lib/src/language/ast/variable.dart b/graphql_parser/lib/src/language/ast/variable.dart index 71e7ab56..576734e4 100644 --- a/graphql_parser/lib/src/language/ast/variable.dart +++ b/graphql_parser/lib/src/language/ast/variable.dart @@ -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 { + /// 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 variables) => variables[name]; } diff --git a/graphql_parser/lib/src/language/ast/variable_definition.dart b/graphql_parser/lib/src/language/ast/variable_definition.dart index 9585bfd1..7432798a 100644 --- a/graphql_parser/lib/src/language/ast/variable_definition.dart +++ b/graphql_parser/lib/src/language/ast/variable_definition.dart @@ -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); } diff --git a/graphql_parser/lib/src/language/ast/variable_definitions.dart b/graphql_parser/lib/src/language/ast/variable_definitions.dart index 852ee94e..ddc4405f 100644 --- a/graphql_parser/lib/src/language/ast/variable_definitions.dart +++ b/graphql_parser/lib/src/language/ast/variable_definitions.dart @@ -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 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( - 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); } } diff --git a/graphql_parser/lib/src/language/lexer.dart b/graphql_parser/lib/src/language/lexer.dart index 85ff83c0..1d7c35fd 100644 --- a/graphql_parser/lib/src/language/lexer.dart +++ b/graphql_parser/lib/src/language/lexer.dart @@ -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 _patterns = { '@': TokenType.ARROBA, @@ -42,7 +42,7 @@ final Map _patterns = { List scan(String text, {sourceUrl}) { List out = []; - var scanner = new SpanScanner(text, sourceUrl: sourceUrl); + var scanner = SpanScanner(text, sourceUrl: sourceUrl); while (!scanner.isDone) { List potential = []; @@ -54,14 +54,14 @@ List 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)); diff --git a/graphql_parser/lib/src/language/parser.dart b/graphql_parser/lib/src/language/parser.dart index 810bf8ab..9ea7d33c 100644 --- a/graphql_parser/lib/src/language/parser.dart +++ b/graphql_parser/lib/src/language/parser.dart @@ -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 ?? []) ..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 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 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 values = []; - ValueContext value = parseValue(); + List 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; } diff --git a/graphql_parser/lib/src/language/token.dart b/graphql_parser/lib/src/language/token.dart index 5d12c90d..4a8498ae 100644 --- a/graphql_parser/lib/src/language/token.dart +++ b/graphql_parser/lib/src/language/token.dart @@ -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"; + } } } diff --git a/graphql_parser/pubspec.yaml b/graphql_parser/pubspec.yaml index cf00535d..c9a0a30c 100644 --- a/graphql_parser/pubspec.yaml +++ b/graphql_parser/pubspec.yaml @@ -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" diff --git a/graphql_parser/test/argument_test.dart b/graphql_parser/test/argument_test.dart index ba473144..fbe12542 100644 --- a/graphql_parser/test/argument_test.dart +++ b/graphql_parser/test/argument_test.dart @@ -32,10 +32,9 @@ ArgumentContext parseArgument(String text) => parse(text).parseArgument(); List 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 arguments) => - new _IsArgumentList(arguments); +Matcher isArgumentList(List 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)); } } diff --git a/graphql_parser/test/common.dart b/graphql_parser/test/common.dart index 728b96a8..e30cb713 100644 --- a/graphql_parser/test/common.dart +++ b/graphql_parser/test/common.dart @@ -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)); diff --git a/graphql_parser/test/directive_test.dart b/graphql_parser/test/directive_test.dart index 454c89aa..001c4ea8 100644 --- a/graphql_parser/test/directive_test.dart +++ b/graphql_parser/test/directive_test.dart @@ -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 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; + } } } diff --git a/graphql_parser/test/field_test.dart b/graphql_parser/test/field_test.dart index a0b3634e..e39f3f63 100644 --- a/graphql_parser/test/field_test.dart +++ b/graphql_parser/test/field_test.dart @@ -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; + } } } diff --git a/graphql_parser/test/fragment_spread_test.dart b/graphql_parser/test/fragment_spread_test.dart index e40b4827..752e1122 100644 --- a/graphql_parser/test/fragment_spread_test.dart +++ b/graphql_parser/test/fragment_spread_test.dart @@ -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; + } } } diff --git a/graphql_parser/test/inline_fragment_test.dart b/graphql_parser/test/inline_fragment_test.dart index 5ad6fbca..1936e820 100644 --- a/graphql_parser/test/inline_fragment_test.dart +++ b/graphql_parser/test/inline_fragment_test.dart @@ -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; diff --git a/graphql_parser/test/issue23_test.dart b/graphql_parser/test/issue23_test.dart new file mode 100644 index 00000000..6ab4950a --- /dev/null +++ b/graphql_parser/test/issue23_test.dart @@ -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(String name, String text) { + test('name', () { + final List 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()); + var conditionValue = condition.value as ObjectValueContext; + var memberId = conditionValue.fields + .singleWhere((f) => f.nameToken.text == 'memberId'); + expect(memberId.value, TypeMatcher()); + print('Found \$memberId: Instance of $T'); + }); + } + + testStr('member id as var', memberIdAsVar); + testStr('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 + } + } + } + } + } +} +'''; diff --git a/graphql_parser/test/selection_set_test.dart b/graphql_parser/test/selection_set_test.dart index 438db5bd..cd482481 100644 --- a/graphql_parser/test/selection_set_test.dart +++ b/graphql_parser/test/selection_set_test.dart @@ -55,8 +55,7 @@ main() { SelectionSetContext parseSelectionSet(String text) => parse(text).parseSelectionSet(); -Matcher isSelectionSet(List selections) => - new _IsSelectionSet(selections); +Matcher isSelectionSet(List selections) => _IsSelectionSet(selections); class _IsSelectionSet extends Matcher { final List 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; diff --git a/graphql_parser/test/type_test.dart b/graphql_parser/test/type_test.dart index 1da1f4f8..93b017ca 100644 --- a/graphql_parser/test/type_test.dart +++ b/graphql_parser/test/type_test.dart @@ -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 diff --git a/graphql_parser/test/value_test.dart b/graphql_parser/test/value_test.dart index 61e90f48..a5edaaaf 100644 --- a/graphql_parser/test/value_test.dart +++ b/graphql_parser/test/value_test.dart @@ -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); } } diff --git a/graphql_parser/test/variable_definition_test.dart b/graphql_parser/test/variable_definition_test.dart index 04f85987..f3380aea 100644 --- a/graphql_parser/test/variable_definition_test.dart +++ b/graphql_parser/test/variable_definition_test.dart @@ -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; diff --git a/graphql_parser/test/variable_test.dart b/graphql_parser/test/variable_test.dart index d4484666..eb9759f3 100644 --- a/graphql_parser/test/variable_test.dart +++ b/graphql_parser/test/variable_test.dart @@ -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; diff --git a/graphql_server/CHANGELOG.md b/graphql_server/CHANGELOG.md index 1c419dc9..4bc354ef 100644 --- a/graphql_server/CHANGELOG.md +++ b/graphql_server/CHANGELOG.md @@ -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. diff --git a/graphql_server/lib/graphql_server.dart b/graphql_server/lib/graphql_server.dart index 33c6a534..a01bb7a2 100644 --- a/graphql_server/lib/graphql_server.dart +++ b/graphql_server/lib/graphql_server.dart @@ -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 = [ @@ -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 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( diff --git a/graphql_server/lib/introspection.dart b/graphql_server/lib/introspection.dart index 935f7fed..bb755754 100644 --- a/graphql_server/lib/introspection.dart +++ b/graphql_server/lib/introspection.dart @@ -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', diff --git a/graphql_server/pubspec.yaml b/graphql_server/pubspec.yaml index 22ea102a..c3c33d4e 100644 --- a/graphql_server/pubspec.yaml +++ b/graphql_server/pubspec.yaml @@ -1,5 +1,5 @@ name: graphql_server -version: 1.0.3 +version: 1.1.0 author: Tobe O 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