merge working branch in
This commit is contained in:
commit
935c06a51b
59 changed files with 1020 additions and 464 deletions
|
@ -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`.
|
||||
|
||||
|
|
|
@ -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()),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
|
|
@ -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});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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...
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
include: package:pedantic/analysis_options.yaml
|
||||
analyzer:
|
||||
strong-mode:
|
||||
implicit-casts: false
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 {}
|
||||
|
|
11
graphql_parser/lib/src/language/ast/deprecated_value.dart
Normal file
11
graphql_parser/lib/src/language/ast/deprecated_value.dart
Normal 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;
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
7
graphql_parser/lib/src/language/ast/input_value.dart
Normal file
7
graphql_parser/lib/src/language/ast/input_value.dart
Normal 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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
import 'node.dart';
|
||||
|
||||
abstract class ValueContext<T> extends Node {
|
||||
T get value;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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];
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
94
graphql_parser/test/issue23_test.dart
Normal file
94
graphql_parser/test/issue23_test.dart
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
''';
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue