Mysterious returns of "null"

This commit is contained in:
Tobe O 2019-03-28 23:37:56 -04:00
parent 9c876a643c
commit 943a3cb53b
14 changed files with 329 additions and 85 deletions

View file

@ -1,11 +0,0 @@
targets:
_standalone:
sources:
- lib/src/models/character.dart
- lib/src/models/droid.dart
- lib/src/models/starship.dart
$default:
dependencies:
- ":_standalone"
sources:
- lib/src/models/human.dart

View file

@ -1,12 +1,12 @@
import 'package:angel_model/angel_model.dart';
import 'package:graphql_schema/graphql_schema.dart';
import 'episode.dart';
part 'character.g.dart';
@graphQLClass
abstract class Character {
String get id;
String get name;
List<Episode> get appearsIn;
List<Character> get friends;
// List<Episode> get appearsIn;
}

View file

@ -0,0 +1,13 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'character.dart';
// **************************************************************************
// _GraphQLGenerator
// **************************************************************************
/// Auto-generated from [Character].
final GraphQLObjectType characterGraphQLType = objectType('Character',
isInterface: true,
interfaces: [],
fields: [field('id', graphQLString), field('name', graphQLString)]);

View file

@ -1,17 +1,23 @@
import 'package:angel_model/angel_model.dart';
import 'package:angel_serialize/angel_serialize.dart';
import 'package:collection/collection.dart';
import 'package:graphql_schema/graphql_schema.dart';
import 'character.dart';
import 'episode.dart';
part 'droid.g.dart';
@serializable
abstract class _Droid extends Model implements Character {
@graphQLClass
@GraphQLDocumentation(description: 'Beep! Boop!')
abstract class _Droid extends Model implements Character {
String get id;
String get name;
List<Episode> get appearsIn;
@GraphQLDocumentation(
description: 'The list of episodes this droid appears in.')
List<Episode> get appearsIn;
/// Doc comments automatically become GraphQL descriptions.
List<Character> get friends;
}

View file

@ -11,7 +11,7 @@ class Droid extends _Droid {
Droid(
{this.id,
this.name,
List<dynamic> appearsIn,
List<Episode> appearsIn,
List<Character> friends,
this.createdAt,
this.updatedAt})
@ -25,7 +25,7 @@ class Droid extends _Droid {
final String name;
@override
final List<dynamic> appearsIn;
final List<Episode> appearsIn;
@override
final List<Character> friends;
@ -39,7 +39,7 @@ class Droid extends _Droid {
Droid copyWith(
{String id,
String name,
List<dynamic> appearsIn,
List<Episode> appearsIn,
List<Character> friends,
DateTime createdAt,
DateTime updatedAt}) {
@ -56,7 +56,7 @@ class Droid extends _Droid {
return other is _Droid &&
other.id == id &&
other.name == name &&
const ListEquality<dynamic>(const DefaultEquality())
const ListEquality<Episode>(const DefaultEquality<Episode>())
.equals(other.appearsIn, appearsIn) &&
const ListEquality<Character>(const DefaultEquality<Character>())
.equals(other.friends, friends) &&
@ -84,7 +84,7 @@ abstract class DroidSerializer {
id: map['id'] as String,
name: map['name'] as String,
appearsIn: map['appears_in'] is Iterable
? (map['appears_in'] as Iterable).cast<dynamic>().toList()
? (map['appears_in'] as Iterable).cast<Episode>().toList()
: null,
friends: map['friends'] is Iterable
? (map['friends'] as Iterable).cast<Character>().toList()
@ -138,3 +138,27 @@ abstract class DroidFields {
static const String updatedAt = 'updated_at';
}
// **************************************************************************
// _GraphQLGenerator
// **************************************************************************
/// Auto-generated from [Droid].
final GraphQLObjectType droidGraphQLType = objectType('Droid',
isInterface: false,
description: 'Beep! Boop!',
interfaces: [
characterGraphQLType
],
fields: [
field('id', graphQLString),
field('name', graphQLString),
field('appears_in', listOf(episodeGraphQLType),
description: 'The list of episodes this droid appears in.'),
field('friends', listOf(characterGraphQLType),
description:
'Doc comments automatically become GraphQL descriptions.'),
field('created_at', graphQLDate),
field('updated_at', graphQLDate),
field('idAsInt', graphQLInt)
]);

View file

@ -1,7 +1,9 @@
import 'package:graphql_schema/graphql_schema.dart';
part 'episode.g.dart';
@GraphQLDocumentation(
description: 'The episodes of the Star Wars original trilogy.')
@graphQLClass
enum Episode {
NEWHOPE,
EMPIRE,

View file

@ -0,0 +1,12 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'episode.dart';
// **************************************************************************
// _GraphQLGenerator
// **************************************************************************
/// Auto-generated from [Episode].
final GraphQLEnumType<String> episodeGraphQLType = enumTypeFromStrings(
'Episode', const ['NEWHOPE', 'EMPIRE', 'JEDI'],
description: 'The episodes of the Star Wars original trilogy.');

View file

@ -4,10 +4,10 @@ import 'package:collection/collection.dart';
import 'package:graphql_schema/graphql_schema.dart';
import 'character.dart';
import 'episode.dart';
import 'starship.dart';
part 'human.g.dart';
@serializable
@graphQLClass
abstract class _Human extends Model implements Character {
// @GraphQLDocumentation(description: "This human's name, of course.")
// String name;
@ -26,8 +26,6 @@ abstract class _Human extends Model implements Character {
int get totalCredits;
List<Starship> get starships;
// Human(
// {this.name,
// this.friends,

View file

@ -11,15 +11,13 @@ class Human extends _Human {
Human(
{this.id,
this.name,
List<dynamic> appearsIn,
List<Episode> appearsIn,
List<Character> friends,
this.totalCredits,
List<dynamic> starships,
this.createdAt,
this.updatedAt})
: this.appearsIn = new List.unmodifiable(appearsIn ?? []),
this.friends = new List.unmodifiable(friends ?? []),
this.starships = new List.unmodifiable(starships ?? []);
this.friends = new List.unmodifiable(friends ?? []);
@override
final String id;
@ -28,7 +26,7 @@ class Human extends _Human {
final String name;
@override
final List<dynamic> appearsIn;
final List<Episode> appearsIn;
@override
final List<Character> friends;
@ -36,9 +34,6 @@ class Human extends _Human {
@override
final int totalCredits;
@override
final List<dynamic> starships;
@override
final DateTime createdAt;
@ -48,10 +43,9 @@ class Human extends _Human {
Human copyWith(
{String id,
String name,
List<dynamic> appearsIn,
List<Episode> appearsIn,
List<Character> friends,
int totalCredits,
List<dynamic> starships,
DateTime createdAt,
DateTime updatedAt}) {
return new Human(
@ -60,7 +54,6 @@ class Human extends _Human {
appearsIn: appearsIn ?? this.appearsIn,
friends: friends ?? this.friends,
totalCredits: totalCredits ?? this.totalCredits,
starships: starships ?? this.starships,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt);
}
@ -69,29 +62,19 @@ class Human extends _Human {
return other is _Human &&
other.id == id &&
other.name == name &&
const ListEquality<dynamic>(const DefaultEquality())
const ListEquality<Episode>(const DefaultEquality<Episode>())
.equals(other.appearsIn, appearsIn) &&
const ListEquality<Character>(const DefaultEquality<Character>())
.equals(other.friends, friends) &&
other.totalCredits == totalCredits &&
const ListEquality<dynamic>(const DefaultEquality())
.equals(other.starships, starships) &&
other.createdAt == createdAt &&
other.updatedAt == updatedAt;
}
@override
int get hashCode {
return hashObjects([
id,
name,
appearsIn,
friends,
totalCredits,
starships,
createdAt,
updatedAt
]);
return hashObjects(
[id, name, appearsIn, friends, totalCredits, createdAt, updatedAt]);
}
Map<String, dynamic> toJson() {
@ -109,15 +92,12 @@ abstract class HumanSerializer {
id: map['id'] as String,
name: map['name'] as String,
appearsIn: map['appears_in'] is Iterable
? (map['appears_in'] as Iterable).cast<dynamic>().toList()
? (map['appears_in'] as Iterable).cast<Episode>().toList()
: null,
friends: map['friends'] is Iterable
? (map['friends'] as Iterable).cast<Character>().toList()
: null,
totalCredits: map['total_credits'] as int,
starships: map['starships'] is Iterable
? (map['starships'] as Iterable).cast<dynamic>().toList()
: null,
createdAt: map['created_at'] != null
? (map['created_at'] is DateTime
? (map['created_at'] as DateTime)
@ -140,7 +120,6 @@ abstract class HumanSerializer {
'appears_in': model.appearsIn,
'friends': model.friends,
'total_credits': model.totalCredits,
'starships': model.starships,
'created_at': model.createdAt?.toIso8601String(),
'updated_at': model.updatedAt?.toIso8601String()
};
@ -154,7 +133,6 @@ abstract class HumanFields {
appearsIn,
friends,
totalCredits,
starships,
createdAt,
updatedAt
];
@ -169,9 +147,26 @@ abstract class HumanFields {
static const String totalCredits = 'total_credits';
static const String starships = 'starships';
static const String createdAt = 'created_at';
static const String updatedAt = 'updated_at';
}
// **************************************************************************
// _GraphQLGenerator
// **************************************************************************
/// Auto-generated from [Human].
final GraphQLObjectType humanGraphQLType =
objectType('Human', isInterface: false, interfaces: [
characterGraphQLType
], fields: [
field('id', graphQLString),
field('name', graphQLString),
field('appears_in', listOf(episodeGraphQLType)),
field('friends', listOf(characterGraphQLType)),
field('total_credits', graphQLInt),
field('created_at', graphQLDate),
field('updated_at', graphQLDate),
field('idAsInt', graphQLInt)
]);

View file

@ -1,8 +1,10 @@
import 'package:angel_model/angel_model.dart';
import 'package:angel_serialize/angel_serialize.dart';
import 'package:graphql_schema/graphql_schema.dart';
part 'starship.g.dart';
@serializable
@graphQLClass
abstract class _Starship extends Model {
String get name;
int get length;

View file

@ -113,3 +113,18 @@ abstract class StarshipFields {
static const String updatedAt = 'updated_at';
}
// **************************************************************************
// _GraphQLGenerator
// **************************************************************************
/// Auto-generated from [Starship].
final GraphQLObjectType starshipGraphQLType =
objectType('Starship', isInterface: false, interfaces: [], fields: [
field('id', graphQLString),
field('name', graphQLString),
field('length', graphQLInt),
field('created_at', graphQLDate),
field('updated_at', graphQLDate),
field('idAsInt', graphQLInt)
]);

View file

@ -2,30 +2,24 @@ import 'dart:async';
import 'dart:math';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_graphql/angel_graphql.dart';
import 'package:angel_typed_service/angel_typed_service.dart';
import 'package:graphql_schema/graphql_schema.dart';
import 'package:graphql_server/graphql_server.dart';
import 'package:graphql_server/mirrors.dart';
import 'src/models/models.dart';
Future configureServer(Angel app) async {
// Create standard Angel services. Note that these will also *automatically* be
// exposed via a REST API as well.
var droidService = mountService<Droid>(app, '/api/droids');
var humansService = mountService<Human>(app, '/api/humans');
var starshipService = mountService<Starship>(app, '/api/starships');
var droidService = app.use('/api/droids', MapService());
var humansService = app.use('/api/humans', MapService());
var starshipService = app.use('/api/starships', MapService());
var rnd = Random();
// Create the GraphQL schema.
// This code uses dart:mirrors to easily create GraphQL types from Dart PODO's.
var droidType = convertDartClass(Droid);
var episodeType = convertDartType(Episode);
var humanType = convertDartClass(Human);
var starshipType = convertDartType(Starship);
// `package:graphql_generator` has generated schemas for some of our
// classes.
// A Hero can be either a Droid or Human; create a union type that represents this.
var heroType = GraphQLUnionType('Hero', [droidType, humanType]);
var heroType = GraphQLUnionType('Hero', [droidGraphQLType, humanGraphQLType]);
// Create the query type.
//
@ -37,19 +31,19 @@ Future configureServer(Angel app) async {
fields: [
field(
'droids',
listOf(droidType.nonNullable()),
listOf(droidGraphQLType.nonNullable()),
description: 'All droids in the known galaxy.',
resolve: resolveViaServiceIndex(droidService),
),
field(
'humans',
listOf(humanType.nonNullable()),
listOf(humanGraphQLType.nonNullable()),
description: 'All humans in the known galaxy.',
resolve: resolveViaServiceIndex(humansService),
),
field(
'starships',
listOf(starshipType.nonNullable()),
listOf(starshipGraphQLType.nonNullable()),
description: 'All starships in the known galaxy.',
resolve: resolveViaServiceIndex(starshipService),
),
@ -59,7 +53,7 @@ Future configureServer(Angel app) async {
description:
'Finds a random hero within the known galaxy, whether a Droid or Human.',
inputs: [
GraphQLFieldInput('ep', episodeType),
GraphQLFieldInput('ep', episodeGraphQLType),
],
resolve: randomHeroResolver(droidService, humansService, rnd),
),
@ -68,7 +62,7 @@ Future configureServer(Angel app) async {
// Convert our object types to input objects, so that they can be passed to
// mutations.
var humanChangesType = humanType.toInputObject('HumanChanges');
var humanChangesType = humanGraphQLType.toInputObject('HumanChanges');
// Create the mutation type.
var mutationType = objectType(
@ -77,7 +71,7 @@ Future configureServer(Angel app) async {
// We'll use the `modify_human` mutation to modify a human in the database.
field(
'modify_human',
humanType.nonNullable(),
humanGraphQLType.nonNullable(),
description: 'Modifies a human in the database.',
inputs: [
GraphQLFieldInput('id', graphQLId.nonNullable()),
@ -135,10 +129,6 @@ Future configureServer(Angel app) async {
});
}
Service<String, dynamic> mountService<T extends Model>(
Angel app, String path) =>
app.use(path, TypedService<String, T>(MapService()));
GraphQLFieldResolver randomHeroResolver(
Service droidService, Service humansService, Random rnd) {
return (_, args) async {

View file

@ -6,7 +6,6 @@ dependencies:
path: ../angel_graphql
angel_hot: ^2.0.0-alpha
angel_serialize: ^2.0.0
angel_typed_service: ^1.0.0
io: ^0.3.2
dev_dependencies:
angel_serialize_generator: ^2.0.0

View file

@ -1,10 +1,14 @@
import 'dart:async';
import 'dart:mirrors';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:angel_model/angel_model.dart';
import 'package:angel_serialize_generator/build_context.dart';
import 'package:angel_serialize_generator/context.dart';
import 'package:build/build.dart';
import 'package:code_builder/code_builder.dart';
import 'package:graphql_schema/graphql_schema.dart';
import 'package:recase/recase.dart';
import 'package:source_gen/source_gen.dart';
/// Generates GraphQL schemas, statically.
@ -12,23 +16,218 @@ Builder graphQLBuilder(_) {
return SharedPartBuilder([_GraphQLGenerator()], 'graphql_generator');
}
var _docComment = RegExp(r'^/// ', multiLine: true);
var _graphQLDoc = TypeChecker.fromRuntime(GraphQLDocumentation);
var _graphQLClassTypeChecker = TypeChecker.fromRuntime(GraphQLClass);
class _GraphQLGenerator extends GeneratorForAnnotation<GraphQLClass> {
@override
Future<String> generateForAnnotatedElement(
Element element, ConstantReader annotation, BuildStep buildStep) async {
if (element is ClassElement) {
var ctx = await buildContext(
element, annotation, buildStep, buildStep.resolver, false);
var lib = buildSchemaLibrary(ctx);
var ctx = element.isEnum
? null
: await buildContext(
element,
annotation,
buildStep,
buildStep.resolver,
serializableTypeChecker.hasAnnotationOf(element));
var lib = buildSchemaLibrary(element, ctx, annotation);
return lib.accept(DartEmitter()).toString();
} else {
throw UnsupportedError('@GraphQLClass() is only supported on classes.');
}
}
Library buildSchemaLibrary(BuildContext ctx) {
bool isInterface(ClassElement clazz) {
return clazz.isAbstract && !serializableTypeChecker.hasAnnotationOf(clazz);
}
bool _isGraphQLClass(InterfaceType clazz) {
var search = clazz;
while (search != null) {
if (_graphQLClassTypeChecker.hasAnnotationOf(search.element)) return true;
search = search.superclass;
}
return false;
}
Expression _inferType(String className, String name, DartType type) {
// Firstly, check if it's a GraphQL class.
if (type is InterfaceType && _isGraphQLClass(type)) {
var c = type;
var name = serializableTypeChecker.hasAnnotationOf(c.element) &&
c.name.startsWith('_')
? c.name.substring(1)
: c.name;
var rc = ReCase(name);
return refer('${rc.snakeCase}GraphQLType');
}
// Next, check if this is the "id" field of a `Model`.
if (TypeChecker.fromRuntime(Model).isAssignableFromType(type) &&
name == 'id') {
return refer('graphQLId');
}
var primitive = {
String: 'graphQLString',
int: 'graphQLInt',
double: 'graphQLFloat',
bool: 'graphQLBoolean',
DateTime: 'graphQLDate'
};
// Check to see if it's a primitive type.
for (var entry in primitive.entries) {
if (TypeChecker.fromRuntime(entry.key).isAssignableFromType(type)) {
return refer(entry.value);
}
}
// Next, check to see if it's a List.
if (type is InterfaceType &&
type.typeArguments.isNotEmpty &&
TypeChecker.fromRuntime(Iterable).isAssignableFromType(type)) {
var arg = type.typeArguments[0];
var inner = _inferType(className, name, arg);
return refer('listOf').call([inner]);
}
// Nothing else is allowed.
throw 'Cannot infer the GraphQL type for field $className.$name (type=$type).';
}
void _applyDescription(
Map<String, Expression> named, Element element, String docComment) {
String docString = docComment;
if (docString == null && _graphQLDoc.hasAnnotationOf(element)) {
var ann = _graphQLDoc.firstAnnotationOf(element);
var cr = ConstantReader(ann);
docString = cr.peek('description')?.stringValue;
}
if (docString != null) {
named['description'] = literalString(
docString.replaceAll(_docComment, '').replaceAll('\n', '\\n'));
}
}
Library buildSchemaLibrary(
ClassElement clazz, BuildContext ctx, ConstantReader ann) {
return Library((b) {
var clazz = ctx.clazz;
// Generate a top-level xGraphQLType object
if (clazz.isEnum) {
b.body.add(Field((b) {
// enumTypeFromStrings(String name, List<String> values, {String description}
var args = <Expression>[literalString(clazz.name)];
var values =
clazz.fields.where((f) => f.isEnumConstant).map((f) => f.name);
var named = <String, Expression>{};
_applyDescription(named, clazz, clazz.documentationComment);
args.add(literalConstList(values.map(literalString).toList()));
b
..name = ReCase(clazz.name).snakeCase + 'GraphQLType'
..docs.add('/// Auto-generated from [${clazz.name}].')
..type = TypeReference((b) => b
..symbol = 'GraphQLEnumType'
..types.add(refer('String')))
..modifier = FieldModifier.final$
..assignment = refer('enumTypeFromStrings').call(args, named).code;
}));
} else {
b.body.add(Field((b) {
var args = <Expression>[literalString(ctx.modelClassName)];
var named = <String, Expression>{
'isInterface': literalBool(isInterface(clazz))
};
// Add documentation
_applyDescription(named, clazz, clazz.documentationComment);
// Add interfaces
var interfaces = clazz.interfaces.where(_isGraphQLClass).map((c) {
var name = serializableTypeChecker.hasAnnotationOf(c.element) &&
c.name.startsWith('_')
? c.name.substring(1)
: c.name;
var rc = ReCase(name);
return refer('${rc.snakeCase}GraphQLType');
});
named['interfaces'] = literalList(interfaces);
// Add fields
var ctxFields = ctx.fields.toList();
// Also incorporate parent fields.
var search = clazz.type;
while (search != null &&
!TypeChecker.fromRuntime(Object).isExactlyType(search)) {
for (var field in search.element.fields) {
if (!ctxFields.any((f) => f.name == field.name)) {
ctxFields.add(field);
}
}
search = search.superclass;
}
var fields = <Expression>[];
for (var field in ctxFields) {
var named = <String, Expression>{};
var originalField = clazz.fields
.firstWhere((f) => f.name == field.name, orElse: () => null);
// Check if it is deprecated.
var depEl = originalField?.getter ?? originalField ?? field;
var depAnn =
TypeChecker.fromRuntime(Deprecated).firstAnnotationOf(depEl);
if (depAnn != null) {
var dep = ConstantReader(depAnn);
var reason = dep.peek('messages')?.stringValue ??
dep.peek('expires')?.stringValue ??
'Expires: ${deprecated.message}.';
named['deprecationReason'] = literalString(reason);
}
// Description finder...
_applyDescription(
named,
originalField?.getter ?? originalField ?? field,
originalField?.getter?.documentationComment ??
originalField?.documentationComment);
// Pick the type.
var doc = _graphQLDoc.firstAnnotationOf(depEl);
Expression type;
if (doc != null) {
var cr = ConstantReader(doc);
var typeName = cr.peek('typeName')?.symbolValue;
if (typeName != null)
type = refer(MirrorSystem.getName(typeName));
}
fields.add(refer('field').call([
literalString(ctx.resolveFieldName(field.name)),
type ??= _inferType(clazz.name, field.name, field.type)
], named));
}
named['fields'] = literalList(fields);
b
..name = ctx.modelClassNameRecase.snakeCase + 'GraphQLType'
..docs.add('/// Auto-generated from [${ctx.modelClassName}].')
..type = refer('GraphQLObjectType')
..modifier = FieldModifier.final$
..assignment = refer('objectType').call(args, named).code;
}));
}
});
}
}