From d97feaa899dc2216b839778021785ae3d7f4539b Mon Sep 17 00:00:00 2001 From: Tobe O Date: Wed, 9 Jan 2019 14:25:05 -0500 Subject: [PATCH] 2.4.1, etc. --- README.md | 22 +++--- angel_serialize/CHANGELOG.md | 3 + angel_serialize/lib/angel_serialize.dart | 14 +++- angel_serialize/pubspec.yaml | 2 +- angel_serialize_generator/CHANGELOG.md | 5 ++ .../lib/build_context.dart | 12 ++- angel_serialize_generator/lib/context.dart | 30 +++++++- angel_serialize_generator/lib/model.dart | 6 +- angel_serialize_generator/lib/serialize.dart | 74 +++++++++---------- angel_serialize_generator/lib/typescript.dart | 6 +- angel_serialize_generator/pubspec.yaml | 2 +- .../test/models/game_pad.dart | 2 +- .../test/models/game_pad_button.dart | 2 +- .../test/models/goat.dart | 2 +- .../test/models/has_map.dart | 7 +- .../test/models/with_enum.dart | 2 +- 16 files changed, 117 insertions(+), 74 deletions(-) diff --git a/README.md b/README.md index 7d882698..dd6c8f9c 100644 --- a/README.md +++ b/README.md @@ -255,13 +255,20 @@ Provide `serializer` and `deserializer` arguments to `@SerializableField()` as y They are typically used together. Note that the argument to `serializer` will always be `dynamic`. +In such a case, you might want to also provide a `serializesTo` argument. +This lets the generator, as well as the ORM, apply the correct (de)serialization rules +and validations. + ```dart DateTime _dateFromString(s) => s is String ? HttpDate.parse(s) : null; String _dateToString(v) => v == null ? null : HttpDate.format(v); -@Serializable(autoIdAndDateFields: false) +@serializable abstract class _HttpRequest { - @SerializableField(serializer: #_dateToString, deserializer: #_dateFromString) + @SerializableField( + serializer: #_dateToString, + deserializer: #_dateFromString, + serializesTo: String) DateTime date; } ``` @@ -294,15 +301,8 @@ then you will need to generate `book.g.dart` before, `author.g.dart`, # ID and Dates This package will automatically generate `id`, `createdAt`, and `updatedAt` fields for you, -in the style of an Angel `Model`. To disable this, set `autoIdAndDateFields` to `false` in the -builder constructor. - -You can also override `autoIdAndDateFields` per model: - -```dart -@Serializable(autoIdAndDateFields: false) -abstract class _Skinny extends Model {} -``` +in the style of an Angel `Model`. This will automatically be generated, **only** for classes +extending `Model`. # Binary Data diff --git a/angel_serialize/CHANGELOG.md b/angel_serialize/CHANGELOG.md index 3769e1f0..10d336bf 100644 --- a/angel_serialize/CHANGELOG.md +++ b/angel_serialize/CHANGELOG.md @@ -1,3 +1,6 @@ +# 2.2.1 +* Add `serializesTo`. + # 2.2.0 * Add `@SerializableField`. diff --git a/angel_serialize/lib/angel_serialize.dart b/angel_serialize/lib/angel_serialize.dart index a6371040..b8af0ecd 100644 --- a/angel_serialize/lib/angel_serialize.dart +++ b/angel_serialize/lib/angel_serialize.dart @@ -52,6 +52,14 @@ class SerializableField { /// Whether this field can be serialized, if [exclude] is `true`. Defaults to `false`. final bool canSerialize; + /// May be used with [serializer] and [deserializer]. + /// + /// Specifies the [Type] that this field serializes to. + /// + /// Ex. If you have a field that serializes to a JSON string, + /// specify `serializesTo: String`. + final Type serializesTo; + const SerializableField( {this.alias, this.defaultValue, @@ -61,7 +69,8 @@ class SerializableField { this.isNullable: true, this.exclude: false, this.canDeserialize: false, - this.canSerialize: false}); + this.canSerialize: false, + this.serializesTo}); } /// Marks a class as eligible for serialization. @@ -69,7 +78,7 @@ class Serializable { const Serializable( {this.serializers: const [Serializers.map, Serializers.json], this.autoSnakeCaseNames: true, - this.autoIdAndDateFields: true, + @deprecated this.autoIdAndDateFields: true, this.includeAnnotations: const []}); /// A list of enabled serialization modes. @@ -81,6 +90,7 @@ class Serializable { final bool autoSnakeCaseNames; /// Overrides the setting in `JsonModelGenerator`. + @deprecated final bool autoIdAndDateFields; /// A list of constant members to affix to the generated class. diff --git a/angel_serialize/pubspec.yaml b/angel_serialize/pubspec.yaml index ba75d3fa..4979b5f7 100644 --- a/angel_serialize/pubspec.yaml +++ b/angel_serialize/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_serialize -version: 2.2.0 +version: 2.2.1+2 description: Static annotations powering Angel model serialization. Combine with angel_serialize_generator for flexible modeling. author: Tobe O homepage: https://github.com/angel-dart/serialize diff --git a/angel_serialize_generator/CHANGELOG.md b/angel_serialize_generator/CHANGELOG.md index d18b5129..d66ac7da 100644 --- a/angel_serialize_generator/CHANGELOG.md +++ b/angel_serialize_generator/CHANGELOG.md @@ -1,3 +1,8 @@ +# 2.4.1 +* Support `serializesTo`. +* Don't emit `@required` if there is a default value. +* Deprecate `autoIdAndDateFields`. + # 2.4.0 * Introduce `@SerializableField`, and say goodbye to annotation hell. * Support custom (de)serializers. diff --git a/angel_serialize_generator/lib/build_context.dart b/angel_serialize_generator/lib/build_context.dart index cb26a0fe..8ff43ad1 100644 --- a/angel_serialize_generator/lib/build_context.dart +++ b/angel_serialize_generator/lib/build_context.dart @@ -3,6 +3,7 @@ import 'package:analyzer/dart/constant/value.dart'; import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/type.dart'; import 'package:analyzer/src/dart/element/element.dart'; +import 'package:angel_model/angel_model.dart'; import 'package:angel_serialize/angel_serialize.dart'; import 'package:build/build.dart'; import 'package:meta/meta.dart'; @@ -37,7 +38,6 @@ Future buildContext( BuildStep buildStep, Resolver resolver, bool autoSnakeCaseNames, - bool autoIdAndDateFields, {bool heedExclude: true}) async { var id = clazz.location.components.join('-'); if (_cache.containsKey(id)) { @@ -45,8 +45,6 @@ Future buildContext( } // Check for autoIdAndDateFields, autoSnakeCaseNames - autoIdAndDateFields = - annotation.peek('autoIdAndDateFields')?.boolValue ?? autoIdAndDateFields; autoSnakeCaseNames = annotation.peek('autoSnakeCaseNames')?.boolValue ?? autoSnakeCaseNames; @@ -55,7 +53,6 @@ Future buildContext( clazz, originalClassName: clazz.name, sourceFilename: p.basename(buildStep.inputId.path), - autoIdAndDateFields: autoIdAndDateFields, autoSnakeCaseNames: autoSnakeCaseNames, includeAnnotations: annotation.peek('includeAnnotations')?.listValue ?? [], @@ -79,7 +76,7 @@ Future buildContext( if (fieldAnn != null) { var cr = ConstantReader(fieldAnn); - var sField = SerializableField( + var sField = SerializableFieldMirror( alias: cr.peek('alias')?.stringValue, defaultValue: cr.peek('defaultValue')?.objectValue, serializer: cr.peek('serializer')?.symbolValue, @@ -89,12 +86,13 @@ Future buildContext( canDeserialize: cr.peek('canDeserialize')?.boolValue ?? false, canSerialize: cr.peek('canSerialize')?.boolValue ?? false, exclude: cr.peek('exclude')?.boolValue ?? false, + serializesTo: cr.peek('serializesTo')?.typeValue, ); ctx.fieldInfo[field.name] = sField; if (sField.defaultValue != null) { - ctx.defaults[field.name] = sField.defaultValue as DartObject; + ctx.defaults[field.name] = sField.defaultValue; } if (sField.alias != null) { @@ -175,7 +173,7 @@ Future buildContext( } } - if (autoIdAndDateFields != false) { + if (const TypeChecker.fromRuntime(Model).isAssignableFromType(clazz.type)) { if (!fieldNames.contains('id')) { var idField = new ShimFieldImpl('id', lib.context.typeProvider.stringType); diff --git a/angel_serialize_generator/lib/context.dart b/angel_serialize_generator/lib/context.dart index 86ee883e..ad9d763b 100644 --- a/angel_serialize_generator/lib/context.dart +++ b/angel_serialize_generator/lib/context.dart @@ -1,5 +1,6 @@ import 'package:analyzer/dart/constant/value.dart'; import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; import 'package:angel_serialize/angel_serialize.dart'; import 'package:code_builder/code_builder.dart'; import 'package:recase/recase.dart'; @@ -20,7 +21,7 @@ class BuildContext { final Map defaults = {}; /// A map of fields to their related information. - final Map fieldInfo = {}; + final Map fieldInfo = {}; /// A map of fields that have been marked as to be excluded from serialization. // ignore: deprecated_member_use @@ -78,4 +79,31 @@ class BuildContext { /// Get the aliased name (if one is defined) for a field. String resolveFieldName(String name) => aliases.containsKey(name) ? aliases[name] : name; + + /// Finds the type that the field [name] should serialize to. + DartType resolveSerializedFieldType(String name) { + return fieldInfo[name]?.serializesTo ?? + fields.firstWhere((f) => f.name == name).type; + } +} + +class SerializableFieldMirror { + final String alias; + final DartObject defaultValue; + final Symbol serializer, deserializer; + final String errorMessage; + final bool isNullable, canDeserialize, canSerialize, exclude; + final DartType serializesTo; + + SerializableFieldMirror( + {this.alias, + this.defaultValue, + this.serializer, + this.deserializer, + this.errorMessage, + this.isNullable, + this.canDeserialize, + this.canSerialize, + this.exclude, + this.serializesTo}); } diff --git a/angel_serialize_generator/lib/model.dart b/angel_serialize_generator/lib/model.dart index bb723059..f9fe06e5 100644 --- a/angel_serialize_generator/lib/model.dart +++ b/angel_serialize_generator/lib/model.dart @@ -1,9 +1,7 @@ part of angel_serialize_generator; class JsonModelGenerator extends GeneratorForAnnotation { - final bool autoIdAndDateFields; - - const JsonModelGenerator({this.autoIdAndDateFields: true}); + const JsonModelGenerator(); @override Future generateForAnnotatedElement( @@ -12,7 +10,7 @@ class JsonModelGenerator extends GeneratorForAnnotation { throw 'Only classes can be annotated with a @Serializable() annotation.'; var ctx = await buildContext(element as ClassElement, annotation, buildStep, - await buildStep.resolver, true, autoIdAndDateFields != false); + await buildStep.resolver, true); var lib = new Library((b) { generateClass(ctx, b, annotation); diff --git a/angel_serialize_generator/lib/serialize.dart b/angel_serialize_generator/lib/serialize.dart index 9c599760..cb56fb3b 100644 --- a/angel_serialize_generator/lib/serialize.dart +++ b/angel_serialize_generator/lib/serialize.dart @@ -12,7 +12,7 @@ class SerializerGenerator extends GeneratorForAnnotation { throw 'Only classes can be annotated with a @Serializable() annotation.'; var ctx = await buildContext(element as ClassElement, annotation, buildStep, - await buildStep.resolver, true, autoSnakeCaseNames != false); + await buildStep.resolver, autoSnakeCaseNames != false); var serializers = annotation.peek('serializers')?.listValue ?? []; @@ -80,6 +80,8 @@ class SerializerGenerator extends GeneratorForAnnotation { // Add named parameters for (var field in ctx.fields) { + var type = ctx.resolveSerializedFieldType(field.name); + // Skip excluded fields if (ctx.excluded[field.name]?.canSerialize == false) continue; @@ -102,19 +104,17 @@ class SerializerGenerator extends GeneratorForAnnotation { } // Serialize dates - else if (dateTimeTypeChecker.isAssignableFromType(field.type)) + else if (dateTimeTypeChecker.isAssignableFromType(type)) serializedRepresentation = 'model.${field.name}?.toIso8601String()'; // Serialize model classes via `XSerializer.toMap` - else if (isModelClass(field.type)) { - var rc = new ReCase(field.type.name); + else if (isModelClass(type)) { + var rc = new ReCase(type.name); serializedRepresentation = '${serializerToMap(rc, 'model.${field.name}')}'; - } else if (field.type is InterfaceType) { - var t = field.type as InterfaceType; - - if (isListOfModelType(t)) { - var name = t.typeArguments[0].name; + } else if (type is InterfaceType) { + if (isListOfModelType(type)) { + var name = type.typeArguments[0].name; if (name.startsWith('_')) name = name.substring(1); var rc = new ReCase(name); var m = serializerToMap(rc, 'm'); @@ -122,21 +122,21 @@ class SerializerGenerator extends GeneratorForAnnotation { model.${field.name} ?.map((m) => $m) ?.toList()'''; - } else if (isMapToModelType(t)) { - var rc = new ReCase(t.typeArguments[1].name); + } else if (isMapToModelType(type)) { + var rc = new ReCase(type.typeArguments[1].name); serializedRepresentation = '''model.${field.name}.keys?.fold({}, (map, key) { return map..[key] = ${serializerToMap(rc, 'model.${field.name}[key]')}; })'''; - } else if (t.element.isEnum) { + } else if (type.element.isEnum) { serializedRepresentation = ''' model.${field.name} == null ? null - : ${t.name}.values.indexOf(model.${field.name}) + : ${type.name}.values.indexOf(model.${field.name}) '''; } else if (const TypeChecker.fromRuntime(Uint8List) - .isAssignableFromType(t)) { + .isAssignableFromType(type)) { serializedRepresentation = ''' model.${field.name} == null ? null @@ -199,6 +199,8 @@ class SerializerGenerator extends GeneratorForAnnotation { } for (var field in ctx.fields) { + var type = ctx.resolveSerializedFieldType(field.name); + if (ctx.excluded[field.name]?.canDeserialize == false) continue; var alias = ctx.resolveFieldName(field.name); @@ -206,7 +208,7 @@ class SerializerGenerator extends GeneratorForAnnotation { if (i++ > 0) buf.write(', '); String deserializedRepresentation = - "map['$alias'] as ${typeToString(field.type)}"; + "map['$alias'] as ${typeToString(type)}"; var defaultValue = 'null'; var existingDefault = ctx.defaults[field.name]; @@ -221,29 +223,27 @@ class SerializerGenerator extends GeneratorForAnnotation { var name = MirrorSystem.getName(ctx.fieldInfo[field.name].deserializer); deserializedRepresentation = "$name(map['$alias'])"; - } else if (dateTimeTypeChecker.isAssignableFromType(field.type)) + } else if (dateTimeTypeChecker.isAssignableFromType(type)) deserializedRepresentation = "map['$alias'] != null ? " "(map['$alias'] is DateTime ? (map['$alias'] as DateTime) : DateTime.parse(map['$alias'].toString()))" " : $defaultValue"; // Serialize model classes via `XSerializer.toMap` - else if (isModelClass(field.type)) { - var rc = new ReCase(field.type.name); + else if (isModelClass(type)) { + var rc = new ReCase(type.name); deserializedRepresentation = "map['$alias'] != null" " ? ${rc.pascalCase}Serializer.fromMap(map['$alias'] as Map)" " : $defaultValue"; - } else if (field.type is InterfaceType) { - var t = field.type as InterfaceType; - - if (isListOfModelType(t)) { - var rc = new ReCase(t.typeArguments[0].name); + } else if (type is InterfaceType) { + if (isListOfModelType(type)) { + var rc = new ReCase(type.typeArguments[0].name); deserializedRepresentation = "map['$alias'] is Iterable" " ? new List.unmodifiable(((map['$alias'] as Iterable)" ".where((x) => x is Map) as Iterable)" ".map(${rc.pascalCase}Serializer.fromMap))" " : $defaultValue"; - } else if (isMapToModelType(t)) { - var rc = new ReCase(t.typeArguments[1].name); + } else if (isMapToModelType(type)) { + var rc = new ReCase(type.typeArguments[1].name); deserializedRepresentation = ''' map['$alias'] is Map ? new Map.unmodifiable((map['$alias'] as Map).keys.fold({}, (out, key) { @@ -252,21 +252,21 @@ class SerializerGenerator extends GeneratorForAnnotation { })) : $defaultValue '''; - } else if (t.element.isEnum) { + } else if (type.element.isEnum) { deserializedRepresentation = ''' - map['$alias'] is ${t.name} - ? (map['$alias'] as ${t.name}) + map['$alias'] is ${type.name} + ? (map['$alias'] as ${type.name}) : ( map['$alias'] is int - ? ${t.name}.values[map['$alias'] as int] + ? ${type.name}.values[map['$alias'] as int] : $defaultValue ) '''; } else if (const TypeChecker.fromRuntime(List) - .isAssignableFromType(t) && - t.typeArguments.length == 1) { - var arg = convertTypeReference(t.typeArguments[0]) + .isAssignableFromType(type) && + type.typeArguments.length == 1) { + var arg = convertTypeReference(type.typeArguments[0]) .accept(new DartEmitter()); deserializedRepresentation = ''' map['$alias'] is Iterable @@ -274,11 +274,11 @@ class SerializerGenerator extends GeneratorForAnnotation { : $defaultValue '''; } else if (const TypeChecker.fromRuntime(Map) - .isAssignableFromType(t) && - t.typeArguments.length == 2) { - var key = convertTypeReference(t.typeArguments[0]) + .isAssignableFromType(type) && + type.typeArguments.length == 2) { + var key = convertTypeReference(type.typeArguments[0]) .accept(new DartEmitter()); - var value = convertTypeReference(t.typeArguments[1]) + var value = convertTypeReference(type.typeArguments[1]) .accept(new DartEmitter()); deserializedRepresentation = ''' map['$alias'] is Map @@ -286,7 +286,7 @@ class SerializerGenerator extends GeneratorForAnnotation { : $defaultValue '''; } else if (const TypeChecker.fromRuntime(Uint8List) - .isAssignableFromType(t)) { + .isAssignableFromType(type)) { deserializedRepresentation = ''' map['$alias'] is Uint8List ? (map['$alias'] as Uint8List) diff --git a/angel_serialize_generator/lib/typescript.dart b/angel_serialize_generator/lib/typescript.dart index af59223a..8c240cb3 100644 --- a/angel_serialize_generator/lib/typescript.dart +++ b/angel_serialize_generator/lib/typescript.dart @@ -109,7 +109,6 @@ class TypeScriptDefinitionBuilder implements Builder { buildStep, buildStep.resolver, autoSnakeCaseNames, - true, ); typeScriptType = ctx.modelClassNameRecase.pascalCase; } @@ -152,7 +151,6 @@ class TypeScriptDefinitionBuilder implements Builder { element.annotation, buildStep, await buildStep.resolver, - true, autoSnakeCaseNames != false)); } @@ -184,8 +182,8 @@ class TypeScriptDefinitionBuilder implements Builder { if (ctx.excluded[field.name]?.canSerialize == false) continue; var alias = ctx.resolveFieldName(field.name); - var typeScriptType = await compileToTypeScriptType( - ctx, field.name, field.type, refs, ext, buildStep); + var typeScriptType = await compileToTypeScriptType(ctx, field.name, + ctx.resolveSerializedFieldType(field.name), refs, ext, buildStep); // foo: string; if (!ctx.requiredFields.containsKey(field.name)) alias += '?'; diff --git a/angel_serialize_generator/pubspec.yaml b/angel_serialize_generator/pubspec.yaml index f802c5e2..f725cd45 100644 --- a/angel_serialize_generator/pubspec.yaml +++ b/angel_serialize_generator/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_serialize_generator -version: 2.4.0 +version: 2.4.1 description: Model serialization generators, designed for use with Angel. Combine with angel_serialize for flexible modeling. author: Tobe O homepage: https://github.com/angel-dart/serialize diff --git a/angel_serialize_generator/test/models/game_pad.dart b/angel_serialize_generator/test/models/game_pad.dart index dc2c6a8c..cf2290ea 100644 --- a/angel_serialize_generator/test/models/game_pad.dart +++ b/angel_serialize_generator/test/models/game_pad.dart @@ -3,7 +3,7 @@ import 'package:collection/collection.dart'; import 'game_pad_button.dart'; part 'game_pad.g.dart'; -@Serializable(autoIdAndDateFields: false) +@serializable class _Gamepad { List buttons; diff --git a/angel_serialize_generator/test/models/game_pad_button.dart b/angel_serialize_generator/test/models/game_pad_button.dart index e2257e9e..cf31f0f3 100644 --- a/angel_serialize_generator/test/models/game_pad_button.dart +++ b/angel_serialize_generator/test/models/game_pad_button.dart @@ -1,7 +1,7 @@ import 'package:angel_serialize/angel_serialize.dart'; part 'game_pad_button.g.dart'; -@Serializable(autoIdAndDateFields: false) +@serializable abstract class _GamepadButton { String get name; int get radius; diff --git a/angel_serialize_generator/test/models/goat.dart b/angel_serialize_generator/test/models/goat.dart index 73d4395d..e1bb8bd7 100644 --- a/angel_serialize_generator/test/models/goat.dart +++ b/angel_serialize_generator/test/models/goat.dart @@ -2,7 +2,7 @@ import 'package:angel_serialize/angel_serialize.dart'; import 'package:collection/collection.dart'; part 'goat.g.dart'; -@Serializable(autoIdAndDateFields: false) +@serializable abstract class _Goat { @SerializableField(defaultValue: 34) int get integer; diff --git a/angel_serialize_generator/test/models/has_map.dart b/angel_serialize_generator/test/models/has_map.dart index 3e0e2f82..db9d5374 100644 --- a/angel_serialize_generator/test/models/has_map.dart +++ b/angel_serialize_generator/test/models/has_map.dart @@ -8,9 +8,12 @@ Map _fromString(v) => json.decode(v.toString()) as Map; String _toString(Map v) => json.encode(v); -@Serializable(autoIdAndDateFields: false) +@serializable abstract class _HasMap { @SerializableField( - serializer: #_toString, deserializer: #_fromString, isNullable: false) + serializer: #_toString, + deserializer: #_fromString, + isNullable: false, + serializesTo: String) Map get value; } diff --git a/angel_serialize_generator/test/models/with_enum.dart b/angel_serialize_generator/test/models/with_enum.dart index 39975a73..c08f1224 100644 --- a/angel_serialize_generator/test/models/with_enum.dart +++ b/angel_serialize_generator/test/models/with_enum.dart @@ -4,7 +4,7 @@ import 'package:angel_serialize/angel_serialize.dart'; import 'package:collection/collection.dart'; part 'with_enum.g.dart'; -@Serializable(autoIdAndDateFields: false) +@serializable abstract class _WithEnum { WithEnumType get type;