include annotations

This commit is contained in:
Tobe O 2019-01-06 19:56:05 -05:00
parent a30b99a821
commit 4dc0153cbf
10 changed files with 205 additions and 62 deletions

View file

@ -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;

View file

@ -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();

View file

@ -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();

View file

@ -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> 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 ?? <DartObject>[],
);
var lib = await resolver.libraryFor(buildStep.inputId);
List<String> fieldNames = [];
@ -55,49 +66,100 @@ Future<BuildContext> 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);

View file

@ -19,7 +19,11 @@ class BuildContext {
/// A map of field names to their default values.
final Map<String, DartObject> defaults = {};
/// A map of fields to their related information.
final Map<String, SerializableField> fieldInfo = {};
/// A map of fields that have been marked as to be excluded from serialization.
// ignore: deprecated_member_use
final Map<String, Exclude> 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<DartObject> 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 <DartObject>[]});
/// The name of the generated class.
String get modelClassName => originalClassName.startsWith('_')

View file

@ -30,6 +30,10 @@ class JsonModelGenerator extends GeneratorForAnnotation<Serializable> {
..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 {

View file

@ -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<int> get history;

View file

@ -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<double> notModels;
@Alias('camelCase')
@SerializableField(alias: 'camelCase')
String camelCaseString;
}

View file

@ -7,6 +7,8 @@ part of angel_serialize.test.models.book;
// **************************************************************************
@generatedSerializable
@pragma('hello')
@SerializableField(alias: 'omg')
class Book extends _Book {
Book(
{this.id,

View file

@ -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<int> get list;
}