diff --git a/README.md b/README.md index c69e0227..9d7ea6bc 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ [![Pub](https://img.shields.io/pub/v/graphql_parser.svg)](https://pub.dartlang.org/packages/graphql_parser) [![build status](https://travis-ci.org/thosakwe/graphql_parser.svg)](https://travis-ci.org/thosakwe/graphql_parser) -Parses GraphQL queries and schemas. +Parses GraphQL queries and schemas. Also includes a `GraphQLVisitor` class. -*This library is merely a parser*. Any sort of actual GraphQL API functionality must be implemented by you, +*This library is merely a parser/visitor*. Any sort of actual GraphQL API functionality must be implemented by you, or by a third-party package. [Angel framework](https://angel-dart.github.io) diff --git a/example/basic.dart b/example/basic.dart index c4ef1d54..4a842895 100644 --- a/example/basic.dart +++ b/example/basic.dart @@ -1,5 +1,4 @@ -import 'dart:async'; -import 'package:graphql_parser/src/language/language.dart'; +import 'package:graphql_parser/graphql_parser.dart'; final String INPUT = ''' { @@ -13,4 +12,15 @@ final String INPUT = ''' main() { var tokens = scan(INPUT); var parser = new Parser(tokens); + var doc = parser.parseDocument(); + + var operation = doc.definitions.first as OperationDefinitionContext; + + 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 + + var taglineField = projectField.selectionSet.selections.first.field; + print(taglineField.fieldName.name); // tagline } diff --git a/example/visitor.dart b/example/visitor.dart new file mode 100644 index 00000000..3258a893 --- /dev/null +++ b/example/visitor.dart @@ -0,0 +1,50 @@ +import 'package:graphql_parser/graphql_parser.dart'; +import 'package:graphql_parser/visitor.dart'; + +const String QUERY = ''' +{ + foo, + baz: bar +} +'''; + +const Map DATA = const { + 'foo': 'hello', + 'bar': 'world', + 'quux': 'extraneous' +}; + +main() { + // Highly-simplified querying example... + var result = new MapQuerier(DATA).execute(QUERY); + print(result); // { foo: hello, baz: world } + print(result['foo']); // hello + print(result['baz']); // world +} + +class MapQuerier extends GraphQLVisitor { + final Map data; + final Map result = {}; + + MapQuerier(this.data); + + Map execute(String query) { + var doc = new Parser(scan(query)).parseDocument(); + visitDocument(doc); + return result; + } + + @override + visitField(FieldContext ctx) { + String realName, alias; + if (ctx.fieldName.alias == null) + realName = alias = ctx.fieldName.name; + else { + realName = ctx.fieldName.alias.name; + alias = ctx.fieldName.alias.alias; + } + + // Set output field... + result[alias] = data[realName]; + } +} \ No newline at end of file diff --git a/lib/src/language/ast/alias.dart b/lib/src/language/ast/alias.dart index e0699a87..2400b378 100644 --- a/lib/src/language/ast/alias.dart +++ b/lib/src/language/ast/alias.dart @@ -7,15 +7,14 @@ class AliasContext extends Node { AliasContext(this.NAME1, this.COLON, this.NAME2); - /// The actual name of the value. - String get name => NAME1.text; - /// The aliased name of the value. - String get alias => NAME2.text; + String get alias => NAME1.text; + + /// The actual name of the value. + String get name => NAME2.text; @override - SourceSpan get span => - new SourceSpan(NAME1.span?.start, NAME2.span?.end, toSource()); + SourceSpan get span => NAME1.span.union(COLON.span).union(NAME2.span); @override String toSource() => '${NAME1.text}:${NAME2.text}'; diff --git a/lib/src/language/ast/node.dart b/lib/src/language/ast/node.dart index 65bd60f2..1b6bebfe 100644 --- a/lib/src/language/ast/node.dart +++ b/lib/src/language/ast/node.dart @@ -7,7 +7,4 @@ abstract class Node { SourceLocation get end => span.end; String toSource(); - - @override - String toString() => '${runtimeType}: ${toSource()}'; } diff --git a/lib/src/language/ast/operation_definition.dart b/lib/src/language/ast/operation_definition.dart index c21bfcdd..ebc6ab65 100644 --- a/lib/src/language/ast/operation_definition.dart +++ b/lib/src/language/ast/operation_definition.dart @@ -11,10 +11,10 @@ class OperationDefinitionContext extends DefinitionContext { final List directives = []; final SelectionSetContext selectionSet; - bool get isMutation => TYPE.text == 'mutation'; - bool get isQuery => TYPE.text == 'query'; + bool get isMutation => TYPE?.text == 'mutation'; + bool get isQuery => TYPE?.text == 'query'; - String get name => NAME.text; + String get name => NAME?.text; OperationDefinitionContext( this.TYPE, this.NAME, this.variableDefinitions, this.selectionSet) { diff --git a/lib/src/language/parser.dart b/lib/src/language/parser.dart index 0de180fd..b7ec8a55 100644 --- a/lib/src/language/parser.dart +++ b/lib/src/language/parser.dart @@ -49,9 +49,69 @@ class Parser { DefinitionContext parseDefinition() => parseOperationDefinition() ?? parseFragmentDefinition(); - OperationDefinitionContext parseOperationDefinition() {} + OperationDefinitionContext parseOperationDefinition() { + var selectionSet = parseSelectionSet(); + if (selectionSet != null) + return new OperationDefinitionContext(null, null, null, selectionSet); + else { + if (next(TokenType.MUTATION) || next(TokenType.QUERY)) { + var TYPE = current; + if (next(TokenType.NAME)) { + var NAME = current; + var variables = parseVariableDefinitions(); + var dirs = parseDirectives(); + var selectionSet = parseSelectionSet(); + if (selectionSet != null) + return new OperationDefinitionContext( + TYPE, NAME, variables, selectionSet) + ..directives.addAll(dirs); + else + throw new SyntaxError.fromSourceLocation( + 'Expected selection set in fragment definition.', + NAME.span.end); + } else + throw new SyntaxError.fromSourceLocation( + 'Expected name after operation type "${TYPE.text}" in operation definition.', + TYPE.span.end); + } else + return null; + } + } - FragmentDefinitionContext parseFragmentDefinition() {} + FragmentDefinitionContext parseFragmentDefinition() { + if (next(TokenType.FRAGMENT)) { + var FRAGMENT = current; + if (next(TokenType.NAME)) { + var NAME = current; + if (next(TokenType.ON)) { + var ON = current; + var typeCondition = parseTypeCondition(); + if (typeCondition != null) { + var dirs = parseDirectives(); + var selectionSet = parseSelectionSet(); + if (selectionSet != null) + return new FragmentDefinitionContext( + FRAGMENT, NAME, ON, typeCondition, selectionSet) + ..directives.addAll(dirs); + else + throw new SyntaxError.fromSourceLocation( + 'Expected selection set in fragment definition.', + typeCondition.span.end); + } else + throw new SyntaxError.fromSourceLocation( + 'Expected type condition after "on" in fragment definition.', + ON.span.end); + } else + throw new SyntaxError.fromSourceLocation( + 'Expected "on" after name "${NAME.text}" in fragment definition.', + NAME.span.end); + } else + throw new SyntaxError.fromSourceLocation( + 'Expected name after "fragment" in fragment definition.', + FRAGMENT.span.end); + } else + return null; + } FragmentSpreadContext parseFragmentSpread() { if (next(TokenType.ELLIPSIS)) { @@ -164,7 +224,27 @@ class Parser { return null; } - VariableDefinitionsContext parseVariableDefinitions() {} + VariableDefinitionsContext parseVariableDefinitions() { + if (next(TokenType.LPAREN)) { + var LPAREN = current; + List defs = []; + VariableDefinitionContext def = parseVariableDefinition(); + + while (def != null) { + defs.add(def); + maybe(TokenType.COMMA); + def = parseVariableDefinition(); + } + + if (next(TokenType.RPAREN)) + return new VariableDefinitionsContext(LPAREN, current) + ..variableDefinitions.addAll(defs); + else + throw new SyntaxError.fromSourceLocation( + 'Expected ")" after variable definitions.', LPAREN.span.end); + } else + return null; + } VariableDefinitionContext parseVariableDefinition() { var variable = parseVariable(); diff --git a/lib/visitor.dart b/lib/visitor.dart new file mode 100644 index 00000000..1874022b --- /dev/null +++ b/lib/visitor.dart @@ -0,0 +1,125 @@ +import 'graphql_parser.dart'; + +class GraphQLVisitor { + visitDocument(DocumentContext ctx) { + ctx.definitions.forEach(visitDefinition); + } + + visitDefinition(DefinitionContext ctx) { + if (ctx is OperationDefinitionContext) + visitOperationDefinition(ctx); + else if (ctx is FragmentDefinitionContext) visitFragmentDefinition(ctx); + } + + visitOperationDefinition(OperationDefinitionContext ctx) { + if (ctx.variableDefinitions != null) + visitVariableDefinitions(ctx.variableDefinitions); + ctx.directives.forEach(visitDirective); + visitSelectionSet(ctx.selectionSet); + } + + visitFragmentDefinition(FragmentDefinitionContext ctx) { + visitTypeCondition(ctx.typeCondition); + ctx.directives.forEach(visitDirective); + visitSelectionSet(ctx.selectionSet); + } + + visitSelectionSet(SelectionSetContext ctx) { + ctx.selections.forEach(visitSelection); + } + + visitSelection(SelectionContext ctx) { + if (ctx.field != null) visitField(ctx.field); + if (ctx.fragmentSpread != null) visitFragmentSpread(ctx.fragmentSpread); + if (ctx.inlineFragment != null) visitInlineFragment(ctx.inlineFragment); + } + + visitInlineFragment(InlineFragmentContext ctx) { + visitTypeCondition(ctx.typeCondition); + ctx.directives.forEach(visitDirective); + visitSelectionSet(ctx.selectionSet); + } + + visitFragmentSpread(FragmentSpreadContext ctx) { + ctx.directives.forEach(visitDirective); + } + + visitField(FieldContext ctx) { + visitFieldName(ctx.fieldName); + ctx.arguments.forEach(visitArgument); + ctx.directives.forEach(visitDirective); + if (ctx.selectionSet != null) ; + visitSelectionSet(ctx.selectionSet); + } + + visitFieldName(FieldNameContext ctx) { + if (ctx.alias != null) visitAlias(ctx.alias); + } + + visitAlias(AliasContext ctx) {} + + visitDirective(DirectiveContext ctx) { + if (ctx.valueOrVariable != null) visitValueOrVariable(ctx.valueOrVariable); + if (ctx.argument != null) visitArgument(ctx.argument); + } + + visitArgument(ArgumentContext ctx) { + visitValueOrVariable(ctx.valueOrVariable); + } + + visitVariableDefinitions(VariableDefinitionsContext ctx) { + ctx.variableDefinitions.forEach(visitVariableDefinition); + } + + visitVariableDefinition(VariableDefinitionContext ctx) { + visitVariable(ctx.variable); + visitType(ctx.type); + if (ctx.defaultValue != null) visitDefaultValue(ctx.defaultValue); + } + + visitVariable(VariableContext ctx) {} + + visitValueOrVariable(ValueOrVariableContext ctx) { + if (ctx.variable != null) visitVariable(ctx.variable); + if (ctx.value != null) visitValue(ctx.value); + } + + visitDefaultValue(DefaultValueContext ctx) { + visitValue(ctx.value); + } + + visitValue(ValueContext ctx) { + if (ctx is StringValueContext) + visitStringValue(ctx); + else if (ctx is NumberValueContext) + visitNumberValue(ctx); + else if (ctx is BooleanValueContext) + visitBooleanValue(ctx); + else if (ctx is ArrayValueContext) visitArrayValue(ctx); + } + + visitStringValue(StringValueContext ctx) {} + + visitBooleanValue(BooleanValueContext ctx) {} + + visitNumberValue(NumberValueContext ctx) {} + + visitArrayValue(ArrayValueContext ctx) { + ctx.values.forEach(visitValue); + } + + visitTypeCondition(TypeConditionContext ctx) { + visitTypeName(ctx.typeName); + } + + visitType(TypeContext ctx) { + if (ctx.typeName != null) visitTypeName(ctx.typeName); + if (ctx.listType != null) visitListType(ctx.listType); + } + + visitListType(ListTypeContext ctx) { + visitType(ctx.type); + } + + visitTypeName(TypeNameContext ctx) {} +} diff --git a/pubspec.yaml b/pubspec.yaml index b25bcb5d..683983be 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: graphql_parser -version: 0.0.0 +version: 1.0.0 description: Parses GraphQL queries and schemas. author: Tobe O homepage: https://github.com/thosakwe/graphql_parser diff --git a/test/all.dart b/test/all.dart index 35dfdc22..fdc40728 100644 --- a/test/all.dart +++ b/test/all.dart @@ -1,6 +1,7 @@ import 'package:test/test.dart'; import 'argument_test.dart' as argument; import 'directive_test.dart' as directive; +import 'document_test.dart' as document; import 'field_test.dart' as field; import 'fragment_spread_test.dart' as fragment_spread; import 'inline_fragment_test.dart' as inline_fragment; @@ -13,6 +14,7 @@ import 'variable_test.dart' as variable; main() { group('argument', argument.main); group('directive', directive.main); + group('document', document.main); group('field', field.main); group('fragment spread', fragment_spread.main); group('inline fragment', inline_fragment.main); diff --git a/test/document_test.dart b/test/document_test.dart new file mode 100644 index 00000000..013cddf9 --- /dev/null +++ b/test/document_test.dart @@ -0,0 +1,100 @@ +import 'package:graphql_parser/graphql_parser.dart'; +import 'package:test/test.dart'; +import 'common.dart'; +import 'directive_test.dart'; +import 'field_test.dart'; +import 'selection_set_test.dart'; +import 'type_test.dart'; +import 'value_test.dart'; +import 'variable_definition_test.dart'; + +main() { + test('fragment', () { + var fragment = parse(''' + fragment PostInfo on Post { + description + comments { + id + } + } + ''').parseFragmentDefinition(); + + expect(fragment, isNotNull); + expect(fragment.name, 'PostInfo'); + expect(fragment.typeCondition.typeName.name, 'Post'); + expect( + fragment.selectionSet, + isSelectionSet([ + isField(fieldName: isFieldName('description')), + isField( + fieldName: isFieldName('comments'), + selectionSet: + isSelectionSet([isField(fieldName: isFieldName('id'))])), + ])); + }); + + test('fragment exceptions', () { + expect( + () => parse('fragment').parseFragmentDefinition(), throwsSyntaxError); + expect(() => parse('fragment foo').parseFragmentDefinition(), + throwsSyntaxError); + expect(() => parse('fragment foo on').parseFragmentDefinition(), + throwsSyntaxError); + expect(() => parse('fragment foo on bar').parseFragmentDefinition(), + throwsSyntaxError); + }); + + group('operation', () { + test('with selection set', () { + var op = parse('{foo, bar: baz}').parseOperationDefinition(); + expect(op.variableDefinitions, isNull); + expect(op.isQuery, isFalse); + expect(op.isMutation, isFalse); + expect(op.name, isNull); + expect( + op.selectionSet, + isSelectionSet([ + isField(fieldName: isFieldName('foo')), + isField(fieldName: isFieldName('bar', alias: 'baz')) + ])); + }); + + test('with operation type', () { + var doc = + parse(r'query foo ($one: [int] = 2) @foo @bar: 2 {foo, bar: baz}') + .parseDocument(); + expect(doc.definitions, hasLength(1)); + expect(doc.definitions.first, + const isInstanceOf()); + var op = doc.definitions.first as OperationDefinitionContext; + expect(op.isMutation, isFalse); + expect(op.isQuery, isTrue); + + expect(op.variableDefinitions.variableDefinitions, hasLength(1)); + expect( + op.variableDefinitions.variableDefinitions.first, + isVariableDefinition('one', + type: isListType(isType('int'), isNullable: true), + defaultValue: isValue(2))); + + expect(op.directives, hasLength(2)); + expect(op.directives[0], isDirective('foo')); + expect(op.directives[1], isDirective('bar', valueOrVariable: equals(2))); + + expect(op.selectionSet, isNotNull); + expect( + op.selectionSet, + isSelectionSet([ + isField(fieldName: isFieldName('foo')), + isField(fieldName: isFieldName('bar', alias: 'baz')) + ])); + }); + + test('exceptions', () { + expect( + () => parse('query').parseOperationDefinition(), throwsSyntaxError); + expect(() => parse('query foo()').parseOperationDefinition(), + throwsSyntaxError); + }); + }); +} diff --git a/test/field_test.dart b/test/field_test.dart index d10c6ac9..7f5acc41 100644 --- a/test/field_test.dart +++ b/test/field_test.dart @@ -109,22 +109,22 @@ class _IsField extends Matcher { } class _IsFieldName extends Matcher { - final String name, alias; + final String name, realName; - _IsFieldName(this.name, this.alias); + _IsFieldName(this.name, this.realName); @override Description describe(Description description) { - if (alias != null) - return description.add('is field with name "$name" and alias "$alias"'); + if (realName != null) + return description.add('is field with name "$name" and alias "$realName"'); return description.add('is field with name "$name"'); } @override bool matches(item, Map matchState) { var fieldName = item is FieldNameContext ? item : parseFieldName(item); - if (alias != null) - return fieldName.alias?.name == name && fieldName.alias?.alias == alias; + if (realName != null) + return fieldName.alias?.alias == name && fieldName.alias?.name == realName; else return fieldName.name == name; } diff --git a/test/variable_definition_test.dart b/test/variable_definition_test.dart index b22a09e7..bd772479 100644 --- a/test/variable_definition_test.dart +++ b/test/variable_definition_test.dart @@ -21,6 +21,8 @@ main() { expect(() => parseVariableDefinition(r'$foo'), throwsSyntaxError); expect(() => parseVariableDefinition(r'$foo:'), throwsSyntaxError); expect(() => parseVariableDefinition(r'$foo: int ='), throwsSyntaxError); + expect(() => parse(r'($foo: int = 2').parseVariableDefinitions(), + throwsSyntaxError); }); }