diff --git a/README.md b/README.md index c4afaa03..29764e90 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ the time you spend writing boilerplate serialization code for your models. - [Field Aliases](#aliases) - [Excluding Keys](#excluding-keys) - [Required Fields](#required-fields) + - [Adding Annotations to Generated Classes](#adding-annotations-to-generated-classes) - [Serialization](#serializaition) - [Nesting](#nesting) - [ID and Date Fields](#id-and-dates) @@ -67,7 +68,7 @@ part 'book.g.dart'; abstract class _Book extends Model { String get author; - @DefaultValue('[Untitled]') + @SerializableField(defaultValue: '[Untitled]') String get title; String get description; @@ -155,7 +156,7 @@ Whereas Dart fields conventionally are camelCased, most database columns tend to be snake_cased. This is not a problem, because we can define an alias for a field. -By default `angel_serialize` will transform keys into snake case. Use `Alias` to +By default `angel_serialize` will transform keys into snake case. Use `alias` to provide a custom name, or pass `autoSnakeCaseNames`: `false` to the builder; ```dart @@ -169,7 +170,7 @@ abstract class _Spy extends Model { /// Hooray! String agencyId; - @Alias('foo') + @SerializableField(alias: 'foo') String someOtherField; } ``` @@ -192,7 +193,7 @@ To accomplish this, simply annotate them with `@exclude`: @serializable abstract class _Whisper extends Model { /// Will never be serialized to JSON - @exclude + @SerializableField(exclude: true) String secret; } ``` @@ -209,23 +210,22 @@ abstract class _Whisper extends Model { /// Will never be serialized to JSON /// /// ... But it can be deserialized - @Exclude(canDeserialize: true) + @SerializableField(exclude: true, canDeserialize: true) String secret; } ``` ## Required Fields -It is easy to mark a field as required; just use the -`@required` annotation from `package:meta`: +It is easy to mark a field as required: ```dart @serializable abstract class _Foo extends Model { - @required + @SerializableField(isNullable: false) int myRequiredInt; - @Required('Custom message') + @SerializableField(isNullable: false, errorMessage: 'Custom message') int myOtherRequiredInt; } ``` @@ -234,6 +234,19 @@ The given field will be marked as `@required` in the generated constructor, and serializers will check for its presence, throwing a `FormatException` if it is missing. +## Adding Annotations to Generated Classes +There are times when you need the generated class to have annotations affixed to it: + +```dart +@Serializable( + includeAnnotations: [ + Deprecated('blah blah blah'), + pragma('something...'), + ] +) +abstract class _Foo extends Model {} +``` + # Nesting `angel_serialize` also supports a few types of nesting of `@serializable` classes: @@ -327,7 +340,7 @@ The following: ```dart @serializable abstract class _Bookmark extends _BookmarkBase { - @exclude + @SerializableField(exclude: true) final Book book; int get page; diff --git a/angel_serialize/lib/angel_serialize.dart b/angel_serialize/lib/angel_serialize.dart index 33ff3fc0..a6371040 100644 --- a/angel_serialize/lib/angel_serialize.dart +++ b/angel_serialize/lib/angel_serialize.dart @@ -1,6 +1,7 @@ export 'package:quiver_hashcode/hashcode.dart' show hashObjects; /// Excludes a field from being excluded. +@deprecated class Exclude { final bool canSerialize; @@ -9,6 +10,8 @@ class Exclude { const Exclude({this.canDeserialize: false, this.canSerialize: false}); } +@deprecated +// ignore: deprecated_member_use const Exclude exclude = const Exclude(); @deprecated @@ -34,15 +37,31 @@ class SerializableField { /// A custom serializer for this field. final Symbol deserializer; - /// A list of constant members to affix to the generated class. - final List includeAnnotations; + /// An error message to be printed when the provided value is invalid. + final String errorMessage; - SerializableField( + /// Whether this field can be set to `null`. + final bool isNullable; + + /// Whether to exclude this field from serialization. Defaults to `false`. + final bool exclude; + + /// Whether this field can be serialized, if [exclude] is `true`. Defaults to `false`. + final bool canDeserialize; + + /// Whether this field can be serialized, if [exclude] is `true`. Defaults to `false`. + final bool canSerialize; + + const SerializableField( {this.alias, this.defaultValue, this.serializer, this.deserializer, - this.includeAnnotations: const []}); + this.errorMessage, + this.isNullable: true, + this.exclude: false, + this.canDeserialize: false, + this.canSerialize: false}); } /// Marks a class as eligible for serialization. @@ -50,7 +69,8 @@ class Serializable { const Serializable( {this.serializers: const [Serializers.map, Serializers.json], this.autoSnakeCaseNames: true, - this.autoIdAndDateFields: true}); + this.autoIdAndDateFields: true, + this.includeAnnotations: const []}); /// A list of enabled serialization modes. /// @@ -62,6 +82,9 @@ class Serializable { /// Overrides the setting in `JsonModelGenerator`. final bool autoIdAndDateFields; + + /// A list of constant members to affix to the generated class. + final List includeAnnotations; } const Serializable serializable = const Serializable(); diff --git a/angel_serialize_generator/lib/angel_serialize_generator.dart b/angel_serialize_generator/lib/angel_serialize_generator.dart index 9e366b1f..1f99838c 100644 --- a/angel_serialize_generator/lib/angel_serialize_generator.dart +++ b/angel_serialize_generator/lib/angel_serialize_generator.dart @@ -48,6 +48,30 @@ TypeReference convertTypeReference(DartType t) { }); } +Expression convertObject(DartObject o) { + if (o.isNull) return literalNull; + if (o.toBoolValue() != null) return literalBool(o.toBoolValue()); + if (o.toIntValue() != null) return literalNum(o.toIntValue()); + if (o.toDoubleValue() != null) return literalNum(o.toDoubleValue()); + if (o.toSymbolValue() != null) + return CodeExpression(Code('#' + o.toSymbolValue())); + if (o.toStringValue() != null) return literalString(o.toStringValue()); + if (o.toTypeValue() != null) return convertTypeReference(o.toTypeValue()); + if (o.toListValue() != null) + return literalList(o.toListValue().map(convertObject)); + if (o.toMapValue() != null) { + return literalMap(o + .toMapValue() + .map((k, v) => MapEntry(convertObject(k), convertObject(v)))); + } + + var rev = ConstantReader(o).revive(); + Expression target = convertTypeReference(o.type); + target = rev.accessor.isEmpty ? target : target.property(rev.accessor); + return target.call(rev.positionalArguments.map(convertObject), + rev.namedArguments.map((k, v) => MapEntry(k, convertObject(v)))); +} + String dartObjectToString(DartObject v) { if (v.isNull) return 'null'; if (v.toBoolValue() != null) return v.toBoolValue().toString(); diff --git a/angel_serialize_generator/lib/build_context.dart b/angel_serialize_generator/lib/build_context.dart index 6b5c92e7..45f7c2bb 100644 --- a/angel_serialize_generator/lib/build_context.dart +++ b/angel_serialize_generator/lib/build_context.dart @@ -1,4 +1,5 @@ import 'dart:async'; +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'; @@ -10,12 +11,17 @@ import 'package:recase/recase.dart'; import 'package:source_gen/source_gen.dart'; import 'context.dart'; +// ignore: deprecated_member_use const TypeChecker aliasTypeChecker = const TypeChecker.fromRuntime(Alias); const TypeChecker dateTimeTypeChecker = const TypeChecker.fromRuntime(DateTime); +// ignore: deprecated_member_use const TypeChecker excludeTypeChecker = const TypeChecker.fromRuntime(Exclude); +const TypeChecker serializableFieldTypeChecker = + const TypeChecker.fromRuntime(SerializableField); + const TypeChecker serializableTypeChecker = const TypeChecker.fromRuntime(Serializable); @@ -37,11 +43,16 @@ Future buildContext( autoSnakeCaseNames = annotation.peek('autoSnakeCaseNames')?.boolValue ?? autoSnakeCaseNames; - var ctx = new BuildContext(annotation, clazz, - originalClassName: clazz.name, - sourceFilename: p.basename(buildStep.inputId.path), - autoIdAndDateFields: autoIdAndDateFields, - autoSnakeCaseNames: autoSnakeCaseNames); + var ctx = new BuildContext( + annotation, + clazz, + originalClassName: clazz.name, + sourceFilename: p.basename(buildStep.inputId.path), + autoIdAndDateFields: autoIdAndDateFields, + autoSnakeCaseNames: autoSnakeCaseNames, + includeAnnotations: + annotation.peek('includeAnnotations')?.listValue ?? [], + ); var lib = await resolver.libraryFor(buildStep.inputId); List fieldNames = []; @@ -55,49 +66,100 @@ Future buildContext( (field.setter != null || field.getter.isAbstract)) { var el = field.setter == null ? field.getter : field; fieldNames.add(field.name); - // Skip if annotated with @exclude - var excludeAnnotation = excludeTypeChecker.firstAnnotationOf(el); - if (excludeAnnotation != null) { - var cr = new ConstantReader(excludeAnnotation); + // Check for @SerializableField + var fieldAnn = serializableFieldTypeChecker.firstAnnotationOf(el); - ctx.excluded[field.name] = new Exclude( - canSerialize: cr.read('canSerialize').boolValue, - canDeserialize: cr.read('canDeserialize').boolValue, + if (fieldAnn != null) { + var cr = ConstantReader(fieldAnn); + var sField = SerializableField( + alias: cr.peek('alias')?.stringValue, + defaultValue: cr.peek('defaultValue')?.objectValue, + serializer: cr.peek('serializer')?.symbolValue, + deserializer: cr.peek('deserializer')?.symbolValue, + errorMessage: cr.peek('errorMessage')?.stringValue, + isNullable: cr.peek('isNullable')?.boolValue ?? true, + canDeserialize: cr.peek('canDeserialize')?.boolValue ?? false, + canSerialize: cr.peek('canSerialize')?.boolValue ?? false, + exclude: cr.peek('exclude')?.boolValue ?? false, ); - } - // Check for @DefaultValue() - var defAnn = - const TypeChecker.fromRuntime(DefaultValue).firstAnnotationOf(el); - if (defAnn != null) { - var rev = new ConstantReader(defAnn).revive().positionalArguments[0]; - ctx.defaults[field.name] = rev; - } + ctx.fieldInfo[field.name] = sField; - // Check for alias - Alias alias; - var aliasAnn = aliasTypeChecker.firstAnnotationOf(el); + if (sField.defaultValue != null) { + ctx.defaults[field.name] = sField.defaultValue as DartObject; + } - if (aliasAnn != null) { - alias = new Alias(aliasAnn.getField('name').toStringValue()); - } + if (sField.alias != null) { + ctx.aliases[field.name] = sField.alias; + } else if (autoSnakeCaseNames != false) { + ctx.aliases[field.name] = new ReCase(field.name).snakeCase; + } - if (alias?.name?.isNotEmpty == true) { - ctx.aliases[field.name] = alias.name; - } else if (autoSnakeCaseNames != false) { - ctx.aliases[field.name] = new ReCase(field.name).snakeCase; - } + if (sField.isNullable == false) { + var reason = sField.errorMessage ?? + "Missing required field '${ctx.resolveFieldName(field.name)}' on ${ctx.modelClassName}."; + ctx.requiredFields[field.name] = reason; + } - // Check for @required - var required = - const TypeChecker.fromRuntime(Required).firstAnnotationOf(el); + if (sField.exclude) { + // ignore: deprecated_member_use + ctx.excluded[field.name] = new Exclude( + canSerialize: sField.canSerialize, + canDeserialize: sField.canDeserialize, + ); + } - if (required != null) { - var cr = new ConstantReader(required); - var reason = cr.peek('reason')?.stringValue ?? - "Missing required field '${ctx.resolveFieldName(field.name)}' on ${ctx.modelClassName}."; - ctx.requiredFields[field.name] = reason; + // Apply + } else { + // Skip if annotated with @exclude + var excludeAnnotation = excludeTypeChecker.firstAnnotationOf(el); + + if (excludeAnnotation != null) { + var cr = new ConstantReader(excludeAnnotation); + + // ignore: deprecated_member_use + ctx.excluded[field.name] = new Exclude( + canSerialize: cr.read('canSerialize').boolValue, + canDeserialize: cr.read('canDeserialize').boolValue, + ); + } + + // Check for @DefaultValue() + var defAnn = + // ignore: deprecated_member_use + const TypeChecker.fromRuntime(DefaultValue).firstAnnotationOf(el); + if (defAnn != null) { + var rev = new ConstantReader(defAnn).revive().positionalArguments[0]; + ctx.defaults[field.name] = rev; + } + + // Check for alias + // ignore: deprecated_member_use + Alias alias; + var aliasAnn = aliasTypeChecker.firstAnnotationOf(el); + + if (aliasAnn != null) { + // ignore: deprecated_member_use + alias = new Alias(aliasAnn.getField('name').toStringValue()); + } + + if (alias?.name?.isNotEmpty == true) { + ctx.aliases[field.name] = alias.name; + } else if (autoSnakeCaseNames != false) { + ctx.aliases[field.name] = new ReCase(field.name).snakeCase; + } + + // Check for @required + var required = + const TypeChecker.fromRuntime(Required).firstAnnotationOf(el); + + if (required != null) { + var cr = new ConstantReader(required); + var reason = cr.peek('reason')?.stringValue ?? + "Missing required field '${ctx.resolveFieldName(field.name)}' on ${ctx.modelClassName}."; + ctx.requiredFields[field.name] = reason; + } } ctx.fields.add(field); diff --git a/angel_serialize_generator/lib/context.dart b/angel_serialize_generator/lib/context.dart index 9c5fe0ef..86ee883e 100644 --- a/angel_serialize_generator/lib/context.dart +++ b/angel_serialize_generator/lib/context.dart @@ -19,7 +19,11 @@ class BuildContext { /// A map of field names to their default values. final Map defaults = {}; + /// A map of fields to their related information. + final Map fieldInfo = {}; + /// A map of fields that have been marked as to be excluded from serialization. + // ignore: deprecated_member_use final Map excluded = {}; /// A map of "synthetic" fields, i.e. `id` and `created_at` injected automatically. @@ -38,6 +42,9 @@ class BuildContext { final ClassElement clazz; + /// Any annotations to include in the generated class. + final List includeAnnotations; + /// The name of the field that identifies data of this model type. String primaryKeyName = 'id'; @@ -45,7 +52,8 @@ class BuildContext { {this.originalClassName, this.sourceFilename, this.autoSnakeCaseNames, - this.autoIdAndDateFields}); + this.autoIdAndDateFields, + this.includeAnnotations: const []}); /// The name of the generated class. String get modelClassName => originalClassName.startsWith('_') diff --git a/angel_serialize_generator/lib/model.dart b/angel_serialize_generator/lib/model.dart index 0b3d82ab..bb723059 100644 --- a/angel_serialize_generator/lib/model.dart +++ b/angel_serialize_generator/lib/model.dart @@ -30,6 +30,10 @@ class JsonModelGenerator extends GeneratorForAnnotation { ..name = ctx.modelClassNameRecase.pascalCase ..annotations.add(refer('generatedSerializable')); + for (var ann in ctx.includeAnnotations) { + clazz.annotations.add(convertObject(ann)); + } + if (shouldBeConstant(ctx)) { clazz.implements.add(new Reference(ctx.originalClassName)); } else { diff --git a/angel_serialize_generator/test/models/author.dart b/angel_serialize_generator/test/models/author.dart index 414f2548..7db71732 100644 --- a/angel_serialize_generator/test/models/author.dart +++ b/angel_serialize_generator/test/models/author.dart @@ -21,10 +21,10 @@ abstract class _Author extends Model { Book get newestBook; - @exclude + @SerializableField(exclude: true) String get secret; - @Exclude(canDeserialize: true) + @SerializableField(exclude: true, canDeserialize: true) String get obscured; } @@ -35,7 +35,8 @@ abstract class _Library extends Model { @Serializable(serializers: Serializers.all) abstract class _Bookmark extends Model { - @exclude + + @SerializableField(exclude: true) final Book book; List get history; diff --git a/angel_serialize_generator/test/models/book.dart b/angel_serialize_generator/test/models/book.dart index bb02e464..a00f6d5e 100644 --- a/angel_serialize_generator/test/models/book.dart +++ b/angel_serialize_generator/test/models/book.dart @@ -5,12 +5,18 @@ import 'package:angel_serialize/angel_serialize.dart'; import 'package:collection/collection.dart'; part 'book.g.dart'; -@Serializable(serializers: Serializers.all) +@Serializable( + serializers: Serializers.all, + includeAnnotations: [ + pragma('hello'), + SerializableField(alias: 'omg'), + ], +) abstract class _Book extends Model { String author, title, description; int pageCount; List notModels; - @Alias('camelCase') + @SerializableField(alias: 'camelCase') String camelCaseString; } diff --git a/angel_serialize_generator/test/models/book.g.dart b/angel_serialize_generator/test/models/book.g.dart index 52fb099c..6383c140 100644 --- a/angel_serialize_generator/test/models/book.g.dart +++ b/angel_serialize_generator/test/models/book.g.dart @@ -7,6 +7,8 @@ part of angel_serialize.test.models.book; // ************************************************************************** @generatedSerializable +@pragma('hello') +@SerializableField(alias: 'omg') class Book extends _Book { Book( {this.id, diff --git a/angel_serialize_generator/test/models/goat.dart b/angel_serialize_generator/test/models/goat.dart index 2b1a0584..73d4395d 100644 --- a/angel_serialize_generator/test/models/goat.dart +++ b/angel_serialize_generator/test/models/goat.dart @@ -4,9 +4,9 @@ part 'goat.g.dart'; @Serializable(autoIdAndDateFields: false) abstract class _Goat { - @DefaultValue(34) + @SerializableField(defaultValue: 34) int get integer; - @DefaultValue([34, 35]) + @SerializableField(defaultValue: [34, 35]) List get list; }