include annotations
This commit is contained in:
parent
a30b99a821
commit
4dc0153cbf
10 changed files with 205 additions and 62 deletions
33
README.md
33
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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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('_')
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -7,6 +7,8 @@ part of angel_serialize.test.models.book;
|
|||
// **************************************************************************
|
||||
|
||||
@generatedSerializable
|
||||
@pragma('hello')
|
||||
@SerializableField(alias: 'omg')
|
||||
class Book extends _Book {
|
||||
Book(
|
||||
{this.id,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue