From 1a80b66ea5b387434289582474e7fa2fd6a5256e Mon Sep 17 00:00:00 2001 From: Tobe O Date: Thu, 2 Aug 2018 15:22:16 -0400 Subject: [PATCH] Type introspection base --- angel_graphql/example/main.dart | 4 +- graphql_schema/example/todo.dart | 2 +- graphql_schema/lib/introspection.dart | 112 +++++++++++++++++++++--- graphql_schema/lib/src/gen.dart | 4 +- graphql_schema/lib/src/object_type.dart | 3 +- graphql_schema/lib/src/scalar.dart | 35 ++++++-- graphql_schema/lib/src/type.dart | 24 +++++ graphql_schema/test/common.dart | 6 +- graphql_server/lib/graphql_server.dart | 7 +- graphql_server/lib/mirrors.dart | 12 ++- graphql_server/test/query_test.dart | 4 +- 11 files changed, 182 insertions(+), 31 deletions(-) diff --git a/angel_graphql/example/main.dart b/angel_graphql/example/main.dart index d16c6dd1..c4deebb4 100644 --- a/angel_graphql/example/main.dart +++ b/angel_graphql/example/main.dart @@ -1,10 +1,10 @@ import 'package:angel_framework/angel_framework.dart'; import 'package:angel_graphql/angel_graphql.dart'; import 'package:angel_serialize/angel_serialize.dart'; -import 'package:logging/logging.dart'; import 'package:graphql_schema/graphql_schema.dart'; import 'package:graphql_server/graphql_server.dart'; import 'package:graphql_server/mirrors.dart'; +import 'package:logging/logging.dart'; main() async { var app = new Angel(); @@ -19,7 +19,7 @@ main() async { var todoService = app.use('api/todos', new MapService()) as Service; - var api = objectType('api', [ + var api = objectType('api', fields: [ field( 'todo', type: listType(objectTypeFromDartType(Todo)), diff --git a/graphql_schema/example/todo.dart b/graphql_schema/example/todo.dart index b837d612..32f0d93d 100644 --- a/graphql_schema/example/todo.dart +++ b/graphql_schema/example/todo.dart @@ -1,7 +1,7 @@ import 'package:graphql_schema/graphql_schema.dart'; final GraphQLSchema todoSchema = new GraphQLSchema( - query: objectType('Todo', [ + query: objectType('Todo', fields: [ field( 'text', type: graphQLString.nonNullable(), diff --git a/graphql_schema/lib/introspection.dart b/graphql_schema/lib/introspection.dart index 5903253d..cfe1ac8a 100644 --- a/graphql_schema/lib/introspection.dart +++ b/graphql_schema/lib/introspection.dart @@ -1,16 +1,33 @@ import 'package:graphql_schema/graphql_schema.dart'; // TODO: How to handle custom types??? -GraphQLSchema reflectSchema(GraphQLSchema schema) { - var objectTypes = _fetchAllTypes(schema); - var typeType = _reflectSchemaTypes(schema); +GraphQLSchema reflectSchema(GraphQLSchema schema, List allTypes) { + var objectTypes = fetchAllTypes(schema); + allTypes.addAll(objectTypes); + var typeType = _reflectSchemaTypes(); - var schemaType = objectType('__Schema', [ + var schemaType = objectType('__Schema', fields: [ field( 'types', type: listType(typeType), resolve: (_, __) => objectTypes, ), + field( + 'queryType', + type: typeType, + resolve: (_, __) => schema.query, + ), + field( + 'mutationType', + type: typeType, + resolve: (_, __) => schema.mutation, + ), + ]); + + allTypes.addAll([ + typeType, + schemaType, + _reflectFields(), ]); var fields = [ @@ -37,38 +54,109 @@ GraphQLSchema reflectSchema(GraphQLSchema schema) { fields.addAll(schema.query.fields); return new GraphQLSchema( - query: objectType(schema.query.name, fields), + query: objectType(schema.query.name, fields: fields), mutation: schema.mutation, ); } -GraphQLObjectType _reflectSchemaTypes(GraphQLSchema schema) { +GraphQLObjectType _typeType; + +GraphQLObjectType _reflectSchemaTypes() { + if (_typeType == null) { + _typeType = _createTypeType(); + _typeType.fields.add( + field( + 'ofType', + type: _reflectSchemaTypes(), + resolve: (type, _) { + if (type is GraphQLListType) + return type.innerType; + else if (type is GraphQLNonNullableType) return type.innerType; + return null; + }, + ), + ); + + var fieldType = _reflectFields(); + var typeField = fieldType.fields + .firstWhere((f) => f.name == 'type', orElse: () => null); + + if (typeField == null) { + fieldType.fields.add( + field( + 'type', + type: _reflectSchemaTypes(), + resolve: (f, _) => (f as GraphQLField).type, + ), + ); + } + } + + return _typeType; +} + +GraphQLObjectType _createTypeType() { var fieldType = _reflectFields(); - return objectType('__Type', [ + return objectType('__Type', fields: [ field( 'name', type: graphQLString, - resolve: (type, _) => (type as GraphQLObjectType).name, + resolve: (type, _) => (type as GraphQLType).name, + ), + field( + 'description', + type: graphQLString, + resolve: (type, _) => (type as GraphQLType).description, ), field( 'kind', type: graphQLString, - resolve: (type, _) => 'OBJECT', // TODO: Union, interface + resolve: (type, _) { + var t = type as GraphQLType; + + if (t is GraphQLScalarType) + return 'SCALAR'; + else if (t is GraphQLObjectType) + return 'OBJECT'; + else if (t is GraphQLListType) + return 'LIST'; + else if (t is GraphQLNonNullableType) + return 'NON_NULL'; + else + throw new UnsupportedError( + 'Cannot get the kind of $t.'); // TODO: Interface + union + }, ), field( 'fields', type: listType(fieldType), - resolve: (type, _) => (type as GraphQLObjectType).fields, + resolve: (type, _) => type is GraphQLObjectType ? type.fields : [], ), ]); } +GraphQLObjectType _fieldType; + GraphQLObjectType _reflectFields() { - return objectType('__Field', []); + if (_fieldType == null) { + _fieldType = _createFieldType(); + } + + return _fieldType; } -List _fetchAllTypes(GraphQLSchema schema) { +GraphQLObjectType _createFieldType() { + return objectType('__Field', fields: [ + field( + 'name', + type: graphQLString, + resolve: (f, _) => (f as GraphQLField).name, + ), + ]); +} + +List fetchAllTypes(GraphQLSchema schema) { var typess = []; typess.addAll(_fetchAllTypesFromObject(schema.query)); diff --git a/graphql_schema/lib/src/gen.dart b/graphql_schema/lib/src/gen.dart index 5b833bf2..31d6a5c3 100644 --- a/graphql_schema/lib/src/gen.dart +++ b/graphql_schema/lib/src/gen.dart @@ -1,8 +1,8 @@ part of graphql_schema.src.schema; GraphQLObjectType objectType(String name, - [Iterable fields = const []]) => - new GraphQLObjectType(name)..fields.addAll(fields ?? []); + {String description, Iterable fields = const []}) => + new GraphQLObjectType(name, description)..fields.addAll(fields ?? []); GraphQLField field(String name, {Iterable> arguments: const [], diff --git a/graphql_schema/lib/src/object_type.dart b/graphql_schema/lib/src/object_type.dart index 8f98a72e..264f5521 100644 --- a/graphql_schema/lib/src/object_type.dart +++ b/graphql_schema/lib/src/object_type.dart @@ -4,9 +4,10 @@ class GraphQLObjectType extends GraphQLType, Map> with _NonNullableMixin, Map> { final String name; + final String description; final List fields = []; - GraphQLObjectType(this.name); + GraphQLObjectType(this.name, this.description); @override ValidationResult> validate(String key, Map input) { diff --git a/graphql_schema/lib/src/scalar.dart b/graphql_schema/lib/src/scalar.dart index 21210bdf..2041d3c7 100644 --- a/graphql_schema/lib/src/scalar.dart +++ b/graphql_schema/lib/src/scalar.dart @@ -17,15 +17,20 @@ final GraphQLScalarType graphQLDate = new _GraphQLDateType._(); /// A signed 32‐bit integer. -final GraphQLScalarType graphQLInt = - new _GraphQLNumType((x) => x is int, 'an integer'); +final GraphQLScalarType graphQLInt = new _GraphQLNumType( + 'Int', 'A signed 64-bit integer.', (x) => x is int, 'an integer'); /// A signed double-precision floating-point value. final GraphQLScalarType graphQLFloat = - new _GraphQLNumType((x) => x is double, 'a float'); + new _GraphQLNumType( + 'Float', + 'A signed double-precision floating-point value.', + (x) => x is double, + 'a float'); abstract class GraphQLScalarType - extends GraphQLType with _NonNullableMixin {} + extends GraphQLType + with _NonNullableMixin {} typedef bool _NumVerifier(x); @@ -35,6 +40,12 @@ class _GraphQLBoolType extends GraphQLScalarType { return value; } + @override + String get name => 'Boolean'; + + @override + String get description => 'A boolean value; can be either true or false.'; + @override ValidationResult validate(String key, input) { if (input != null && input is! bool) @@ -50,10 +61,12 @@ class _GraphQLBoolType extends GraphQLScalarType { } class _GraphQLNumType extends GraphQLScalarType { + final String name; + final String description; final _NumVerifier verifier; final String expected; - _GraphQLNumType(this.verifier, this.expected); + _GraphQLNumType(this.name, this.description, this.verifier, this.expected); @override ValidationResult validate(String key, input) { @@ -78,6 +91,12 @@ class _GraphQLNumType extends GraphQLScalarType { class _GraphQLStringType extends GraphQLScalarType { _GraphQLStringType._(); + @override + String get name => 'String'; + + @override + String get description => 'A character sequence.'; + @override String serialize(String value) => value; @@ -95,6 +114,12 @@ class _GraphQLDateType extends GraphQLScalarType with _NonNullableMixin { _GraphQLDateType._(); + @override + String get name => 'Date'; + + @override + String get description => 'An ISO0-8601 Date.'; + @override String serialize(DateTime value) => value.toIso8601String(); diff --git a/graphql_schema/lib/src/type.dart b/graphql_schema/lib/src/type.dart index db4e092a..3944f681 100644 --- a/graphql_schema/lib/src/type.dart +++ b/graphql_schema/lib/src/type.dart @@ -1,9 +1,16 @@ part of graphql_schema.src.schema; abstract class GraphQLType { + String get name; + + String get description; + Serialized serialize(Value value); + Value deserialize(Serialized serialized); + ValidationResult validate(String key, Serialized input); + GraphQLType nonNullable(); } @@ -16,8 +23,16 @@ class GraphQLListType extends GraphQLType, List> with _NonNullableMixin, List> { final GraphQLType innerType; + GraphQLListType(this.innerType); + @override + String get name => null; + + @override + String get description => + 'A list of items of type ${innerType.name ?? '(${innerType.description}).'}'; + @override ValidationResult> validate( String key, List input) { @@ -55,6 +70,7 @@ class GraphQLListType abstract class _NonNullableMixin implements GraphQLType { GraphQLType _nonNullableCache; + GraphQLType nonNullable() => _nonNullableCache ??= new GraphQLNonNullableType._(this); } @@ -62,8 +78,16 @@ abstract class _NonNullableMixin class GraphQLNonNullableType extends GraphQLType { final GraphQLType innerType; + GraphQLNonNullableType._(this.innerType); + @override + String get name => innerType.name; + + @override + String get description => + 'A non-nullable binding to ${innerType.name ?? '(${innerType.description}).'}'; + @override GraphQLType nonNullable() { throw new UnsupportedError( diff --git a/graphql_schema/test/common.dart b/graphql_schema/test/common.dart index 5b5ae5d2..a9f9a0b4 100644 --- a/graphql_schema/test/common.dart +++ b/graphql_schema/test/common.dart @@ -1,14 +1,14 @@ import 'package:graphql_schema/graphql_schema.dart'; -final GraphQLObjectType pokemonType = objectType('Pokemon', [ +final GraphQLObjectType pokemonType = objectType('Pokemon', fields:[ field('species', type: graphQLString), field('catch_date', type: graphQLDate) ]); final GraphQLObjectType trainerType = - objectType('Trainer', [field('name', type: graphQLString)]); + objectType('Trainer', fields:[field('name', type: graphQLString)]); -final GraphQLObjectType pokemonRegionType = objectType('PokemonRegion', [ +final GraphQLObjectType pokemonRegionType = objectType('PokemonRegion', fields:[ field('trainer', type: trainerType), field('pokemon_species', type: listType(pokemonType)) ]); diff --git a/graphql_server/lib/graphql_server.dart b/graphql_server/lib/graphql_server.dart index dcd4bf53..8b5f3035 100644 --- a/graphql_server/lib/graphql_server.dart +++ b/graphql_server/lib/graphql_server.dart @@ -10,7 +10,12 @@ class GraphQL { GraphQL(GraphQLSchema schema, {bool introspect: true}) : _schema = schema { if (introspect) { - _schema = reflectSchema(_schema); + var allTypes = []; + _schema = reflectSchema(_schema, allTypes); + + for (var type in allTypes) { + customTypes[type.name] = type; + } } if (_schema.query != null) customTypes[_schema.query.name] = _schema.query; diff --git a/graphql_server/lib/mirrors.dart b/graphql_server/lib/mirrors.dart index 5eb8d60d..329a0d9d 100644 --- a/graphql_server/lib/mirrors.dart +++ b/graphql_server/lib/mirrors.dart @@ -23,8 +23,13 @@ GraphQLType _objectTypeFromDartType(Type type, [List typeArguments]) { return graphQLBoolean; } else if (type == int) { return graphQLInt; - } else if (type == double || type == num) { + } else if (type == double) { return graphQLFloat; + } else if (type == num) { + throw new UnsupportedError( + 'Cannot convert `num` to a GraphQL type. Choose `int` or `float` instead.'); + } else if (type == Null) { + throw new UnsupportedError('Cannot convert `Null` to a GraphQL type.'); } else if (type == String) { return graphQLString; } else if (type == DateTime) { @@ -57,7 +62,10 @@ GraphQLObjectType objectTypeFromClassMirror(ClassMirror mirror) { } } - return objectType(MirrorSystem.getName(mirror.simpleName), fields); + return objectType( + MirrorSystem.getName(mirror.simpleName), + fields: fields, + ); } GraphQLField fieldFromGetter( diff --git a/graphql_server/test/query_test.dart b/graphql_server/test/query_test.dart index 8ca391fb..e9775dd0 100644 --- a/graphql_server/test/query_test.dart +++ b/graphql_server/test/query_test.dart @@ -4,7 +4,7 @@ import 'package:test/test.dart'; void main() { test('single element', () async { - var todoType = objectType('todo', [ + var todoType = objectType('todo',fields: [ field( 'text', type: graphQLString, @@ -18,7 +18,7 @@ void main() { ]); var schema = graphQLSchema( - query: objectType('api', [ + query: objectType('api', fields:[ field( 'todos', type: listType(todoType),