Add 'packages/serialize/' from commit 'be6a3669cca34cd83d189a1169edf6f381101cd8'

git-subtree-dir: packages/serialize
git-subtree-mainline: f7c6ebf200
git-subtree-split: be6a3669cc
This commit is contained in:
Tobe O 2020-02-15 18:22:00 -05:00
commit 42a86be549
51 changed files with 4529 additions and 0 deletions

23
packages/serialize/.gitignore vendored Normal file
View file

@ -0,0 +1,23 @@
# Created by .ignore support plugin (hsz.mobi)
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/dictionaries
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.xml
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/gradle.xml
.idea/**/libraries
.idea/**/mongoSettings.xml
*.iws
/out/
.idea_modules/
atlassian-ide-plugin.xml
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
.dart_tool

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/serialize.iml" filepath="$PROJECT_DIR$/.idea/serialize.iml" />
</modules>
</component>
</project>

View file

@ -0,0 +1,8 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="tests in angel_serialize_generator" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true" nameIsGenerated="true">
<option name="filePath" value="$PROJECT_DIR$/angel_serialize_generator" />
<option name="scope" value="FOLDER" />
<option name="testRunnerOptions" value="-j 4" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="tests in enum_test.dart" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true" nameIsGenerated="true">
<option name="filePath" value="$PROJECT_DIR$/angel_serialize_generator/test/enum_test.dart" />
<option name="testRunnerOptions" value="-j4" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$/angel_serialize">
<excludeFolder url="file://$MODULE_DIR$/angel_serialize/.dart_tool" />
<excludeFolder url="file://$MODULE_DIR$/angel_serialize/.pub" />
<excludeFolder url="file://$MODULE_DIR$/angel_serialize/build" />
</content>
<content url="file://$MODULE_DIR$/angel_serialize_generator">
<excludeFolder url="file://$MODULE_DIR$/angel_serialize_generator/.dart_tool" />
<excludeFolder url="file://$MODULE_DIR$/angel_serialize_generator/.pub" />
<excludeFolder url="file://$MODULE_DIR$/angel_serialize_generator/build" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Dart SDK" level="project" />
<orderEntry type="library" name="Dart Packages" level="project" />
</component>
</module>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View file

@ -0,0 +1,4 @@
language: dart
script: bash tool/.travis.sh
dart:
- stable

View file

@ -0,0 +1,427 @@
# serialize
[![Pub](https://img.shields.io/pub/v/angel_serialize.svg)](https://pub.dartlang.org/packages/angel_serialize)
[![build status](https://travis-ci.org/angel-dart/serialize.svg)](https://travis-ci.org/angel-dart/serialize)
Source-generated serialization for Dart objects. This package uses `package:source_gen` to eliminate
the time you spend writing boilerplate serialization code for your models.
`package:angel_serialize` also powers `package:angel_orm`.
- [Usage](#usage)
- [Models](#models)
- [Subclasses](#subclasses)
- [Field Aliases](#aliases)
- [Excluding Keys](#excluding-keys)
- [Required Fields](#required-fields)
- [Adding Annotations to Generated Classes](#adding-annotations-to-generated-classes)
- [Custom Serializers](#custom-serializers)
- [Serialization](#serializaition)
- [Nesting](#nesting)
- [ID and Date Fields](#id-and-dates)
- [Binary Data](#binary-data)
- [TypeScript Definition Generator](#typescript-definitions)
- [Constructor Parameters](#constructor-parameters)
# Usage
In your `pubspec.yaml`, you need to install the following dependencies:
```yaml
dependencies:
angel_model: ^1.0.0
angel_serialize: ^2.0.0
dev_dependencies:
angel_serialize_generator: ^2.0.0
build_runner: ^1.0.0
```
With the recent updates to `package:build_runner`, you can build models automatically,
anywhere in your project structure,
by running `pub run build_runner build`.
To tweak this:
https://pub.dartlang.org/packages/build_config
If you want to watch for file changes and re-build when necessary, replace the `build` call
with a call to `watch`. They take the same parameters.
# Models
There are a few changes opposed to normal Model classes. You need to add a `@serializable` annotation to your model
class to have it serialized, and a serializable model class's name should also start
with a leading underscore.
In addition, you may consider using an `abstract` class to ensure immutability
of models.
Rather you writing the public class, `angel_serialize` does it for you. This means that the main class can have
its constructors automatically generated, in addition into serialization functions.
For example, say we have a `Book` model. Create a class named `_Book`:
```dart
library angel_serialize.test.models.book;
import 'package:angel_model/angel_model.dart';
import 'package:angel_serialize/angel_serialize.dart';
import 'package:collection/collection.dart';
part 'book.g.dart';
@serializable
abstract class _Book extends Model {
String get author;
@SerializableField(defaultValue: '[Untitled]')
String get title;
String get description;
int get pageCount;
BookType get type;
}
/// It even supports enums!
enum BookType {
fiction,
nonFiction
}
```
The following file will be generated:
- `book.g.dart`
Producing these classes:
- `Book`: Extends or implements `_Book`; may be `const`-enabled.
- `BookSerializer`: static functionality for serializing `Book` models.
- `BookFields`: The names of all fields from the `Book` model, statically-available.
- `BookEncoder`: Allows `BookSerializer` to extend `Codec<Book, Map>`.
- `BookDecoder`: Also allows `BookSerializer` to extend `Codec<Book, Map>`.
And the following other features:
- `bookSerializer`: A top-level, `const` instance of `BookSerializer`.
- `Book.toString`: Prints out all of a `Book` instance's fields.
# Serialization
You can use the generated files as follows:
```dart
myFunction() {
var warAndPeace = new Book(
author: 'Leo Tolstoy',
title: 'War and Peace',
description: 'You will cry after reading this.',
pageCount: 1225
);
// Easily serialize models into Maps
var map = BookSerializer.toMap(warAndPeace);
// Also deserialize from Maps
var book = BookSerializer.fromMap(map);
print(book.title); // 'War and Peace'
// For compatibility with `JSON.encode`, a `toJson` method
// is included that forwards to `BookSerializer.toMap`:
expect(book.toJson(), map);
// Generated classes act as value types, and thus can be compared.
expect(BookSerializer.fromMap(map), equals(warAndPeace));
}
```
As of `2.0.2`, the generated output also includes information
about the serialized names of keys on your model class.
```dart
myOtherFunction() {
// Relying on the serialized key of a field? No worries.
map[BookFields.author] = 'Zora Neale Hurston';
}
}
```
## Customizing Serialization
Currently, these serialization methods are supported:
- to `Map`
- to JSON
- to TypeScript definitions
You can customize these by means of `serializers`:
```dart
@Serializable(serializers: const [Serializers.map, Serializers.json])
class _MyClass extends Model {}
```
## Subclasses
`angel_serialize` pulls in fields from parent classes, as well as
implemented interfaces, so it is extremely easy to share attributes among
model classes:
```dart
import 'package:angel_serialize/angel_serialize.dart';
part 'subclass.g.dart';
@serializable
class _Animal {
@notNull
String genus;
@notNull
String species;
}
@serializable
class _Bird extends _Animal {
@DefaultsTo(false)
bool isSparrow;
}
var saxaulSparrow = Bird(
genus: 'Passer',
species: 'ammodendri',
isSparrow: true,
);
```
## Aliases
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
provide a custom name, or pass `autoSnakeCaseNames`: `false` to the builder;
```dart
@serializable
abstract class _Spy extends Model {
/// Will show up as 'agency_id' in serialized JSON.
///
/// When deserializing JSON, instead of searching for an 'agencyId' key,
/// it will use 'agency_id'.
///
/// Hooray!
String agencyId;
@SerializableField(alias: 'foo')
String someOtherField;
}
```
You can also override `autoSnakeCaseNames` per model:
```dart
@Serializable(autoSnakeCaseNames: false)
abstract class _OtherCasing extends Model {
String camelCasedField;
}
```
## Excluding Keys
In pratice, there may keys that you want to exclude from JSON.
To accomplish this, simply annotate them with `@exclude`:
```dart
@serializable
abstract class _Whisper extends Model {
/// Will never be serialized to JSON
@SerializableField(exclude: true)
String secret;
}
```
There are times, however, when you want to only exclude either serialization
or deserialization, but not both. For example, you might want to deserialize
passwords from a database without sending them to users as JSON.
In this case, use `canSerialize` or `canDeserialize`:
```dart
@serializable
abstract class _Whisper extends Model {
/// Will never be serialized to JSON
///
/// ... But it can be deserialized
@SerializableField(exclude: true, canDeserialize: true)
String secret;
}
```
## Required Fields
It is easy to mark a field as required:
```dart
@serializable
abstract class _Foo extends Model {
@SerializableField(isNullable: false)
int myRequiredInt;
@SerializableField(isNullable: false, errorMessage: 'Custom message')
int myOtherRequiredInt;
}
```
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 {}
```
## Custom Serializers
`package:angel_serialize` does not cover every known Dart data type; you can add support for your own.
Provide `serializer` and `deserializer` arguments to `@SerializableField()` as you see fit.
They are typically used together. Note that the argument to `deserializer` will always be
`dynamic`, while `serializer` can receive the data type in question.
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(DateTime v) => v == null ? null : HttpDate.format(v);
@serializable
abstract class _HttpRequest {
@SerializableField(
serializer: #_dateToString,
deserializer: #_dateFromString,
serializesTo: String)
DateTime date;
}
```
# Nesting
`angel_serialize` also supports a few types of nesting of `@serializable` classes:
- As a class member, ex. `Book myField`
- As the type argument to a `List`, ex. `List<Book>`
- As the second type argument to a `Map`, ex. `Map<String, Book>`
In other words, the following are all legal, and will be serialized/deserialized.
You can use either the underscored name of a child class (ex. `_Book`), or the
generated class name (ex `Book`):
```dart
@serializable
abstract class _Author extends Model {
List<Book> books;
Book newestBook;
Map<String, Book> booksByIsbn;
}
```
If your model (`Author`) depends on a model defined in another file (`Book`),
then you will need to generate `book.g.dart` before, `author.g.dart`,
**in a separate build action**. This way, the analyzer can resolve the `Book` type.
# ID and Dates
This package will automatically generate `id`, `createdAt`, and `updatedAt` fields for you,
in the style of an Angel `Model`. This will automatically be generated, **only** for classes
extending `Model`.
# Binary Data
`package:angel_serialize` also handles `Uint8List` fields, by means of serialization to
and from `base64` encoding.
# TypeScript Definitions
It is quite common to build frontends with JavaScript and/or TypeScript,
so why not generate typings as well?
To accomplish this, add `Serializers.typescript` to your `@Serializable()` declaration:
```dart
@Serializable(serializers: const [Serializers.map, Serializers.json, Serializers.typescript])
class _Foo extends Model {}
```
The aforementioned `_Author` class will generate the following in `author.d.ts`:
```typescript
interface Author {
id: string;
name: string;
age: number;
books: Book[];
newest_book: Book;
created_at: any;
updated_at: any;
}
interface Library {
id: string;
collection: BookCollection;
created_at: any;
updated_at: any;
}
interface BookCollection {
[key: string]: Book;
}
```
Fields with an `@Exclude()` that specifies `canSerialize: false` will not be present in the
TypeScript definition. The rationale for this is that if a field (i.e. `password`) will
never be sent to the client, the client shouldn't even know the field exists.
# Constructor Parameters
Sometimes, you may need to have custom constructor parameters, for example, when
using depedency injection frameworks. For these cases, `angel_serialize` can forward
custom constructor parameters.
The following:
```dart
@serializable
abstract class _Bookmark extends _BookmarkBase {
@SerializableField(exclude: true)
final Book book;
int get page;
String get comment;
_Bookmark(this.book);
}
```
Generates:
```dart
class Bookmark extends _Bookmark {
Bookmark(Book book,
{this.id,
this.page,
this.comment,
this.createdAt,
this.updatedAt})
: super(book);
@override
final String id;
// ...
}
```

View file

@ -0,0 +1,57 @@
# See https://www.dartlang.org/tools/private-files.html
# Files and directories created by pub
../.packages
.packages
.pub/
build/
# If you're building an application, you may want to check-in your pubspec.lock
pubspec.lock
# Directory created by dartdoc
# If you don't generate documentation locally you can remove this line.
doc/api/
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/dictionaries
# Sensitive or high-churn files:
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.xml
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
# Gradle:
.idea/**/gradle.xml
.idea/**/libraries
# Mongo Explorer plugin:
.idea/**/mongoSettings.xml
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties

View file

@ -0,0 +1,47 @@
# 2.2.3+3
* Add `exclude: true` to `super` call in `Exclude` constructor.
# 2.2.3+2
* Apply `package:pedantic`.
# 2.2.3+1
* Export `json`, `Codec`, and `Converter` from `dart:convert`.
# 2.2.3
* `isNullable` defaults to `true`, and will not change.
* Deprecate `@nullable`.
* Add `@notNull`.
# 2.2.2+1
* Export commonly-used packages, for the sake of convenience.
# 2.2.2
* Add `HasAlias`, `DefaultsTo`, `nullable` idioms.
* `isNullable` defaults to `false` now.
# 2.2.1
* Add `serializesTo`.
# 2.2.0
* Add `@SerializableField`.
# 2.1.0
* Export `hashObjects`.
# 2.0.4+1
* Allow Dart 1 for this annotation-only package.
# 2.0.4
* Added `generatedSerializable`.
# 2.0.3
* Increased the upper SDK boundary.
# 2.0.2
* Added `DefaultValue`.
# 2.0.1
* Added `Serializers.typescript`.
# 2.0.0
* Dart 2+ constraint

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 The Angel Framework
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,5 @@
# angel_serialize
The frontend for Angel model serialization.
See documentation in the main project repo:
https://github.com/angel-dart/serialize

View file

@ -0,0 +1,4 @@
include: package:pedantic/analysis_options.yaml
analyzer:
strong-mode:
implicit-casts: false

View file

@ -0,0 +1,8 @@
// ignore_for_file: unused_element
import 'package:angel_serialize/angel_serialize.dart';
@serializable
class _Todo {
String text;
bool completed;
}

View file

@ -0,0 +1,148 @@
export 'dart:convert' show json, Codec, Converter;
export 'package:angel_model/angel_model.dart';
export 'package:collection/collection.dart';
export 'package:meta/meta.dart' show required, Required;
export 'package:quiver_hashcode/hashcode.dart' show hashObjects;
/// Excludes a field from being excluded.
class Exclude extends SerializableField {
const Exclude({bool canDeserialize = false, bool canSerialize = false})
: super(
exclude: true,
canDeserialize: canDeserialize,
canSerialize: canSerialize);
}
/// No longer necessary, as this is the default.
@deprecated
const SerializableField nullable = SerializableField(isNullable: true);
/// Marks a field as not accepting `null` values.
const SerializableField notNull = SerializableField(isNullable: false);
const Exclude exclude = Exclude();
/// Shorthand for [SerializableField].
class DefaultsTo extends SerializableField {
const DefaultsTo(value) : super(defaultValue: value);
}
/// Shorthand for [SerializableField].
class HasAlias extends SerializableField {
const HasAlias(String name) : super(alias: name);
}
/// Attaches options to a field.
class SerializableField {
/// An alternative name for this field.
final String alias;
/// A default for this field.
final defaultValue;
/// A custom serializer for this field.
final Symbol serializer;
/// A custom serializer for this field.
final Symbol deserializer;
/// An error message to be printed when the provided value is invalid.
final String errorMessage;
/// 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;
/// 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,
this.serializer,
this.deserializer,
this.errorMessage,
this.isNullable = true,
this.exclude = false,
this.canDeserialize = false,
this.canSerialize = false,
this.serializesTo});
}
/// Marks a class as eligible for serialization.
class Serializable {
const Serializable(
{this.serializers = const [Serializers.map, Serializers.json],
this.autoSnakeCaseNames = true,
// ignore: deprecated_member_use_from_same_package
@deprecated this.autoIdAndDateFields = true,
this.includeAnnotations = const []});
/// A list of enabled serialization modes.
///
/// See [Serializers].
final List<int> serializers;
/// Overrides the setting in `SerializerGenerator`.
final bool autoSnakeCaseNames;
/// Overrides the setting in `JsonModelGenerator`.
@deprecated
final bool autoIdAndDateFields;
/// A list of constant members to affix to the generated class.
final List includeAnnotations;
}
const Serializable serializable = Serializable();
/// Used by `package:angel_serialize_generator` to reliably identify generated models.
class GeneratedSerializable {
const GeneratedSerializable();
}
const GeneratedSerializable generatedSerializable = GeneratedSerializable();
/// The supported serialization types.
abstract class Serializers {
/// All supported serialization types.
static const List<int> all = [map, json, typescript];
/// Enable `fromMap` and `toMap` methods on the model class.
static const int map = 0;
/// Enable a `toJson` method on the model class.
static const int json = 1;
/// Generate a TypeScript definition file (`.d.ts`) for use on the client-side.
static const int typescript = 2;
}
@deprecated
class DefaultValue {
final value;
const DefaultValue(this.value);
}
@deprecated
/// Prefer [SerializableField] instead.
class Alias {
final String name;
const Alias(this.name);
}

View file

@ -0,0 +1,13 @@
name: angel_serialize
version: 2.2.3+3
description: Static annotations powering Angel model serialization. Combine with angel_serialize_generator for flexible modeling.
author: Tobe O <thosakwe@gmail.com>
homepage: https://github.com/angel-dart/serialize
environment:
sdk: '>=2.0.0-dev <3.0.0'
dependencies:
angel_model: ^1.0.0
collection: ^1.0.0
meta: ^1.0.0
pedantic: ^1.0.0
quiver_hashcode: ^2.0.0

View file

@ -0,0 +1,58 @@
# See https://www.dartlang.org/tools/private-files.html
# Files and directories created by pub
.packages
.pub/
build/
# If you're building an application, you may want to check-in your pubspec.lock
pubspec.lock
# Directory created by dartdoc
# If you don't generate documentation locally you can remove this line.
doc/api/
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/dictionaries
# Sensitive or high-churn files:
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.xml
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
# Gradle:
.idea/**/gradle.xml
.idea/**/libraries
# Mongo Explorer plugin:
.idea/**/mongoSettings.xml
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
*.g.part

View file

@ -0,0 +1,134 @@
# 2.5.0
* Support mutable models (again).
* Use `whereType()` instead of chaining `where()` and `cast()`.
* Support pulling fields from parent classes and interfaces.
* Only generate `const` constructors if *all*
fields lack a setter.
* Don't type-annotate initializing formals.
# 2.4.4
* Remove unnecessary `new` and `const`.
# 2.4.3
* Generate `Codec` and `Converter` classes.
* Generate `toString` methods.
* Include original documentation comments from the model.
# 2.4.2
* Fix bug where enums didn't support default values.
* Stop emitting `@required` on items with default values.
* Create default `@SerializableField` for fields without them.
# 2.4.1+1
* Change `as Iterable<Map>` to `.cast<Map>`.
# 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.
* Allow passing of annotations to the generated class.
* Fixted TypeScript `ref` generator.
# 2.3.0
* Add `@DefaultValue` support.
# 2.2.2
* Split out TS def builder, to emit to source.
# 2.2.1
* Explicit changes for assisting `angel_orm_generator`.
# 2.2.0
* Build to `cache`.
* Only generate one `.g.dart` file.
* Support for `Uint8List`.
* Use `.cast()` for `List`s and `Map`s of *non-`Model`* types.
# 2.1.2
* Add `declare module` to generated TypeScript files.
# 2.1.1
* Generate `hashCode`.
# 2.1.0
* Removed dependency on `package:id`.
* Update dependencies for Dart2Stable.
* `jsonModelBuilder` now uses `SharedPartBuilder`, rather than
`PartBuilder`.
# 2.0.10
* Generate `XFields.allFields` constant.
* No longer breaks in cases where `dynamic` is present.
* Call `toJson` in `toMap` on nested models.
* Never generate named parameters from private fields.
* Use the new `@generatedSerializable` to *always* find generated
models.
# 2.0.9+4
* Remove `defaults` in `build.yaml`.
# 2.0.9+3
* Fix a cast error when self-referencing nested list expressions.
# 2.0.9+2
* Fix previously unseen cast errors with enums.
# 2.0.9+1
* Fix a cast error when deserializing nested model classes.
# 2.0.9
* Upgrade to `source_gen@^0.8.0`.
# 2.0.8+3
* Don't fail on `null` in `toMap`.
* Support self-referencing via `toJson()`.
# 2.0.8+2
* Better discern when custom methods disqualify classes
from `const` protection.
# 2.0.8+1
* Fix generation of `const` constructors with iterables.
# 2.0.8
* Now supports de/serialization of `enum` types.
* Generate `const` constructors when possible.
* Remove `whereType`, perform manual coercion.
* Generate a `fromMap` with typecasting, for Dart 2's sake.
# 2.0.7
* Create unmodifiable Lists and Maps.
* Support `@required` on fields.
* Affix an `@immutable` annotation to classes, if
`package:meta` is imported.
* Add `/// <reference path="..." />` to TypeScript models.
# 2.0.6
* Support for using `abstract` to create immutable model classes.
* Add support for custom constructor parameters.
* Closed [#21](https://github.com/angel-dart/serialize/issues/21) - better naming
of `Map` types.
* Added overridden `==` operators.
# 2.0.5
* Deserialization now supports un-serialized `DateTime`.
* Better support for regular typed Lists and Maps in TypeScript.
# 2.0.4
* Fields in TypeScript definitions are now nullable by default.
# 2.0.3
* Added a `TypeScriptDefinitionBuilder`.
# 2.0.2
* Generates an `XFields` class with the serialized names of
all fields in a model class `X`.
* Removed unnecessary named parameters from `XSerializer.fromMap`.
# 2.0.1
* Ensured that `List` is only transformed if
it generically references a `Model`.

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 The Angel Framework
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,5 @@
# angel_serialize_generator
The builder for Angel's model serialization.
Find documentation in the main project repo:
https://github.com/angel-dart/serialize

View file

@ -0,0 +1,8 @@
include: package:pedantic/analysis_options.yaml
analyzer:
strong-mode:
implicit-casts: false
linter:
rules:
- unnecessary_new
- unnecessary_const

View file

@ -0,0 +1,37 @@
builders:
angel_serialize:
import: "package:angel_serialize_generator/angel_serialize_generator.dart"
builder_factories:
- jsonModelBuilder
- serializerBuilder
auto_apply: root_package
build_to: cache
build_extensions:
.dart:
- ".angel_serialize.g.part"
- ".angel_serialize_serializer.g.part"
applies_builders: ["source_gen|combining_builder", "source_gen|part_cleanup"]
runs_before: ["angel_orm_generator|angel_orm"]
typescript:
import: "package:angel_serialize_generator/angel_serialize_generator.dart"
builder_factories:
- typescriptDefinitionBuilder
auto_apply: root_package
build_to: source
build_extensions:
.dart:
- ".d.ts"
# targets:
# _book:
# sources:
# - "test/models/book.dart"
# - "test/models/has_map.dart"
# - "test/models/goat.dart"
# - "test/models/game_pad_button.dart"
# - "test/models/with_enum.dart"
# $default:
# dependencies:
# - "angel_serialize_generator:_book"
# sources:
# - "test/models/author.dart"
# - "test/models/game_pad.dart"

View file

@ -0,0 +1,9 @@
// ignore_for_file: unused_element
import 'package:angel_serialize/angel_serialize.dart';
part 'main.g.dart';
@serializable
class _Todo {
String text;
bool completed;
}

View file

@ -0,0 +1,89 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'main.dart';
// **************************************************************************
// JsonModelGenerator
// **************************************************************************
@generatedSerializable
class Todo extends _Todo {
Todo({this.text, this.completed});
@override
String text;
@override
bool completed;
Todo copyWith({String text, bool completed}) {
return Todo(
text: text ?? this.text, completed: completed ?? this.completed);
}
bool operator ==(other) {
return other is _Todo && other.text == text && other.completed == completed;
}
@override
int get hashCode {
return hashObjects([text, completed]);
}
@override
String toString() {
return "Todo(text=$text, completed=$completed)";
}
Map<String, dynamic> toJson() {
return TodoSerializer.toMap(this);
}
}
// **************************************************************************
// SerializerGenerator
// **************************************************************************
const TodoSerializer todoSerializer = TodoSerializer();
class TodoEncoder extends Converter<Todo, Map> {
const TodoEncoder();
@override
Map convert(Todo model) => TodoSerializer.toMap(model);
}
class TodoDecoder extends Converter<Map, Todo> {
const TodoDecoder();
@override
Todo convert(Map map) => TodoSerializer.fromMap(map);
}
class TodoSerializer extends Codec<Todo, Map> {
const TodoSerializer();
@override
get encoder => const TodoEncoder();
@override
get decoder => const TodoDecoder();
static Todo fromMap(Map map) {
return Todo(
text: map['text'] as String, completed: map['completed'] as bool);
}
static Map<String, dynamic> toMap(_Todo model) {
if (model == null) {
return null;
}
return {'text': model.text, 'completed': model.completed};
}
}
abstract class TodoFields {
static const List<String> allFields = <String>[text, completed];
static const String text = 'text';
static const String completed = 'completed';
}

View file

@ -0,0 +1,180 @@
library angel_serialize_generator;
import 'dart:async';
import 'dart:mirrors';
import 'dart:typed_data';
import 'package:analyzer/dart/constant/value.dart';
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/angel_serialize.dart';
import 'package:build/build.dart';
import 'package:code_buffer/code_buffer.dart';
import 'package:code_builder/code_builder.dart';
import 'package:path/path.dart' as p;
import 'package:recase/recase.dart';
import 'package:source_gen/source_gen.dart' hide LibraryBuilder;
import 'build_context.dart';
import 'context.dart';
part 'model.dart';
part 'serialize.dart';
part 'typescript.dart';
Builder jsonModelBuilder(_) {
return SharedPartBuilder(const [JsonModelGenerator()], 'angel_serialize');
}
Builder serializerBuilder(_) {
return SharedPartBuilder(
const [SerializerGenerator()], 'angel_serialize_serializer');
}
Builder typescriptDefinitionBuilder(_) {
return TypeScriptDefinitionBuilder();
}
/// Converts a [DartType] to a [TypeReference].
TypeReference convertTypeReference(DartType t) {
return TypeReference((b) {
b..symbol = t.name;
if (t is InterfaceType) {
b.types.addAll(t.typeArguments.map(convertTypeReference));
}
});
}
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) {
var type = v.type;
if (v.isNull) return 'null';
if (v.toBoolValue() != null) return v.toBoolValue().toString();
if (v.toIntValue() != null) return v.toIntValue().toString();
if (v.toDoubleValue() != null) return v.toDoubleValue().toString();
if (v.toSymbolValue() != null) return '#' + v.toSymbolValue();
if (v.toTypeValue() != null) return v.toTypeValue().name;
if (v.toListValue() != null) {
return 'const [' + v.toListValue().map(dartObjectToString).join(', ') + ']';
}
if (v.toMapValue() != null) {
return 'const {' +
v.toMapValue().entries.map((entry) {
var k = dartObjectToString(entry.key);
var v = dartObjectToString(entry.value);
return '$k: $v';
}).join(', ') +
'}';
}
if (v.toStringValue() != null) {
return literalString(v.toStringValue()).accept(DartEmitter()).toString();
}
if (type is InterfaceType && type.element.isEnum) {
// Find the index of the enum, then find the member.
for (var field in type.element.fields) {
if (field.isEnumConstant && field.isStatic) {
var value = type.element.getField(field.name).constantValue;
if (value == v) {
return '${type.name}.${field.name}';
}
}
}
}
throw ArgumentError(v.toString());
}
/// Determines if a type supports `package:angel_serialize`.
bool isModelClass(DartType t) {
if (t == null) return false;
if (serializableTypeChecker.hasAnnotationOf(t.element)) {
return true;
}
if (generatedSerializableTypeChecker.hasAnnotationOf(t.element)) {
return true;
}
if (const TypeChecker.fromRuntime(Model).isAssignableFromType(t)) {
return true;
}
if (t is InterfaceType) {
return isModelClass(t.superclass);
} else {
return false;
}
}
bool isListOrMapType(DartType t) {
return (const TypeChecker.fromRuntime(List).isAssignableFromType(t) ||
const TypeChecker.fromRuntime(Map).isAssignableFromType(t)) &&
!const TypeChecker.fromRuntime(Uint8List).isAssignableFromType(t);
}
bool isEnumType(DartType t) {
if (t is InterfaceType) {
return t.element.isEnum;
}
return false;
}
/// Determines if a [DartType] is a `List` with the first type argument being a `Model`.
bool isListOfModelType(InterfaceType t) {
return const TypeChecker.fromRuntime(List).isAssignableFromType(t) &&
t.typeArguments.length == 1 &&
isModelClass(t.typeArguments[0]);
}
/// Determines if a [DartType] is a `Map` with the second type argument being a `Model`.
bool isMapToModelType(InterfaceType t) {
return const TypeChecker.fromRuntime(Map).isAssignableFromType(t) &&
t.typeArguments.length == 2 &&
isModelClass(t.typeArguments[1]);
}
bool isAssignableToModel(DartType type) =>
const TypeChecker.fromRuntime(Model).isAssignableFromType(type);
/// Compute a [String] representation of a [type].
String typeToString(DartType type) {
if (type is InterfaceType) {
if (type.typeArguments.isEmpty) return type.name;
return type.name +
'<' +
type.typeArguments.map(typeToString).join(', ') +
'>';
} else {
return type.name;
}
}

View file

@ -0,0 +1,248 @@
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';
import 'package:angel_serialize/angel_serialize.dart';
import 'package:build/build.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as p;
import 'package:recase/recase.dart';
import 'package:source_gen/source_gen.dart';
import 'context.dart';
// ignore: deprecated_member_use
const TypeChecker aliasTypeChecker = TypeChecker.fromRuntime(Alias);
const TypeChecker dateTimeTypeChecker = TypeChecker.fromRuntime(DateTime);
// ignore: deprecated_member_use
const TypeChecker excludeTypeChecker = TypeChecker.fromRuntime(Exclude);
const TypeChecker serializableFieldTypeChecker =
TypeChecker.fromRuntime(SerializableField);
const TypeChecker serializableTypeChecker =
TypeChecker.fromRuntime(Serializable);
const TypeChecker generatedSerializableTypeChecker =
TypeChecker.fromRuntime(GeneratedSerializable);
final Map<String, BuildContext> _cache = {};
/// Create a [BuildContext].
Future<BuildContext> buildContext(ClassElement clazz, ConstantReader annotation,
BuildStep buildStep, Resolver resolver, bool autoSnakeCaseNames,
{bool heedExclude = true}) async {
var id = clazz.location.components.join('-');
if (_cache.containsKey(id)) {
return _cache[id];
}
// Check for autoIdAndDateFields, autoSnakeCaseNames
autoSnakeCaseNames =
annotation.peek('autoSnakeCaseNames')?.boolValue ?? autoSnakeCaseNames;
var ctx = BuildContext(
annotation,
clazz,
originalClassName: clazz.name,
sourceFilename: p.basename(buildStep.inputId.path),
autoSnakeCaseNames: autoSnakeCaseNames,
includeAnnotations:
annotation.peek('includeAnnotations')?.listValue ?? <DartObject>[],
);
// var lib = await resolver.libraryFor(buildStep.inputId);
List<String> fieldNames = [];
var fields = <FieldElement>[];
// Crawl for classes from parent classes.
void crawlClass(InterfaceType t) {
while (t != null) {
fields.insertAll(0, t.element.fields);
t.interfaces.forEach(crawlClass);
t = t.superclass;
}
}
crawlClass(clazz.type);
for (var field in fields) {
// Skip private fields
if (field.name.startsWith('_')) {
continue;
}
if (field.getter != null &&
(field.setter != null || field.getter.isAbstract)) {
var el = field.setter == null ? field.getter : field;
fieldNames.add(field.name);
// Check for @SerializableField
var fieldAnn = serializableFieldTypeChecker.firstAnnotationOf(el);
void handleSerializableField(SerializableFieldMirror sField) {
ctx.fieldInfo[field.name] = sField;
if (sField.defaultValue != null) {
ctx.defaults[field.name] = sField.defaultValue;
}
if (sField.alias != null) {
ctx.aliases[field.name] = sField.alias;
} else if (autoSnakeCaseNames != false) {
ctx.aliases[field.name] = 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;
}
if (sField.exclude) {
// ignore: deprecated_member_use
ctx.excluded[field.name] = Exclude(
canSerialize: sField.canSerialize,
canDeserialize: sField.canDeserialize,
);
}
}
if (fieldAnn != null) {
var cr = ConstantReader(fieldAnn);
var excluded = cr.peek('exclude')?.boolValue ?? false;
var sField = SerializableFieldMirror(
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 ?? !excluded,
canDeserialize: cr.peek('canDeserialize')?.boolValue ?? false,
canSerialize: cr.peek('canSerialize')?.boolValue ?? false,
exclude: excluded,
serializesTo: cr.peek('serializesTo')?.typeValue,
);
handleSerializableField(sField);
// Apply
} else {
var foundNone = true;
// Skip if annotated with @exclude
var excludeAnnotation = excludeTypeChecker.firstAnnotationOf(el);
if (excludeAnnotation != null) {
var cr = ConstantReader(excludeAnnotation);
foundNone = false;
// ignore: deprecated_member_use
ctx.excluded[field.name] = 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 = ConstantReader(defAnn).revive().positionalArguments[0];
ctx.defaults[field.name] = rev;
foundNone = false;
}
// Check for alias
// ignore: deprecated_member_use
Alias alias;
var aliasAnn = aliasTypeChecker.firstAnnotationOf(el);
if (aliasAnn != null) {
// ignore: deprecated_member_use
alias = Alias(aliasAnn.getField('name').toStringValue());
foundNone = false;
}
if (alias?.name?.isNotEmpty == true) {
ctx.aliases[field.name] = alias.name;
} else if (autoSnakeCaseNames != false) {
ctx.aliases[field.name] = ReCase(field.name).snakeCase;
}
// Check for @required
var required =
const TypeChecker.fromRuntime(Required).firstAnnotationOf(el);
if (required != null) {
log.warning(
'Using @required on fields (like ${clazz.name}.${field.name}) is now deprecated; use @SerializableField(isNullable: false) instead.');
var cr = ConstantReader(required);
var reason = cr.peek('reason')?.stringValue ??
"Missing required field '${ctx.resolveFieldName(field.name)}' on ${ctx.modelClassName}.";
ctx.requiredFields[field.name] = reason;
foundNone = false;
}
if (foundNone) {
var f = SerializableField();
var sField = SerializableFieldMirror(
alias: f.alias,
defaultValue: null,
serializer: f.serializer,
deserializer: f.deserializer,
errorMessage: f.errorMessage,
isNullable: f.isNullable,
canDeserialize: f.canDeserialize,
canSerialize: f.canSerialize,
exclude: f.exclude,
serializesTo: null,
);
handleSerializableField(sField);
}
}
ctx.fields.add(field);
}
}
// ShimFields are no longer used.
// if (const TypeChecker.fromRuntime(Model).isAssignableFromType(clazz.type)) {
// if (!fieldNames.contains('id')) {
// var idField = ShimFieldImpl('id', lib.context.typeProvider.stringType);
// ctx.fields.insert(0, idField);
// ctx.shimmed['id'] = true;
// }
// DartType dateTime;
// for (var key in ['createdAt', 'updatedAt']) {
// if (!fieldNames.contains(key)) {
// if (dateTime == null) {
// var coreLib =
// await resolver.libraries.singleWhere((lib) => lib.isDartCore);
// var dt = coreLib.getType('DateTime');
// dateTime = dt.type;
// }
// var field = ShimFieldImpl(key, dateTime);
// ctx.aliases[key] = ReCase(key).snakeCase;
// ctx.fields.add(field);
// ctx.shimmed[key] = true;
// }
// }
// }
// Get constructor params, if any
ctx.constructorParameters.addAll(clazz.unnamedConstructor.parameters);
return ctx;
}
/// A manually-instantiated [FieldElement].
class ShimFieldImpl extends FieldElementImpl {
@override
final DartType type;
ShimFieldImpl(String name, this.type) : super(name, -1);
}

View file

@ -0,0 +1,109 @@
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';
import 'package:source_gen/source_gen.dart';
/// A base context for building serializable classes.
class BuildContext {
ReCase _modelClassNameRecase;
TypeReference _modelClassType;
/// A map of fields that are absolutely required, and error messages for when they are absent.
final Map<String, String> requiredFields = {};
/// A map of field names to resolved names from `@Alias()` declarations.
final Map<String, String> aliases = {};
/// 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, SerializableFieldMirror> 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.
final Map<String, bool> shimmed = {};
final bool autoIdAndDateFields, autoSnakeCaseNames;
final String originalClassName, sourceFilename;
/// The fields declared on the original class.
final List<FieldElement> fields = [];
final List<ParameterElement> constructorParameters = [];
final ConstantReader annotation;
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';
BuildContext(this.annotation, this.clazz,
{this.originalClassName,
this.sourceFilename,
this.autoSnakeCaseNames,
this.autoIdAndDateFields,
this.includeAnnotations = const <DartObject>[]});
/// The name of the generated class.
String get modelClassName => originalClassName.startsWith('_')
? originalClassName.substring(1)
: originalClassName;
/// A [ReCase] instance reflecting on the [modelClassName].
ReCase get modelClassNameRecase =>
_modelClassNameRecase ??= ReCase(modelClassName);
TypeReference get modelClassType =>
_modelClassType ??= TypeReference((b) => b.symbol = modelClassName);
/// The [FieldElement] pointing to the primary key.
FieldElement get primaryKeyField =>
fields.firstWhere((f) => f.name == primaryKeyName);
bool get importsPackageMeta {
return clazz.library.imports.any((i) => i.uri == 'package:meta/meta.dart');
}
/// 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});
}

View file

@ -0,0 +1,294 @@
part of angel_serialize_generator;
class JsonModelGenerator extends GeneratorForAnnotation<Serializable> {
const JsonModelGenerator();
@override
Future<String> generateForAnnotatedElement(
Element element, ConstantReader annotation, BuildStep buildStep) async {
if (element.kind != ElementKind.CLASS) {
throw 'Only classes can be annotated with a @Serializable() annotation.';
}
var ctx = await buildContext(element as ClassElement, annotation, buildStep,
await buildStep.resolver, true);
var lib = Library((b) {
generateClass(ctx, b, annotation);
});
var buf = lib.accept(DartEmitter());
return buf.toString();
}
/// Generate an extended model class.
void generateClass(
BuildContext ctx, LibraryBuilder file, ConstantReader annotation) {
file.body.add(Class((clazz) {
clazz
..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(Reference(ctx.originalClassName));
} else {
clazz.extend = Reference(ctx.originalClassName);
}
//if (ctx.importsPackageMeta)
// clazz.annotations.add(CodeExpression(Code('immutable')));
for (var field in ctx.fields) {
clazz.fields.add(Field((b) {
b
..name = field.name
// ..modifier = FieldModifier.final$
..annotations.add(CodeExpression(Code('override')))
..type = convertTypeReference(field.type);
// Fields should only be forced-final if the original field has no setter.
if (field.setter == null && field is! ShimFieldImpl) {
b.modifier = FieldModifier.final$;
}
for (var el in [field.getter, field]) {
if (el?.documentationComment != null) {
b.docs.addAll(el.documentationComment.split('\n'));
}
}
}));
}
generateConstructor(ctx, clazz, file);
generateCopyWithMethod(ctx, clazz, file);
generateEqualsOperator(ctx, clazz, file);
generateHashCode(ctx, clazz);
generateToString(ctx, clazz);
// Generate toJson() method if necessary
var serializers = annotation.peek('serializers')?.listValue ?? [];
if (serializers.any((o) => o.toIntValue() == Serializers.json)) {
clazz.methods.add(Method((method) {
method
..name = 'toJson'
..returns = Reference('Map<String, dynamic>')
..body = Code('return ${clazz.name}Serializer.toMap(this);');
}));
}
}));
}
bool shouldBeConstant(BuildContext ctx) {
// Check if all fields are without a getter
return !isAssignableToModel(ctx.clazz.type) &&
ctx.clazz.fields.every((f) =>
f.getter?.isAbstract != false && f.setter?.isAbstract != false);
}
/// Generate a constructor with named parameters.
void generateConstructor(
BuildContext ctx, ClassBuilder clazz, LibraryBuilder file) {
clazz.constructors.add(Constructor((constructor) {
// Add all `super` params
constructor.constant = (ctx.clazz.unnamedConstructor?.isConst == true ||
shouldBeConstant(ctx)) &&
ctx.fields.every((f) {
return f.setter == null && f is! ShimFieldImpl;
});
for (var param in ctx.constructorParameters) {
constructor.requiredParameters.add(Parameter((b) => b
..name = param.name
..type = convertTypeReference(param.type)));
}
for (var field in ctx.fields) {
if (!shouldBeConstant(ctx) && isListOrMapType(field.type)) {
String typeName = const TypeChecker.fromRuntime(List)
.isAssignableFromType(field.type)
? 'List'
: 'Map';
var defaultValue = typeName == 'List' ? '[]' : '{}';
var existingDefault = ctx.defaults[field.name];
if (existingDefault != null) {
defaultValue = dartObjectToString(existingDefault);
}
constructor.initializers.add(Code('''
this.${field.name} =
$typeName.unmodifiable(${field.name} ?? $defaultValue)'''));
}
}
for (var field in ctx.fields) {
constructor.optionalParameters.add(Parameter((b) {
b
..toThis = shouldBeConstant(ctx)
..name = field.name
..named = true;
var existingDefault = ctx.defaults[field.name];
if (existingDefault != null) {
b.defaultTo = Code(dartObjectToString(existingDefault));
}
if (!isListOrMapType(field.type)) {
b.toThis = true;
} else if (!b.toThis) {
b.type = convertTypeReference(field.type);
}
if (ctx.requiredFields.containsKey(field.name) &&
b.defaultTo == null) {
b.annotations.add(CodeExpression(Code('required')));
}
}));
}
if (ctx.constructorParameters.isNotEmpty) {
if (!shouldBeConstant(ctx) ||
ctx.clazz.unnamedConstructor?.isConst == true) {
constructor.initializers.add(Code(
'super(${ctx.constructorParameters.map((p) => p.name).join(',')})'));
}
}
}));
}
/// Generate a `copyWith` method.
void generateCopyWithMethod(
BuildContext ctx, ClassBuilder clazz, LibraryBuilder file) {
clazz.methods.add(Method((method) {
method
..name = 'copyWith'
..returns = ctx.modelClassType;
// Add all `super` params
if (ctx.constructorParameters.isNotEmpty) {
for (var param in ctx.constructorParameters) {
method.requiredParameters.add(Parameter((b) => b
..name = param.name
..type = convertTypeReference(param.type)));
}
}
var buf = StringBuffer('return ${ctx.modelClassName}(');
int i = 0;
for (var param in ctx.constructorParameters) {
if (i++ > 0) buf.write(', ');
buf.write(param.name);
}
// Add named parameters
for (var field in ctx.fields) {
method.optionalParameters.add(Parameter((b) {
b
..name = field.name
..named = true
..type = convertTypeReference(field.type);
}));
if (i++ > 0) buf.write(', ');
buf.write('${field.name}: ${field.name} ?? this.${field.name}');
}
buf.write(');');
method.body = Code(buf.toString());
}));
}
static String generateEquality(DartType type, [bool nullable = false]) {
if (type is InterfaceType) {
if (const TypeChecker.fromRuntime(List).isAssignableFromType(type)) {
if (type.typeParameters.length == 1) {
var eq = generateEquality(type.typeArguments[0]);
return 'ListEquality<${type.typeArguments[0].name}>($eq)';
} else {
return 'ListEquality()';
}
} else if (const TypeChecker.fromRuntime(Map)
.isAssignableFromType(type)) {
if (type.typeParameters.length == 2) {
var keq = generateEquality(type.typeArguments[0]),
veq = generateEquality(type.typeArguments[1]);
return 'MapEquality<${type.typeArguments[0].name}, ${type.typeArguments[1].name}>(keys: $keq, values: $veq)';
} else {
return 'MapEquality()';
}
}
return nullable ? null : 'DefaultEquality<${type.name}>()';
} else {
return 'DefaultEquality()';
}
}
static String Function(String, String) generateComparator(DartType type) {
if (type is! InterfaceType || type.name == 'dynamic') {
return (a, b) => '$a == $b';
}
var eq = generateEquality(type, true);
if (eq == null) return (a, b) => '$a == $b';
return (a, b) => '$eq.equals($a, $b)';
}
void generateHashCode(BuildContext ctx, ClassBuilder clazz) {
clazz
..methods.add(Method((method) {
method
..name = 'hashCode'
..type = MethodType.getter
..returns = refer('int')
..annotations.add(refer('override'))
..body = refer('hashObjects')
.call([literalList(ctx.fields.map((f) => refer(f.name)))])
.returned
.statement;
}));
}
void generateToString(BuildContext ctx, ClassBuilder clazz) {
clazz.methods.add(Method((b) {
b
..name = 'toString'
..returns = refer('String')
..annotations.add(refer('override'))
..body = Block((b) {
var buf = StringBuffer('\"${ctx.modelClassName}(');
var i = 0;
for (var field in ctx.fields) {
if (i++ > 0) buf.write(', ');
buf.write('${field.name}=\$${field.name}');
}
buf.write(')\"');
b.addExpression(CodeExpression(Code(buf.toString())).returned);
});
}));
}
void generateEqualsOperator(
BuildContext ctx, ClassBuilder clazz, LibraryBuilder file) {
clazz.methods.add(Method((method) {
method
..name = 'operator =='
..returns = Reference('bool')
..requiredParameters.add(Parameter((b) => b.name = 'other'));
var buf = ['other is ${ctx.originalClassName}'];
buf.addAll(ctx.fields.map((f) {
return generateComparator(f.type)('other.${f.name}', f.name);
}));
method.body = Code('return ${buf.join('&&')};');
}));
}
}

View file

@ -0,0 +1,394 @@
part of angel_serialize_generator;
class SerializerGenerator extends GeneratorForAnnotation<Serializable> {
final bool autoSnakeCaseNames;
const SerializerGenerator({this.autoSnakeCaseNames = true});
@override
Future<String> generateForAnnotatedElement(
Element element, ConstantReader annotation, BuildStep buildStep) async {
if (element.kind != ElementKind.CLASS) {
throw 'Only classes can be annotated with a @Serializable() annotation.';
}
var ctx = await buildContext(element as ClassElement, annotation, buildStep,
await buildStep.resolver, autoSnakeCaseNames != false);
var serializers = annotation.peek('serializers')?.listValue ?? [];
if (serializers.isEmpty) return null;
// Check if any serializer is recognized
if (!serializers.any((s) => Serializers.all.contains(s.toIntValue()))) {
return null;
}
var lib = Library((b) {
generateClass(serializers.map((s) => s.toIntValue()).toList(), ctx, b);
generateFieldsClass(ctx, b);
});
var buf = lib.accept(DartEmitter());
return buf.toString();
}
/// Generate a serializer class.
void generateClass(
List<int> serializers, BuildContext ctx, LibraryBuilder file) {
// Generate canonical codecs, etc.
var pascal = ctx.modelClassNameRecase.pascalCase,
camel = ctx.modelClassNameRecase.camelCase;
if (ctx.constructorParameters.isEmpty) {
file.body.add(Code('''
const ${pascal}Serializer ${camel}Serializer = ${pascal}Serializer();
class ${pascal}Encoder extends Converter<${pascal}, Map> {
const ${pascal}Encoder();
@override
Map convert(${pascal} model) => ${pascal}Serializer.toMap(model);
}
class ${pascal}Decoder extends Converter<Map, ${pascal}> {
const ${pascal}Decoder();
@override
${pascal} convert(Map map) => ${pascal}Serializer.fromMap(map);
}
'''));
}
file.body.add(Class((clazz) {
clazz..name = '${pascal}Serializer';
if (ctx.constructorParameters.isEmpty) {
clazz
..extend = TypeReference((b) => b
..symbol = 'Codec'
..types.addAll([ctx.modelClassType, refer('Map')]));
// Add constructor, Codec impl, etc.
clazz.constructors.add(Constructor((b) => b..constant = true));
clazz.methods.add(Method((b) => b
..name = 'encoder'
..type = MethodType.getter
..annotations.add(refer('override'))
..body = refer('${pascal}Encoder').constInstance([]).code));
clazz.methods.add(Method((b) => b
..name = 'decoder'
..type = MethodType.getter
..annotations.add(refer('override'))
..body = refer('${pascal}Decoder').constInstance([]).code));
} else {
clazz.abstract = true;
}
if (serializers.contains(Serializers.map)) {
generateFromMapMethod(clazz, ctx, file);
}
if (serializers.contains(Serializers.map) ||
serializers.contains(Serializers.json)) {
generateToMapMethod(clazz, ctx, file);
}
}));
}
void generateToMapMethod(
ClassBuilder clazz, BuildContext ctx, LibraryBuilder file) {
clazz.methods.add(Method((method) {
method
..static = true
..name = 'toMap'
..returns = Reference('Map<String, dynamic>')
..requiredParameters.add(Parameter((b) {
b
..name = 'model'
..type = refer(ctx.originalClassName);
}));
var buf = StringBuffer();
ctx.requiredFields.forEach((key, msg) {
if (ctx.excluded[key]?.canSerialize == false) return;
buf.writeln('''
if (model.$key == null) {
throw FormatException("$msg");
}
''');
});
buf.writeln('return {');
int i = 0;
// 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;
var alias = ctx.resolveFieldName(field.name);
if (i++ > 0) buf.write(', ');
String serializedRepresentation = 'model.${field.name}';
String serializerToMap(ReCase rc, String value) {
// if (rc.pascalCase == ctx.modelClassName) {
// return '($value)?.toJson()';
// }
return '${rc.pascalCase}Serializer.toMap($value)';
}
if (ctx.fieldInfo[field.name]?.serializer != null) {
var name = MirrorSystem.getName(ctx.fieldInfo[field.name].serializer);
serializedRepresentation = '$name(model.${field.name})';
}
// Serialize dates
else if (dateTimeTypeChecker.isAssignableFromType(type)) {
serializedRepresentation = 'model.${field.name}?.toIso8601String()';
}
// Serialize model classes via `XSerializer.toMap`
else if (isModelClass(type)) {
var rc = ReCase(type.name);
serializedRepresentation =
'${serializerToMap(rc, 'model.${field.name}')}';
} else if (type is InterfaceType) {
if (isListOfModelType(type)) {
var name = type.typeArguments[0].name;
if (name.startsWith('_')) name = name.substring(1);
var rc = ReCase(name);
var m = serializerToMap(rc, 'm');
serializedRepresentation = '''
model.${field.name}
?.map((m) => $m)
?.toList()''';
} else if (isMapToModelType(type)) {
var rc = ReCase(type.typeArguments[1].name);
serializedRepresentation =
'''model.${field.name}.keys?.fold({}, (map, key) {
return map..[key] =
${serializerToMap(rc, 'model.${field.name}[key]')};
})''';
} else if (type.element.isEnum) {
serializedRepresentation = '''
model.${field.name} == null ?
null
: ${type.name}.values.indexOf(model.${field.name})
''';
} else if (const TypeChecker.fromRuntime(Uint8List)
.isAssignableFromType(type)) {
serializedRepresentation = '''
model.${field.name} == null ?
null
: base64.encode(model.${field.name})
''';
}
}
buf.write("'$alias': $serializedRepresentation");
}
buf.write('};');
method.body = Block.of([
Code('if (model == null) { return null; }'),
Code(buf.toString()),
]);
}));
}
void generateFromMapMethod(
ClassBuilder clazz, BuildContext ctx, LibraryBuilder file) {
clazz.methods.add(Method((method) {
method
..static = true
..name = 'fromMap'
..returns = ctx.modelClassType
..requiredParameters.add(
Parameter((b) => b
..name = 'map'
..type = Reference('Map')),
);
// Add all `super` params
if (ctx.constructorParameters.isNotEmpty) {
for (var param in ctx.constructorParameters) {
method.requiredParameters.add(Parameter((b) => b
..name = param.name
..type = convertTypeReference(param.type)));
}
}
var buf = StringBuffer();
ctx.requiredFields.forEach((key, msg) {
if (ctx.excluded[key]?.canDeserialize == false) return;
var name = ctx.resolveFieldName(key);
buf.writeln('''
if (map['$name'] == null) {
throw FormatException("$msg");
}
''');
});
buf.writeln('return ${ctx.modelClassName}(');
int i = 0;
for (var param in ctx.constructorParameters) {
if (i++ > 0) buf.write(', ');
buf.write(param.name);
}
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);
if (i++ > 0) buf.write(', ');
String deserializedRepresentation =
"map['$alias'] as ${typeToString(type)}";
var defaultValue = 'null';
var existingDefault = ctx.defaults[field.name];
if (existingDefault != null) {
defaultValue = dartObjectToString(existingDefault);
deserializedRepresentation =
'$deserializedRepresentation ?? $defaultValue';
}
if (ctx.fieldInfo[field.name]?.deserializer != null) {
var name =
MirrorSystem.getName(ctx.fieldInfo[field.name].deserializer);
deserializedRepresentation = "$name(map['$alias'])";
} 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(type)) {
var rc = ReCase(type.name);
deserializedRepresentation = "map['$alias'] != null"
" ? ${rc.pascalCase}Serializer.fromMap(map['$alias'] as Map)"
" : $defaultValue";
} else if (type is InterfaceType) {
if (isListOfModelType(type)) {
var rc = ReCase(type.typeArguments[0].name);
deserializedRepresentation = "map['$alias'] is Iterable"
" ? List.unmodifiable(((map['$alias'] as Iterable)"
".whereType<Map>())"
".map(${rc.pascalCase}Serializer.fromMap))"
" : $defaultValue";
} else if (isMapToModelType(type)) {
var rc = ReCase(type.typeArguments[1].name);
deserializedRepresentation = '''
map['$alias'] is Map
? Map.unmodifiable((map['$alias'] as Map).keys.fold({}, (out, key) {
return out..[key] = ${rc.pascalCase}Serializer
.fromMap(((map['$alias'] as Map)[key]) as Map);
}))
: $defaultValue
''';
} else if (type.element.isEnum) {
deserializedRepresentation = '''
map['$alias'] is ${type.name}
? (map['$alias'] as ${type.name})
:
(
map['$alias'] is int
? ${type.name}.values[map['$alias'] as int]
: $defaultValue
)
''';
} else if (const TypeChecker.fromRuntime(List)
.isAssignableFromType(type) &&
type.typeArguments.length == 1) {
var arg = convertTypeReference(type.typeArguments[0])
.accept(DartEmitter());
deserializedRepresentation = '''
map['$alias'] is Iterable
? (map['$alias'] as Iterable).cast<$arg>().toList()
: $defaultValue
''';
} else if (const TypeChecker.fromRuntime(Map)
.isAssignableFromType(type) &&
type.typeArguments.length == 2) {
var key = convertTypeReference(type.typeArguments[0])
.accept(DartEmitter());
var value = convertTypeReference(type.typeArguments[1])
.accept(DartEmitter());
deserializedRepresentation = '''
map['$alias'] is Map
? (map['$alias'] as Map).cast<$key, $value>()
: $defaultValue
''';
} else if (const TypeChecker.fromRuntime(Uint8List)
.isAssignableFromType(type)) {
deserializedRepresentation = '''
map['$alias'] is Uint8List
? (map['$alias'] as Uint8List)
:
(
map['$alias'] is Iterable<int>
? Uint8List.fromList((map['$alias'] as Iterable<int>).toList())
:
(
map['$alias'] is String
? Uint8List.fromList(base64.decode(map['$alias'] as String))
: $defaultValue
)
)
''';
}
}
buf.write('${field.name}: $deserializedRepresentation');
}
buf.write(');');
method.body = Code(buf.toString());
}));
}
void generateFieldsClass(BuildContext ctx, LibraryBuilder file) {
file.body.add(Class((clazz) {
clazz
..abstract = true
..name = '${ctx.modelClassNameRecase.pascalCase}Fields';
clazz.fields.add(Field((b) {
b
..static = true
..modifier = FieldModifier.constant
..type = TypeReference((b) => b
..symbol = 'List'
..types.add(refer('String')))
..name = 'allFields'
..assignment = literalConstList(
ctx.fields.map((f) => refer(f.name)).toList(),
refer('String'))
.code;
}));
for (var field in ctx.fields) {
clazz.fields.add(Field((b) {
b
..static = true
..modifier = FieldModifier.constant
..type = Reference('String')
..name = field.name
..assignment = Code("'${ctx.resolveFieldName(field.name)}'");
}));
}
}));
}
}

View file

@ -0,0 +1,223 @@
part of angel_serialize_generator;
class TypeScriptDefinitionBuilder implements Builder {
final bool autoSnakeCaseNames;
const TypeScriptDefinitionBuilder({this.autoSnakeCaseNames = true});
@override
Map<String, List<String>> get buildExtensions {
return {
'.dart': ['.d.ts']
};
}
Future<String> compileToTypeScriptType(
BuildContext ctx,
String fieldName,
DartType type,
List<String> refs,
List<CodeBuffer> ext,
BuildStep buildStep) async {
String typeScriptType = 'any';
var types = const {
num: 'number',
bool: 'boolean',
String: 'string',
Symbol: 'Symbol',
};
types.forEach((t, tsType) {
if (TypeChecker.fromRuntime(t).isAssignableFromType(type)) {
typeScriptType = tsType;
}
});
if (type is InterfaceType) {
if (isListOfModelType(type)) {
var arg = await compileToTypeScriptType(
ctx, fieldName, type.typeArguments[0], refs, ext, buildStep);
typeScriptType = '$arg[]';
} else if (const TypeChecker.fromRuntime(Map)
.isAssignableFromType(type) &&
type.typeArguments.length == 2) {
var key = await compileToTypeScriptType(
ctx, fieldName, type.typeArguments[0], refs, ext, buildStep);
var value = await compileToTypeScriptType(
ctx, fieldName, type.typeArguments[1], refs, ext, buildStep);
//var modelType = type.typeArguments[1];
/*var innerCtx = await buildContext(
modelType.element,
ConstantReader(
serializableTypeChecker.firstAnnotationOf(modelType.element)),
buildStep,
buildStep.resolver,
autoSnakeCaseNames,
true,
);*/
typeScriptType =
ctx.modelClassNameRecase.pascalCase + ReCase(fieldName).pascalCase;
ext.add(CodeBuffer()
..writeln('interface $typeScriptType {')
..indent()
..writeln('[key: $key]: $value;')
..outdent()
..writeln('}'));
} else if (const TypeChecker.fromRuntime(List)
.isAssignableFromType(type)) {
if (type.typeArguments.isEmpty) {
typeScriptType = 'any[]';
} else {
var arg = await compileToTypeScriptType(
ctx, fieldName, type.typeArguments[0], refs, ext, buildStep);
typeScriptType = '$arg[]';
}
} else if (isModelClass(type)) {
var sourcePath = buildStep.inputId.uri.toString();
var targetPath = type.element.source.uri.toString();
if (!p.equals(sourcePath, targetPath)) {
var relative = p.relative(targetPath, from: sourcePath);
String ref;
if (type.element.source.uri.scheme == 'asset') {
var id = AssetId.resolve(type.element.source.uri.toString());
if (id.package != buildStep.inputId.package) {
ref = '/// <reference types="${id.package}" />';
}
}
if (ref == null) {
// var relative = (p.dirname(targetPath) == p.dirname(sourcePath))
// ? p.basename(targetPath)
// : p.relative(targetPath, from: sourcePath);
var parent = p.dirname(relative);
var filename =
p.setExtension(p.basenameWithoutExtension(relative), '.d.ts');
relative = p.joinAll(p.split(parent).toList()..add(filename));
ref = '/// <reference path="$relative" />';
}
if (!refs.contains(ref)) refs.add(ref);
}
var ctx = await buildContext(
type.element,
ConstantReader(
serializableTypeChecker.firstAnnotationOf(type.element)),
buildStep,
buildStep.resolver,
autoSnakeCaseNames,
);
typeScriptType = ctx.modelClassNameRecase.pascalCase;
}
}
return typeScriptType;
}
@override
Future build(BuildStep buildStep) async {
var contexts = <BuildContext>[];
LibraryReader lib;
try {
lib = LibraryReader(await buildStep.inputLibrary);
} catch (_) {
return;
}
var elements = <AnnotatedElement>[];
try {
elements = lib
.annotatedWith(const TypeChecker.fromRuntime(Serializable))
.toList();
} catch (_) {
// Ignore error in source_gen/build_runner that has no explanation
}
for (var element in elements) {
if (element.element.kind != ElementKind.CLASS) {
throw 'Only classes can be annotated with a @Serializable() annotation.';
}
var annotation = element.annotation;
var serializers = annotation.peek('serializers')?.listValue ?? [];
if (serializers.isEmpty) continue;
// Check if TypeScript serializer is supported
if (!serializers.any((s) => s.toIntValue() == Serializers.typescript)) {
continue;
}
contexts.add(await buildContext(
element.element as ClassElement,
element.annotation,
buildStep,
await buildStep.resolver,
autoSnakeCaseNames != false));
}
if (contexts.isEmpty) return;
var refs = <String>[];
var buf = CodeBuffer(
trailingNewline: true,
sourceUrl: buildStep.inputId.uri,
);
buf.writeln('// GENERATED CODE - DO NOT MODIFY BY HAND');
// declare module `foo` {
buf
..writeln("declare module '${buildStep.inputId.package}' {")
..indent();
for (var ctx in contexts) {
// interface Bar { ... }
buf
..writeln('interface ${ctx.modelClassNameRecase.pascalCase} {')
..indent();
var ext = <CodeBuffer>[];
for (var field in ctx.fields) {
// Skip excluded fields
if (ctx.excluded[field.name]?.canSerialize == false) continue;
var alias = ctx.resolveFieldName(field.name);
var typeScriptType = await compileToTypeScriptType(ctx, field.name,
ctx.resolveSerializedFieldType(field.name), refs, ext, buildStep);
// foo: string;
if (!ctx.requiredFields.containsKey(field.name)) alias += '?';
buf.writeln('$alias: $typeScriptType;');
}
buf
..outdent()
..writeln('}');
for (var b in ext) {
b.copyInto(buf);
}
}
buf
..outdent()
..writeln('}');
var finalBuf = CodeBuffer();
refs.forEach(finalBuf.writeln);
buf.copyInto(finalBuf);
await buildStep.writeAsString(
buildStep.inputId.changeExtension('.d.ts'),
finalBuf.toString(),
);
}
}

View file

@ -0,0 +1,28 @@
name: angel_serialize_generator
version: 2.5.0
description: Model serialization generators, designed for use with Angel. Combine with angel_serialize for flexible modeling.
author: Tobe O <thosakwe@gmail.com>
homepage: https://github.com/angel-dart/serialize
environment:
sdk: '>=2.0.0 <3.0.0'
dependencies:
analyzer: ">=0.27.1 <2.0.0"
angel_model: ^1.0.0
angel_serialize: ^2.2.0
build: ">=0.12.0 <2.0.0"
build_config: ">=0.3.0 <2.0.0"
code_buffer: ^1.0.0
code_builder: ^3.0.0
meta: ^1.0.0
path: ^1.0.0
recase: ^2.0.0
source_gen: ^0.9.0
quiver: ^2.0.0
dev_dependencies:
build_runner: ^1.0.0
collection: ^1.0.0
pedantic: ^1.0.0
test: ^1.0.0
# dependency_overrides:
# angel_serialize:
# path: ../angel_serialize

View file

@ -0,0 +1,161 @@
import 'package:test/test.dart';
import 'models/book.dart';
const String deathlyHallowsIsbn = '0-545-01022-5';
main() {
var deathlyHallows = Book(
id: '0',
author: 'J.K. Rowling',
title: 'Harry Potter and the Deathly Hallows',
description: 'The 7th book.',
pageCount: 759,
notModels: [1.0, 3.0],
updatedAt: DateTime.now());
var serializedDeathlyHallows = deathlyHallows.toJson();
print('Deathly Hallows: $deathlyHallows');
var jkRowling = Author(
id: '1',
name: 'J.K. Rowling',
age: 51,
books: [deathlyHallows],
newestBook: deathlyHallows);
var serializedJkRowling = authorSerializer.encode(jkRowling);
var deathlyHallowsMap = bookSerializer.encode(deathlyHallows);
print('J.K. Rowling: $jkRowling');
var library = Library(collection: {deathlyHallowsIsbn: deathlyHallows});
var serializedLibrary = LibrarySerializer.toMap(library);
print('Library: $library');
group('serialization', () {
test('serialization sets proper fields', () {
expect(serializedDeathlyHallows['id'], deathlyHallows.id);
expect(serializedDeathlyHallows['author'], deathlyHallows.author);
expect(
serializedDeathlyHallows['description'], deathlyHallows.description);
expect(serializedDeathlyHallows['page_count'], deathlyHallows.pageCount);
expect(serializedDeathlyHallows['created_at'], isNull);
expect(serializedDeathlyHallows['updated_at'],
deathlyHallows.updatedAt.toIso8601String());
});
test('can be mutated', () {
var b = deathlyHallows.copyWith();
b.author = 'Hey';
expect(b.author, 'Hey');
expect(b.toJson()[BookFields.author], 'Hey');
});
test('heeds @Alias', () {
expect(serializedDeathlyHallows['page_count'], deathlyHallows.pageCount);
expect(serializedDeathlyHallows.keys, isNot(contains('pageCount')));
});
test('standard list', () {
expect(serializedDeathlyHallows['not_models'], deathlyHallows.notModels);
});
test('heeds @exclude', () {
expect(serializedJkRowling.keys, isNot(contains('secret')));
});
test('heeds canDeserialize', () {
var map = Map.from(serializedJkRowling)..['obscured'] = 'foo';
var author = authorSerializer.decode(map);
expect(author.obscured, 'foo');
});
test('heeds canSerialize', () {
expect(serializedJkRowling.keys, isNot(contains('obscured')));
});
test('nested @serializable class is serialized', () {
expect(serializedJkRowling['newest_book'], deathlyHallowsMap);
});
test('list of nested @serializable class is serialized', () {
expect(serializedJkRowling['books'], [deathlyHallowsMap]);
});
test('map with @serializable class as second key is serialized', () {
expect(serializedLibrary['collection'],
{deathlyHallowsIsbn: deathlyHallowsMap});
});
});
test('fields', () {
expect(BookFields.author, 'author');
expect(BookFields.notModels, 'not_models');
expect(BookFields.camelCaseString, 'camelCase');
});
test('equals', () {
expect(jkRowling.copyWith(), jkRowling);
expect(deathlyHallows.copyWith(), deathlyHallows);
expect(library.copyWith(), library);
});
test('custom method', () {
expect(jkRowling.customMethod, 'hey!');
});
test('required fields fromMap', () {
expect(() => AuthorSerializer.fromMap({}), throwsFormatException);
});
test('required fields toMap', () {
var author = Author(name: null, age: 24);
expect(() => author.toJson(), throwsFormatException);
});
group('deserialization', () {
test('deserialization sets proper fields', () {
var book = BookSerializer.fromMap(deathlyHallowsMap);
expect(book.id, deathlyHallows.id);
expect(book.author, deathlyHallows.author);
expect(book.description, deathlyHallows.description);
expect(book.pageCount, deathlyHallows.pageCount);
expect(book.notModels, deathlyHallows.notModels);
expect(book.createdAt, isNull);
expect(book.updatedAt, deathlyHallows.updatedAt);
});
group('nested @serializable', () {
var author = AuthorSerializer.fromMap(serializedJkRowling);
test('nested @serializable class is deserialized', () {
var newestBook = author.newestBook;
expect(newestBook, isNotNull);
expect(newestBook.id, deathlyHallows.id);
expect(newestBook.pageCount, deathlyHallows.pageCount);
expect(newestBook.updatedAt, deathlyHallows.updatedAt);
});
test('list of nested @serializable class is deserialized', () {
expect(author.books, allOf(isList, isNotEmpty, hasLength(1)));
var book = author.books.first;
expect(book.id, deathlyHallows.id);
expect(book.author, deathlyHallows.author);
expect(book.description, deathlyHallows.description);
expect(book.pageCount, deathlyHallows.pageCount);
expect(book.createdAt, isNull);
expect(book.updatedAt, deathlyHallows.updatedAt);
});
test('map with @serializable class as second key is deserialized', () {
var lib = LibrarySerializer.fromMap(serializedLibrary);
expect(lib.collection, allOf(isNotEmpty, hasLength(1)));
expect(lib.collection.keys.first, deathlyHallowsIsbn);
var book = lib.collection[deathlyHallowsIsbn];
expect(book.id, deathlyHallows.id);
expect(book.author, deathlyHallows.author);
expect(book.description, deathlyHallows.description);
expect(book.pageCount, deathlyHallows.pageCount);
expect(book.createdAt, isNull);
expect(book.updatedAt, deathlyHallows.updatedAt);
});
});
});
}

View file

@ -0,0 +1,24 @@
import 'package:test/test.dart';
import 'models/goat.dart';
void main() {
group('constructor', () {
test('int default', () {
expect(Goat().integer, 34);
});
test('list default', () {
expect(Goat().list, [34, 35]);
});
});
group('from map', () {
test('int default', () {
expect(GoatSerializer.fromMap({}).integer, 34);
});
test('list default', () {
expect(GoatSerializer.fromMap({}).list, [34, 35]);
});
});
}

View file

@ -0,0 +1,59 @@
import 'dart:typed_data';
import 'package:test/test.dart';
import 'models/with_enum.dart';
const WithEnum aWithEnum = WithEnum(type: WithEnumType.a);
const WithEnum aWithEnum2 = WithEnum(type: WithEnumType.a);
void main() {
test('enum serializes to int', () {
var w = WithEnum(type: WithEnumType.b).toJson();
expect(w[WithEnumFields.type], WithEnumType.values.indexOf(WithEnumType.b));
});
test('enum serializes null if null', () {
var w = WithEnum(type: null).toJson();
expect(w[WithEnumFields.type], null);
});
test('enum deserializes to default value from null', () {
var map = {WithEnumFields.type: null};
var w = WithEnumSerializer.fromMap(map);
expect(w.type, WithEnumType.b);
});
test('enum deserializes from int', () {
var map = {
WithEnumFields.type: WithEnumType.values.indexOf(WithEnumType.b)
};
var w = WithEnumSerializer.fromMap(map);
expect(w.type, WithEnumType.b);
});
test('enum deserializes from value', () {
var map = {WithEnumFields.type: WithEnumType.c};
var w = WithEnumSerializer.fromMap(map);
expect(w.type, WithEnumType.c);
});
test('equality', () {
expect(WithEnum(type: WithEnumType.a), WithEnum(type: WithEnumType.a));
expect(
WithEnum(type: WithEnumType.a), isNot(WithEnum(type: WithEnumType.b)));
});
test('const', () {
expect(identical(aWithEnum, aWithEnum2), true);
});
test('uint8list', () {
var ee = WithEnum(
imageBytes: Uint8List.fromList(List<int>.generate(1000, (i) => i)));
var eeMap = ee.toJson();
print(ee);
var ef = WithEnumSerializer.fromMap(eeMap);
expect(ee.copyWith(), ee);
expect(ef, ee);
});
}

View file

@ -0,0 +1,40 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
declare module 'angel_serialize_generator' {
interface Book {
id?: string;
created_at?: any;
updated_at?: any;
author?: string;
title?: string;
description?: string;
page_count?: number;
not_models?: number[];
camelCase?: string;
}
interface Author {
id?: string;
created_at?: any;
updated_at?: any;
name: string;
age: number;
books?: Book[];
newest_book?: Book;
}
interface Library {
id?: string;
created_at?: any;
updated_at?: any;
collection?: LibraryCollection;
}
interface LibraryCollection {
[key: string]: Book;
}
interface Bookmark {
id?: string;
created_at?: any;
updated_at?: any;
history?: number[];
page: number;
comment?: string;
}
}

View file

@ -0,0 +1,69 @@
library angel_serialize.test.models.book;
import 'package:angel_model/angel_model.dart';
import 'package:angel_serialize/angel_serialize.dart';
import 'package:collection/collection.dart';
import 'package:meta/meta.dart';
part 'book.g.dart';
@Serializable(
serializers: Serializers.all,
includeAnnotations: [
pragma('hello'),
SerializableField(alias: 'omg'),
],
)
abstract class _Book extends Model {
String author, title, description;
/// The number of pages the book has.
int pageCount;
List<double> notModels;
@SerializableField(alias: 'camelCase', isNullable: true)
String camelCaseString;
}
@Serializable(serializers: Serializers.all)
abstract class _Author extends Model {
@SerializableField(isNullable: false)
String get name;
String get customMethod => 'hey!';
@SerializableField(
isNullable: false, errorMessage: 'Custom message for missing `age`')
int get age;
List<_Book> get books;
/// The newest book.
_Book get newestBook;
@SerializableField(exclude: true, isNullable: true)
String get secret;
@SerializableField(exclude: true, canDeserialize: true, isNullable: true)
String get obscured;
}
@Serializable(serializers: Serializers.all)
abstract class _Library extends Model {
Map<String, _Book> get collection;
}
@Serializable(serializers: Serializers.all)
abstract class _Bookmark extends Model {
@SerializableField(exclude: true)
final _Book book;
List<int> get history;
@SerializableField(isNullable: false)
int get page;
String get comment;
_Bookmark(this.book);
}

View file

@ -0,0 +1,712 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of angel_serialize.test.models.book;
// **************************************************************************
// JsonModelGenerator
// **************************************************************************
@generatedSerializable
@pragma('hello')
@SerializableField(alias: 'omg')
class Book extends _Book {
Book(
{this.id,
this.createdAt,
this.updatedAt,
this.author,
this.title,
this.description,
this.pageCount,
List<double> notModels,
this.camelCaseString})
: this.notModels = List.unmodifiable(notModels ?? []);
/// A unique identifier corresponding to this item.
@override
String id;
/// The time at which this item was created.
@override
DateTime createdAt;
/// The last time at which this item was updated.
@override
DateTime updatedAt;
@override
String author;
@override
String title;
@override
String description;
/// The number of pages the book has.
@override
int pageCount;
@override
List<double> notModels;
@override
String camelCaseString;
Book copyWith(
{String id,
DateTime createdAt,
DateTime updatedAt,
String author,
String title,
String description,
int pageCount,
List<double> notModels,
String camelCaseString}) {
return Book(
id: id ?? this.id,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
author: author ?? this.author,
title: title ?? this.title,
description: description ?? this.description,
pageCount: pageCount ?? this.pageCount,
notModels: notModels ?? this.notModels,
camelCaseString: camelCaseString ?? this.camelCaseString);
}
bool operator ==(other) {
return other is _Book &&
other.id == id &&
other.createdAt == createdAt &&
other.updatedAt == updatedAt &&
other.author == author &&
other.title == title &&
other.description == description &&
other.pageCount == pageCount &&
ListEquality<double>(DefaultEquality<double>())
.equals(other.notModels, notModels) &&
other.camelCaseString == camelCaseString;
}
@override
int get hashCode {
return hashObjects([
id,
createdAt,
updatedAt,
author,
title,
description,
pageCount,
notModels,
camelCaseString
]);
}
@override
String toString() {
return "Book(id=$id, createdAt=$createdAt, updatedAt=$updatedAt, author=$author, title=$title, description=$description, pageCount=$pageCount, notModels=$notModels, camelCaseString=$camelCaseString)";
}
Map<String, dynamic> toJson() {
return BookSerializer.toMap(this);
}
}
@generatedSerializable
class Author extends _Author {
Author(
{this.id,
this.createdAt,
this.updatedAt,
@required this.name,
@required this.age,
List<_Book> books,
this.newestBook,
this.secret,
this.obscured})
: this.books = List.unmodifiable(books ?? []);
/// A unique identifier corresponding to this item.
@override
String id;
/// The time at which this item was created.
@override
DateTime createdAt;
/// The last time at which this item was updated.
@override
DateTime updatedAt;
@override
final String name;
@override
final int age;
@override
final List<_Book> books;
/// The newest book.
@override
final _Book newestBook;
@override
final String secret;
@override
final String obscured;
Author copyWith(
{String id,
DateTime createdAt,
DateTime updatedAt,
String name,
int age,
List<_Book> books,
_Book newestBook,
String secret,
String obscured}) {
return Author(
id: id ?? this.id,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
name: name ?? this.name,
age: age ?? this.age,
books: books ?? this.books,
newestBook: newestBook ?? this.newestBook,
secret: secret ?? this.secret,
obscured: obscured ?? this.obscured);
}
bool operator ==(other) {
return other is _Author &&
other.id == id &&
other.createdAt == createdAt &&
other.updatedAt == updatedAt &&
other.name == name &&
other.age == age &&
ListEquality<_Book>(DefaultEquality<_Book>())
.equals(other.books, books) &&
other.newestBook == newestBook &&
other.secret == secret &&
other.obscured == obscured;
}
@override
int get hashCode {
return hashObjects([
id,
createdAt,
updatedAt,
name,
age,
books,
newestBook,
secret,
obscured
]);
}
@override
String toString() {
return "Author(id=$id, createdAt=$createdAt, updatedAt=$updatedAt, name=$name, age=$age, books=$books, newestBook=$newestBook, secret=$secret, obscured=$obscured)";
}
Map<String, dynamic> toJson() {
return AuthorSerializer.toMap(this);
}
}
@generatedSerializable
class Library extends _Library {
Library(
{this.id, this.createdAt, this.updatedAt, Map<String, _Book> collection})
: this.collection = Map.unmodifiable(collection ?? {});
/// A unique identifier corresponding to this item.
@override
String id;
/// The time at which this item was created.
@override
DateTime createdAt;
/// The last time at which this item was updated.
@override
DateTime updatedAt;
@override
final Map<String, _Book> collection;
Library copyWith(
{String id,
DateTime createdAt,
DateTime updatedAt,
Map<String, _Book> collection}) {
return Library(
id: id ?? this.id,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
collection: collection ?? this.collection);
}
bool operator ==(other) {
return other is _Library &&
other.id == id &&
other.createdAt == createdAt &&
other.updatedAt == updatedAt &&
MapEquality<String, _Book>(
keys: DefaultEquality<String>(),
values: DefaultEquality<_Book>())
.equals(other.collection, collection);
}
@override
int get hashCode {
return hashObjects([id, createdAt, updatedAt, collection]);
}
@override
String toString() {
return "Library(id=$id, createdAt=$createdAt, updatedAt=$updatedAt, collection=$collection)";
}
Map<String, dynamic> toJson() {
return LibrarySerializer.toMap(this);
}
}
@generatedSerializable
class Bookmark extends _Bookmark {
Bookmark(_Book book,
{this.id,
this.createdAt,
this.updatedAt,
List<int> history,
@required this.page,
this.comment})
: this.history = List.unmodifiable(history ?? []),
super(book);
/// A unique identifier corresponding to this item.
@override
String id;
/// The time at which this item was created.
@override
DateTime createdAt;
/// The last time at which this item was updated.
@override
DateTime updatedAt;
@override
final List<int> history;
@override
final int page;
@override
final String comment;
Bookmark copyWith(_Book book,
{String id,
DateTime createdAt,
DateTime updatedAt,
List<int> history,
int page,
String comment}) {
return Bookmark(book,
id: id ?? this.id,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
history: history ?? this.history,
page: page ?? this.page,
comment: comment ?? this.comment);
}
bool operator ==(other) {
return other is _Bookmark &&
other.id == id &&
other.createdAt == createdAt &&
other.updatedAt == updatedAt &&
ListEquality<int>(DefaultEquality<int>())
.equals(other.history, history) &&
other.page == page &&
other.comment == comment;
}
@override
int get hashCode {
return hashObjects([id, createdAt, updatedAt, history, page, comment]);
}
@override
String toString() {
return "Bookmark(id=$id, createdAt=$createdAt, updatedAt=$updatedAt, history=$history, page=$page, comment=$comment)";
}
Map<String, dynamic> toJson() {
return BookmarkSerializer.toMap(this);
}
}
// **************************************************************************
// SerializerGenerator
// **************************************************************************
const BookSerializer bookSerializer = BookSerializer();
class BookEncoder extends Converter<Book, Map> {
const BookEncoder();
@override
Map convert(Book model) => BookSerializer.toMap(model);
}
class BookDecoder extends Converter<Map, Book> {
const BookDecoder();
@override
Book convert(Map map) => BookSerializer.fromMap(map);
}
class BookSerializer extends Codec<Book, Map> {
const BookSerializer();
@override
get encoder => const BookEncoder();
@override
get decoder => const BookDecoder();
static Book fromMap(Map map) {
return Book(
id: map['id'] as String,
createdAt: map['created_at'] != null
? (map['created_at'] is DateTime
? (map['created_at'] as DateTime)
: DateTime.parse(map['created_at'].toString()))
: null,
updatedAt: map['updated_at'] != null
? (map['updated_at'] is DateTime
? (map['updated_at'] as DateTime)
: DateTime.parse(map['updated_at'].toString()))
: null,
author: map['author'] as String,
title: map['title'] as String,
description: map['description'] as String,
pageCount: map['page_count'] as int,
notModels: map['not_models'] is Iterable
? (map['not_models'] as Iterable).cast<double>().toList()
: null,
camelCaseString: map['camelCase'] as String);
}
static Map<String, dynamic> toMap(_Book model) {
if (model == null) {
return null;
}
return {
'id': model.id,
'created_at': model.createdAt?.toIso8601String(),
'updated_at': model.updatedAt?.toIso8601String(),
'author': model.author,
'title': model.title,
'description': model.description,
'page_count': model.pageCount,
'not_models': model.notModels,
'camelCase': model.camelCaseString
};
}
}
abstract class BookFields {
static const List<String> allFields = <String>[
id,
createdAt,
updatedAt,
author,
title,
description,
pageCount,
notModels,
camelCaseString
];
static const String id = 'id';
static const String createdAt = 'created_at';
static const String updatedAt = 'updated_at';
static const String author = 'author';
static const String title = 'title';
static const String description = 'description';
static const String pageCount = 'page_count';
static const String notModels = 'not_models';
static const String camelCaseString = 'camelCase';
}
const AuthorSerializer authorSerializer = AuthorSerializer();
class AuthorEncoder extends Converter<Author, Map> {
const AuthorEncoder();
@override
Map convert(Author model) => AuthorSerializer.toMap(model);
}
class AuthorDecoder extends Converter<Map, Author> {
const AuthorDecoder();
@override
Author convert(Map map) => AuthorSerializer.fromMap(map);
}
class AuthorSerializer extends Codec<Author, Map> {
const AuthorSerializer();
@override
get encoder => const AuthorEncoder();
@override
get decoder => const AuthorDecoder();
static Author fromMap(Map map) {
if (map['name'] == null) {
throw FormatException("Missing required field 'name' on Author.");
}
if (map['age'] == null) {
throw FormatException("Custom message for missing `age`");
}
return Author(
id: map['id'] as String,
createdAt: map['created_at'] != null
? (map['created_at'] is DateTime
? (map['created_at'] as DateTime)
: DateTime.parse(map['created_at'].toString()))
: null,
updatedAt: map['updated_at'] != null
? (map['updated_at'] is DateTime
? (map['updated_at'] as DateTime)
: DateTime.parse(map['updated_at'].toString()))
: null,
name: map['name'] as String,
age: map['age'] as int,
books: map['books'] is Iterable
? List.unmodifiable(((map['books'] as Iterable).whereType<Map>())
.map(BookSerializer.fromMap))
: null,
newestBook: map['newest_book'] != null
? BookSerializer.fromMap(map['newest_book'] as Map)
: null,
obscured: map['obscured'] as String);
}
static Map<String, dynamic> toMap(_Author model) {
if (model == null) {
return null;
}
if (model.name == null) {
throw FormatException("Missing required field 'name' on Author.");
}
if (model.age == null) {
throw FormatException("Custom message for missing `age`");
}
return {
'id': model.id,
'created_at': model.createdAt?.toIso8601String(),
'updated_at': model.updatedAt?.toIso8601String(),
'name': model.name,
'age': model.age,
'books': model.books?.map((m) => BookSerializer.toMap(m))?.toList(),
'newest_book': BookSerializer.toMap(model.newestBook)
};
}
}
abstract class AuthorFields {
static const List<String> allFields = <String>[
id,
createdAt,
updatedAt,
name,
age,
books,
newestBook,
secret,
obscured
];
static const String id = 'id';
static const String createdAt = 'created_at';
static const String updatedAt = 'updated_at';
static const String name = 'name';
static const String age = 'age';
static const String books = 'books';
static const String newestBook = 'newest_book';
static const String secret = 'secret';
static const String obscured = 'obscured';
}
const LibrarySerializer librarySerializer = LibrarySerializer();
class LibraryEncoder extends Converter<Library, Map> {
const LibraryEncoder();
@override
Map convert(Library model) => LibrarySerializer.toMap(model);
}
class LibraryDecoder extends Converter<Map, Library> {
const LibraryDecoder();
@override
Library convert(Map map) => LibrarySerializer.fromMap(map);
}
class LibrarySerializer extends Codec<Library, Map> {
const LibrarySerializer();
@override
get encoder => const LibraryEncoder();
@override
get decoder => const LibraryDecoder();
static Library fromMap(Map map) {
return Library(
id: map['id'] as String,
createdAt: map['created_at'] != null
? (map['created_at'] is DateTime
? (map['created_at'] as DateTime)
: DateTime.parse(map['created_at'].toString()))
: null,
updatedAt: map['updated_at'] != null
? (map['updated_at'] is DateTime
? (map['updated_at'] as DateTime)
: DateTime.parse(map['updated_at'].toString()))
: null,
collection: map['collection'] is Map
? Map.unmodifiable(
(map['collection'] as Map).keys.fold({}, (out, key) {
return out
..[key] = BookSerializer.fromMap(
((map['collection'] as Map)[key]) as Map);
}))
: null);
}
static Map<String, dynamic> toMap(_Library model) {
if (model == null) {
return null;
}
return {
'id': model.id,
'created_at': model.createdAt?.toIso8601String(),
'updated_at': model.updatedAt?.toIso8601String(),
'collection': model.collection.keys?.fold({}, (map, key) {
return map..[key] = BookSerializer.toMap(model.collection[key]);
})
};
}
}
abstract class LibraryFields {
static const List<String> allFields = <String>[
id,
createdAt,
updatedAt,
collection
];
static const String id = 'id';
static const String createdAt = 'created_at';
static const String updatedAt = 'updated_at';
static const String collection = 'collection';
}
abstract class BookmarkSerializer {
static Bookmark fromMap(Map map, _Book book) {
if (map['page'] == null) {
throw FormatException("Missing required field 'page' on Bookmark.");
}
return Bookmark(book,
id: map['id'] as String,
createdAt: map['created_at'] != null
? (map['created_at'] is DateTime
? (map['created_at'] as DateTime)
: DateTime.parse(map['created_at'].toString()))
: null,
updatedAt: map['updated_at'] != null
? (map['updated_at'] is DateTime
? (map['updated_at'] as DateTime)
: DateTime.parse(map['updated_at'].toString()))
: null,
history: map['history'] is Iterable
? (map['history'] as Iterable).cast<int>().toList()
: null,
page: map['page'] as int,
comment: map['comment'] as String);
}
static Map<String, dynamic> toMap(_Bookmark model) {
if (model == null) {
return null;
}
if (model.page == null) {
throw FormatException("Missing required field 'page' on Bookmark.");
}
return {
'id': model.id,
'created_at': model.createdAt?.toIso8601String(),
'updated_at': model.updatedAt?.toIso8601String(),
'history': model.history,
'page': model.page,
'comment': model.comment
};
}
}
abstract class BookmarkFields {
static const List<String> allFields = <String>[
id,
createdAt,
updatedAt,
history,
page,
comment
];
static const String id = 'id';
static const String createdAt = 'created_at';
static const String updatedAt = 'updated_at';
static const String history = 'history';
static const String page = 'page';
static const String comment = 'comment';
}

View file

@ -0,0 +1,18 @@
import 'package:angel_serialize/angel_serialize.dart';
part 'game_pad_button.g.dart';
@serializable
abstract class _GamepadButton {
String get name;
int get radius;
}
@serializable
class _Gamepad {
List<_GamepadButton> buttons;
Map<String, dynamic> dynamicMap;
// ignore: unused_field
String _somethingPrivate;
}

View file

@ -0,0 +1,189 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'game_pad_button.dart';
// **************************************************************************
// JsonModelGenerator
// **************************************************************************
@generatedSerializable
class GamepadButton implements _GamepadButton {
const GamepadButton({this.name, this.radius});
@override
final String name;
@override
final int radius;
GamepadButton copyWith({String name, int radius}) {
return GamepadButton(
name: name ?? this.name, radius: radius ?? this.radius);
}
bool operator ==(other) {
return other is _GamepadButton &&
other.name == name &&
other.radius == radius;
}
@override
int get hashCode {
return hashObjects([name, radius]);
}
@override
String toString() {
return "GamepadButton(name=$name, radius=$radius)";
}
Map<String, dynamic> toJson() {
return GamepadButtonSerializer.toMap(this);
}
}
@generatedSerializable
class Gamepad extends _Gamepad {
Gamepad({List<_GamepadButton> buttons, Map<String, dynamic> dynamicMap})
: this.buttons = List.unmodifiable(buttons ?? []),
this.dynamicMap = Map.unmodifiable(dynamicMap ?? {});
@override
List<_GamepadButton> buttons;
@override
Map<String, dynamic> dynamicMap;
Gamepad copyWith(
{List<_GamepadButton> buttons, Map<String, dynamic> dynamicMap}) {
return Gamepad(
buttons: buttons ?? this.buttons,
dynamicMap: dynamicMap ?? this.dynamicMap);
}
bool operator ==(other) {
return other is _Gamepad &&
ListEquality<_GamepadButton>(DefaultEquality<_GamepadButton>())
.equals(other.buttons, buttons) &&
MapEquality<String, dynamic>(
keys: DefaultEquality<String>(), values: DefaultEquality())
.equals(other.dynamicMap, dynamicMap);
}
@override
int get hashCode {
return hashObjects([buttons, dynamicMap]);
}
@override
String toString() {
return "Gamepad(buttons=$buttons, dynamicMap=$dynamicMap)";
}
Map<String, dynamic> toJson() {
return GamepadSerializer.toMap(this);
}
}
// **************************************************************************
// SerializerGenerator
// **************************************************************************
const GamepadButtonSerializer gamepadButtonSerializer =
GamepadButtonSerializer();
class GamepadButtonEncoder extends Converter<GamepadButton, Map> {
const GamepadButtonEncoder();
@override
Map convert(GamepadButton model) => GamepadButtonSerializer.toMap(model);
}
class GamepadButtonDecoder extends Converter<Map, GamepadButton> {
const GamepadButtonDecoder();
@override
GamepadButton convert(Map map) => GamepadButtonSerializer.fromMap(map);
}
class GamepadButtonSerializer extends Codec<GamepadButton, Map> {
const GamepadButtonSerializer();
@override
get encoder => const GamepadButtonEncoder();
@override
get decoder => const GamepadButtonDecoder();
static GamepadButton fromMap(Map map) {
return GamepadButton(
name: map['name'] as String, radius: map['radius'] as int);
}
static Map<String, dynamic> toMap(_GamepadButton model) {
if (model == null) {
return null;
}
return {'name': model.name, 'radius': model.radius};
}
}
abstract class GamepadButtonFields {
static const List<String> allFields = <String>[name, radius];
static const String name = 'name';
static const String radius = 'radius';
}
const GamepadSerializer gamepadSerializer = GamepadSerializer();
class GamepadEncoder extends Converter<Gamepad, Map> {
const GamepadEncoder();
@override
Map convert(Gamepad model) => GamepadSerializer.toMap(model);
}
class GamepadDecoder extends Converter<Map, Gamepad> {
const GamepadDecoder();
@override
Gamepad convert(Map map) => GamepadSerializer.fromMap(map);
}
class GamepadSerializer extends Codec<Gamepad, Map> {
const GamepadSerializer();
@override
get encoder => const GamepadEncoder();
@override
get decoder => const GamepadDecoder();
static Gamepad fromMap(Map map) {
return Gamepad(
buttons: map['buttons'] is Iterable
? List.unmodifiable(((map['buttons'] as Iterable).whereType<Map>())
.map(GamepadButtonSerializer.fromMap))
: null,
dynamicMap: map['dynamic_map'] is Map
? (map['dynamic_map'] as Map).cast<String, dynamic>()
: null);
}
static Map<String, dynamic> toMap(_Gamepad model) {
if (model == null) {
return null;
}
return {
'buttons':
model.buttons?.map((m) => GamepadButtonSerializer.toMap(m))?.toList(),
'dynamic_map': model.dynamicMap
};
}
}
abstract class GamepadFields {
static const List<String> allFields = <String>[buttons, dynamicMap];
static const String buttons = 'buttons';
static const String dynamicMap = 'dynamic_map';
}

View file

@ -0,0 +1,12 @@
import 'package:angel_serialize/angel_serialize.dart';
import 'package:collection/collection.dart';
part 'goat.g.dart';
@serializable
abstract class _Goat {
@SerializableField(defaultValue: 34)
int get integer;
@SerializableField(defaultValue: [34, 35])
List<int> get list;
}

View file

@ -0,0 +1,93 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'goat.dart';
// **************************************************************************
// JsonModelGenerator
// **************************************************************************
@generatedSerializable
class Goat implements _Goat {
const Goat({this.integer = 34, this.list = const [34, 35]});
@override
final int integer;
@override
final List<int> list;
Goat copyWith({int integer, List<int> list}) {
return Goat(integer: integer ?? this.integer, list: list ?? this.list);
}
bool operator ==(other) {
return other is _Goat &&
other.integer == integer &&
ListEquality<int>(DefaultEquality<int>()).equals(other.list, list);
}
@override
int get hashCode {
return hashObjects([integer, list]);
}
@override
String toString() {
return "Goat(integer=$integer, list=$list)";
}
Map<String, dynamic> toJson() {
return GoatSerializer.toMap(this);
}
}
// **************************************************************************
// SerializerGenerator
// **************************************************************************
const GoatSerializer goatSerializer = GoatSerializer();
class GoatEncoder extends Converter<Goat, Map> {
const GoatEncoder();
@override
Map convert(Goat model) => GoatSerializer.toMap(model);
}
class GoatDecoder extends Converter<Map, Goat> {
const GoatDecoder();
@override
Goat convert(Map map) => GoatSerializer.fromMap(map);
}
class GoatSerializer extends Codec<Goat, Map> {
const GoatSerializer();
@override
get encoder => const GoatEncoder();
@override
get decoder => const GoatDecoder();
static Goat fromMap(Map map) {
return Goat(
integer: map['integer'] as int ?? 34,
list: map['list'] is Iterable
? (map['list'] as Iterable).cast<int>().toList()
: const [34, 35]);
}
static Map<String, dynamic> toMap(_Goat model) {
if (model == null) {
return null;
}
return {'integer': model.integer, 'list': model.list};
}
}
abstract class GoatFields {
static const List<String> allFields = <String>[integer, list];
static const String integer = 'integer';
static const String list = 'list';
}

View file

@ -0,0 +1,19 @@
import 'dart:convert';
import 'package:angel_serialize/angel_serialize.dart';
import 'package:collection/collection.dart';
import 'package:meta/meta.dart';
part 'has_map.g.dart';
Map _fromString(v) => json.decode(v.toString()) as Map;
String _toString(Map v) => json.encode(v);
@serializable
abstract class _HasMap {
@SerializableField(
serializer: #_toString,
deserializer: #_fromString,
isNullable: false,
serializesTo: String)
Map get value;
}

View file

@ -0,0 +1,93 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'has_map.dart';
// **************************************************************************
// JsonModelGenerator
// **************************************************************************
@generatedSerializable
class HasMap implements _HasMap {
const HasMap({@required this.value});
@override
final Map<dynamic, dynamic> value;
HasMap copyWith({Map<dynamic, dynamic> value}) {
return HasMap(value: value ?? this.value);
}
bool operator ==(other) {
return other is _HasMap &&
MapEquality<dynamic, dynamic>(
keys: DefaultEquality(), values: DefaultEquality())
.equals(other.value, value);
}
@override
int get hashCode {
return hashObjects([value]);
}
@override
String toString() {
return "HasMap(value=$value)";
}
Map<String, dynamic> toJson() {
return HasMapSerializer.toMap(this);
}
}
// **************************************************************************
// SerializerGenerator
// **************************************************************************
const HasMapSerializer hasMapSerializer = HasMapSerializer();
class HasMapEncoder extends Converter<HasMap, Map> {
const HasMapEncoder();
@override
Map convert(HasMap model) => HasMapSerializer.toMap(model);
}
class HasMapDecoder extends Converter<Map, HasMap> {
const HasMapDecoder();
@override
HasMap convert(Map map) => HasMapSerializer.fromMap(map);
}
class HasMapSerializer extends Codec<HasMap, Map> {
const HasMapSerializer();
@override
get encoder => const HasMapEncoder();
@override
get decoder => const HasMapDecoder();
static HasMap fromMap(Map map) {
if (map['value'] == null) {
throw FormatException("Missing required field 'value' on HasMap.");
}
return HasMap(value: _fromString(map['value']));
}
static Map<String, dynamic> toMap(_HasMap model) {
if (model == null) {
return null;
}
if (model.value == null) {
throw FormatException("Missing required field 'value' on HasMap.");
}
return {'value': _toString(model.value)};
}
}
abstract class HasMapFields {
static const List<String> allFields = <String>[value];
static const String value = 'value';
}

View file

@ -0,0 +1,22 @@
import 'package:angel_serialize/angel_serialize.dart';
part 'subclass.g.dart';
@serializable
class _Animal {
@notNull
String genus;
@notNull
String species;
}
@serializable
class _Bird extends _Animal {
@DefaultsTo(false)
bool isSparrow;
}
var saxaulSparrow = Bird(
genus: 'Passer',
species: 'ammodendri',
isSparrow: true,
);

View file

@ -0,0 +1,214 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'subclass.dart';
// **************************************************************************
// JsonModelGenerator
// **************************************************************************
@generatedSerializable
class Animal extends _Animal {
Animal({@required this.genus, @required this.species});
@override
String genus;
@override
String species;
Animal copyWith({String genus, String species}) {
return Animal(genus: genus ?? this.genus, species: species ?? this.species);
}
bool operator ==(other) {
return other is _Animal && other.genus == genus && other.species == species;
}
@override
int get hashCode {
return hashObjects([genus, species]);
}
@override
String toString() {
return "Animal(genus=$genus, species=$species)";
}
Map<String, dynamic> toJson() {
return AnimalSerializer.toMap(this);
}
}
@generatedSerializable
class Bird extends _Bird {
Bird({@required this.genus, @required this.species, this.isSparrow = false});
@override
String genus;
@override
String species;
@override
bool isSparrow;
Bird copyWith({String genus, String species, bool isSparrow}) {
return Bird(
genus: genus ?? this.genus,
species: species ?? this.species,
isSparrow: isSparrow ?? this.isSparrow);
}
bool operator ==(other) {
return other is _Bird &&
other.genus == genus &&
other.species == species &&
other.isSparrow == isSparrow;
}
@override
int get hashCode {
return hashObjects([genus, species, isSparrow]);
}
@override
String toString() {
return "Bird(genus=$genus, species=$species, isSparrow=$isSparrow)";
}
Map<String, dynamic> toJson() {
return BirdSerializer.toMap(this);
}
}
// **************************************************************************
// SerializerGenerator
// **************************************************************************
const AnimalSerializer animalSerializer = AnimalSerializer();
class AnimalEncoder extends Converter<Animal, Map> {
const AnimalEncoder();
@override
Map convert(Animal model) => AnimalSerializer.toMap(model);
}
class AnimalDecoder extends Converter<Map, Animal> {
const AnimalDecoder();
@override
Animal convert(Map map) => AnimalSerializer.fromMap(map);
}
class AnimalSerializer extends Codec<Animal, Map> {
const AnimalSerializer();
@override
get encoder => const AnimalEncoder();
@override
get decoder => const AnimalDecoder();
static Animal fromMap(Map map) {
if (map['genus'] == null) {
throw FormatException("Missing required field 'genus' on Animal.");
}
if (map['species'] == null) {
throw FormatException("Missing required field 'species' on Animal.");
}
return Animal(
genus: map['genus'] as String, species: map['species'] as String);
}
static Map<String, dynamic> toMap(_Animal model) {
if (model == null) {
return null;
}
if (model.genus == null) {
throw FormatException("Missing required field 'genus' on Animal.");
}
if (model.species == null) {
throw FormatException("Missing required field 'species' on Animal.");
}
return {'genus': model.genus, 'species': model.species};
}
}
abstract class AnimalFields {
static const List<String> allFields = <String>[genus, species];
static const String genus = 'genus';
static const String species = 'species';
}
const BirdSerializer birdSerializer = BirdSerializer();
class BirdEncoder extends Converter<Bird, Map> {
const BirdEncoder();
@override
Map convert(Bird model) => BirdSerializer.toMap(model);
}
class BirdDecoder extends Converter<Map, Bird> {
const BirdDecoder();
@override
Bird convert(Map map) => BirdSerializer.fromMap(map);
}
class BirdSerializer extends Codec<Bird, Map> {
const BirdSerializer();
@override
get encoder => const BirdEncoder();
@override
get decoder => const BirdDecoder();
static Bird fromMap(Map map) {
if (map['genus'] == null) {
throw FormatException("Missing required field 'genus' on Bird.");
}
if (map['species'] == null) {
throw FormatException("Missing required field 'species' on Bird.");
}
return Bird(
genus: map['genus'] as String,
species: map['species'] as String,
isSparrow: map['is_sparrow'] as bool ?? false);
}
static Map<String, dynamic> toMap(_Bird model) {
if (model == null) {
return null;
}
if (model.genus == null) {
throw FormatException("Missing required field 'genus' on Bird.");
}
if (model.species == null) {
throw FormatException("Missing required field 'species' on Bird.");
}
return {
'genus': model.genus,
'species': model.species,
'is_sparrow': model.isSparrow
};
}
}
abstract class BirdFields {
static const List<String> allFields = <String>[genus, species, isSparrow];
static const String genus = 'genus';
static const String species = 'species';
static const String isSparrow = 'is_sparrow';
}

View file

@ -0,0 +1,17 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:angel_serialize/angel_serialize.dart';
import 'package:collection/collection.dart';
part 'with_enum.g.dart';
@serializable
abstract class _WithEnum {
@DefaultsTo(WithEnumType.b)
WithEnumType get type;
List<int> get finalList;
Uint8List get imageBytes;
}
enum WithEnumType { a, b, c }

View file

@ -0,0 +1,123 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'with_enum.dart';
// **************************************************************************
// JsonModelGenerator
// **************************************************************************
@generatedSerializable
class WithEnum implements _WithEnum {
const WithEnum({this.type = WithEnumType.b, this.finalList, this.imageBytes});
@override
final WithEnumType type;
@override
final List<int> finalList;
@override
final Uint8List imageBytes;
WithEnum copyWith(
{WithEnumType type, List<int> finalList, Uint8List imageBytes}) {
return WithEnum(
type: type ?? this.type,
finalList: finalList ?? this.finalList,
imageBytes: imageBytes ?? this.imageBytes);
}
bool operator ==(other) {
return other is _WithEnum &&
other.type == type &&
ListEquality<int>(DefaultEquality<int>())
.equals(other.finalList, finalList) &&
ListEquality().equals(other.imageBytes, imageBytes);
}
@override
int get hashCode {
return hashObjects([type, finalList, imageBytes]);
}
@override
String toString() {
return "WithEnum(type=$type, finalList=$finalList, imageBytes=$imageBytes)";
}
Map<String, dynamic> toJson() {
return WithEnumSerializer.toMap(this);
}
}
// **************************************************************************
// SerializerGenerator
// **************************************************************************
const WithEnumSerializer withEnumSerializer = WithEnumSerializer();
class WithEnumEncoder extends Converter<WithEnum, Map> {
const WithEnumEncoder();
@override
Map convert(WithEnum model) => WithEnumSerializer.toMap(model);
}
class WithEnumDecoder extends Converter<Map, WithEnum> {
const WithEnumDecoder();
@override
WithEnum convert(Map map) => WithEnumSerializer.fromMap(map);
}
class WithEnumSerializer extends Codec<WithEnum, Map> {
const WithEnumSerializer();
@override
get encoder => const WithEnumEncoder();
@override
get decoder => const WithEnumDecoder();
static WithEnum fromMap(Map map) {
return WithEnum(
type: map['type'] is WithEnumType
? (map['type'] as WithEnumType)
: (map['type'] is int
? WithEnumType.values[map['type'] as int]
: WithEnumType.b),
finalList: map['final_list'] is Iterable
? (map['final_list'] as Iterable).cast<int>().toList()
: null,
imageBytes: map['image_bytes'] is Uint8List
? (map['image_bytes'] as Uint8List)
: (map['image_bytes'] is Iterable<int>
? Uint8List.fromList(
(map['image_bytes'] as Iterable<int>).toList())
: (map['image_bytes'] is String
? Uint8List.fromList(
base64.decode(map['image_bytes'] as String))
: null)));
}
static Map<String, dynamic> toMap(_WithEnum model) {
if (model == null) {
return null;
}
return {
'type':
model.type == null ? null : WithEnumType.values.indexOf(model.type),
'final_list': model.finalList,
'image_bytes':
model.imageBytes == null ? null : base64.encode(model.imageBytes)
};
}
}
abstract class WithEnumFields {
static const List<String> allFields = <String>[type, finalList, imageBytes];
static const String type = 'type';
static const String finalList = 'final_list';
static const String imageBytes = 'image_bytes';
}

View file

@ -0,0 +1,18 @@
import 'dart:convert';
import 'package:test/test.dart';
import 'models/has_map.dart';
void main() {
var m = HasMap(value: {'foo': 'bar'});
print(json.encode(m));
test('json', () {
expect(json.encode(m), r'{"value":"{\"foo\":\"bar\"}"}');
});
test('decode', () {
var mm = json.decode(r'{"value":"{\"foo\":\"bar\"}"}') as Map;
var mmm = HasMapSerializer.fromMap(mm);
expect(mmm, m);
});
}

View file

@ -0,0 +1,4 @@
cd angel_serialize_generator
pub get
pub run build_runner build --delete-conflicting-outputs
pub run test