├── angel_serialize ├── mono_pkg.yaml ├── analysis_options.yaml ├── README.md ├── example │ └── main.dart ├── pubspec.yaml ├── CHANGELOG.md ├── LICENSE ├── .gitignore └── lib │ └── angel_serialize.dart ├── angel_serialize_generator ├── mono_pkg.yaml ├── README.md ├── analysis_options.yaml ├── example │ ├── main.dart │ └── main.g.dart ├── test │ ├── models │ │ ├── goat.dart │ │ ├── game_pad_button.dart │ │ ├── with_enum.dart │ │ ├── subclass.dart │ │ ├── has_map.dart │ │ ├── book.d.ts │ │ ├── book.dart │ │ ├── goat.g.dart │ │ ├── has_map.g.dart │ │ ├── with_enum.g.dart │ │ ├── game_pad_button.g.dart │ │ ├── subclass.g.dart │ │ └── book.g.dart │ ├── serializer_test.dart │ ├── default_value_test.dart │ ├── enum_test.dart │ └── book_test.dart ├── pubspec.yaml ├── LICENSE ├── build.yaml ├── .gitignore ├── lib │ ├── context.dart │ ├── angel_serialize_generator.dart │ ├── typescript.dart │ ├── build_context.dart │ ├── model.dart │ └── serialize.dart └── CHANGELOG.md ├── .travis.yml ├── tool └── .travis.sh ├── .idea ├── vcs.xml ├── modules.xml ├── runConfigurations │ ├── tests_in_enum_test_dart.xml │ └── tests_in_angel_serialize_generator.xml └── serialize.iml ├── .gitignore └── README.md /angel_serialize/mono_pkg.yaml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /angel_serialize_generator/mono_pkg.yaml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: dart 2 | script: bash tool/.travis.sh 3 | dart: 4 | - stable -------------------------------------------------------------------------------- /tool/.travis.sh: -------------------------------------------------------------------------------- 1 | cd angel_serialize_generator 2 | pub get 3 | pub run build_runner build --delete-conflicting-outputs 4 | pub run test -------------------------------------------------------------------------------- /angel_serialize/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:pedantic/analysis_options.yaml 2 | analyzer: 3 | strong-mode: 4 | implicit-casts: false -------------------------------------------------------------------------------- /angel_serialize/README.md: -------------------------------------------------------------------------------- 1 | # angel_serialize 2 | The frontend for Angel model serialization. 3 | See documentation in the main project repo: 4 | 5 | https://github.com/angel-dart/serialize 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /angel_serialize_generator/README.md: -------------------------------------------------------------------------------- 1 | # angel_serialize_generator 2 | The builder for Angel's model serialization. 3 | 4 | Find documentation in the main project repo: 5 | https://github.com/angel-dart/serialize -------------------------------------------------------------------------------- /angel_serialize/example/main.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: unused_element 2 | import 'package:angel_serialize/angel_serialize.dart'; 3 | 4 | @serializable 5 | class _Todo { 6 | String text; 7 | bool completed; 8 | } 9 | -------------------------------------------------------------------------------- /angel_serialize_generator/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:pedantic/analysis_options.yaml 2 | analyzer: 3 | strong-mode: 4 | implicit-casts: false 5 | linter: 6 | rules: 7 | - unnecessary_new 8 | - unnecessary_const -------------------------------------------------------------------------------- /angel_serialize_generator/example/main.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: unused_element 2 | import 'package:angel_serialize/angel_serialize.dart'; 3 | part 'main.g.dart'; 4 | 5 | @serializable 6 | class _Todo { 7 | String text; 8 | bool completed; 9 | } 10 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /angel_serialize_generator/test/models/goat.dart: -------------------------------------------------------------------------------- 1 | import 'package:angel_serialize/angel_serialize.dart'; 2 | import 'package:collection/collection.dart'; 3 | part 'goat.g.dart'; 4 | 5 | @serializable 6 | abstract class _Goat { 7 | @SerializableField(defaultValue: 34) 8 | int get integer; 9 | 10 | @SerializableField(defaultValue: [34, 35]) 11 | List get list; 12 | } 13 | -------------------------------------------------------------------------------- /angel_serialize_generator/test/models/game_pad_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:angel_serialize/angel_serialize.dart'; 2 | part 'game_pad_button.g.dart'; 3 | 4 | @serializable 5 | abstract class _GamepadButton { 6 | String get name; 7 | int get radius; 8 | } 9 | 10 | @serializable 11 | class _Gamepad { 12 | List<_GamepadButton> buttons; 13 | 14 | Map dynamicMap; 15 | 16 | // ignore: unused_field 17 | String _somethingPrivate; 18 | } 19 | -------------------------------------------------------------------------------- /.idea/runConfigurations/tests_in_enum_test_dart.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /angel_serialize_generator/test/models/with_enum.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:typed_data'; 3 | import 'package:angel_serialize/angel_serialize.dart'; 4 | import 'package:collection/collection.dart'; 5 | part 'with_enum.g.dart'; 6 | 7 | @serializable 8 | abstract class _WithEnum { 9 | @DefaultsTo(WithEnumType.b) 10 | WithEnumType get type; 11 | 12 | List get finalList; 13 | 14 | Uint8List get imageBytes; 15 | } 16 | 17 | enum WithEnumType { a, b, c } 18 | -------------------------------------------------------------------------------- /angel_serialize/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: angel_serialize 2 | version: 2.2.3+3 3 | description: Static annotations powering Angel model serialization. Combine with angel_serialize_generator for flexible modeling. 4 | author: Tobe O 5 | homepage: https://github.com/angel-dart/serialize 6 | environment: 7 | sdk: '>=2.0.0-dev <3.0.0' 8 | dependencies: 9 | angel_model: ^1.0.0 10 | collection: ^1.0.0 11 | meta: ^1.0.0 12 | pedantic: ^1.0.0 13 | quiver_hashcode: ^2.0.0 14 | -------------------------------------------------------------------------------- /angel_serialize_generator/test/models/subclass.dart: -------------------------------------------------------------------------------- 1 | import 'package:angel_serialize/angel_serialize.dart'; 2 | part 'subclass.g.dart'; 3 | 4 | @serializable 5 | class _Animal { 6 | @notNull 7 | String genus; 8 | @notNull 9 | String species; 10 | } 11 | 12 | @serializable 13 | class _Bird extends _Animal { 14 | @DefaultsTo(false) 15 | bool isSparrow; 16 | } 17 | 18 | var saxaulSparrow = Bird( 19 | genus: 'Passer', 20 | species: 'ammodendri', 21 | isSparrow: true, 22 | ); 23 | -------------------------------------------------------------------------------- /.idea/runConfigurations/tests_in_angel_serialize_generator.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /angel_serialize_generator/test/serializer_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'package:test/test.dart'; 3 | import 'models/has_map.dart'; 4 | 5 | void main() { 6 | var m = HasMap(value: {'foo': 'bar'}); 7 | print(json.encode(m)); 8 | 9 | test('json', () { 10 | expect(json.encode(m), r'{"value":"{\"foo\":\"bar\"}"}'); 11 | }); 12 | 13 | test('decode', () { 14 | var mm = json.decode(r'{"value":"{\"foo\":\"bar\"}"}') as Map; 15 | var mmm = HasMapSerializer.fromMap(mm); 16 | expect(mmm, m); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /angel_serialize_generator/test/models/has_map.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'package:angel_serialize/angel_serialize.dart'; 3 | import 'package:collection/collection.dart'; 4 | import 'package:meta/meta.dart'; 5 | part 'has_map.g.dart'; 6 | 7 | Map _fromString(v) => json.decode(v.toString()) as Map; 8 | 9 | String _toString(Map v) => json.encode(v); 10 | 11 | @serializable 12 | abstract class _HasMap { 13 | @SerializableField( 14 | serializer: #_toString, 15 | deserializer: #_fromString, 16 | isNullable: false, 17 | serializesTo: String) 18 | Map get value; 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | .idea/**/workspace.xml 3 | .idea/**/tasks.xml 4 | .idea/dictionaries 5 | .idea/**/dataSources/ 6 | .idea/**/dataSources.ids 7 | .idea/**/dataSources.xml 8 | .idea/**/dataSources.local.xml 9 | .idea/**/sqlDataSources.xml 10 | .idea/**/dynamic.xml 11 | .idea/**/uiDesigner.xml 12 | .idea/**/gradle.xml 13 | .idea/**/libraries 14 | .idea/**/mongoSettings.xml 15 | *.iws 16 | /out/ 17 | .idea_modules/ 18 | atlassian-ide-plugin.xml 19 | com_crashlytics_export_strings.xml 20 | crashlytics.properties 21 | crashlytics-build.properties 22 | fabric.properties 23 | .dart_tool -------------------------------------------------------------------------------- /angel_serialize_generator/test/default_value_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'models/goat.dart'; 3 | 4 | void main() { 5 | group('constructor', () { 6 | test('int default', () { 7 | expect(Goat().integer, 34); 8 | }); 9 | 10 | test('list default', () { 11 | expect(Goat().list, [34, 35]); 12 | }); 13 | }); 14 | 15 | group('from map', () { 16 | test('int default', () { 17 | expect(GoatSerializer.fromMap({}).integer, 34); 18 | }); 19 | 20 | test('list default', () { 21 | expect(GoatSerializer.fromMap({}).list, [34, 35]); 22 | }); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /angel_serialize_generator/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: angel_serialize_generator 2 | version: 2.5.0 3 | description: Model serialization generators, designed for use with Angel. Combine with angel_serialize for flexible modeling. 4 | author: Tobe O 5 | homepage: https://github.com/angel-dart/serialize 6 | environment: 7 | sdk: '>=2.0.0 <3.0.0' 8 | dependencies: 9 | analyzer: ">=0.27.1 <2.0.0" 10 | angel_model: ^1.0.0 11 | angel_serialize: ^2.2.0 12 | build: ">=0.12.0 <2.0.0" 13 | build_config: ">=0.3.0 <2.0.0" 14 | code_buffer: ^1.0.0 15 | code_builder: ^3.0.0 16 | meta: ^1.0.0 17 | path: ^1.0.0 18 | recase: ^2.0.0 19 | source_gen: ^0.9.0 20 | quiver: ^2.0.0 21 | dev_dependencies: 22 | build_runner: ^1.0.0 23 | collection: ^1.0.0 24 | pedantic: ^1.0.0 25 | test: ^1.0.0 26 | # dependency_overrides: 27 | # angel_serialize: 28 | # path: ../angel_serialize -------------------------------------------------------------------------------- /angel_serialize_generator/test/models/book.d.ts: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | declare module 'angel_serialize_generator' { 3 | interface Book { 4 | id?: string; 5 | created_at?: any; 6 | updated_at?: any; 7 | author?: string; 8 | title?: string; 9 | description?: string; 10 | page_count?: number; 11 | not_models?: number[]; 12 | camelCase?: string; 13 | } 14 | interface Author { 15 | id?: string; 16 | created_at?: any; 17 | updated_at?: any; 18 | name: string; 19 | age: number; 20 | books?: Book[]; 21 | newest_book?: Book; 22 | } 23 | interface Library { 24 | id?: string; 25 | created_at?: any; 26 | updated_at?: any; 27 | collection?: LibraryCollection; 28 | } 29 | interface LibraryCollection { 30 | [key: string]: Book; 31 | } 32 | interface Bookmark { 33 | id?: string; 34 | created_at?: any; 35 | updated_at?: any; 36 | history?: number[]; 37 | page: number; 38 | comment?: string; 39 | } 40 | } -------------------------------------------------------------------------------- /angel_serialize/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 2.2.3+3 2 | * Add `exclude: true` to `super` call in `Exclude` constructor. 3 | 4 | # 2.2.3+2 5 | * Apply `package:pedantic`. 6 | 7 | # 2.2.3+1 8 | * Export `json`, `Codec`, and `Converter` from `dart:convert`. 9 | 10 | # 2.2.3 11 | * `isNullable` defaults to `true`, and will not change. 12 | * Deprecate `@nullable`. 13 | * Add `@notNull`. 14 | 15 | # 2.2.2+1 16 | * Export commonly-used packages, for the sake of convenience. 17 | 18 | # 2.2.2 19 | * Add `HasAlias`, `DefaultsTo`, `nullable` idioms. 20 | * `isNullable` defaults to `false` now. 21 | 22 | # 2.2.1 23 | * Add `serializesTo`. 24 | 25 | # 2.2.0 26 | * Add `@SerializableField`. 27 | 28 | # 2.1.0 29 | * Export `hashObjects`. 30 | 31 | # 2.0.4+1 32 | * Allow Dart 1 for this annotation-only package. 33 | 34 | # 2.0.4 35 | * Added `generatedSerializable`. 36 | 37 | # 2.0.3 38 | * Increased the upper SDK boundary. 39 | 40 | # 2.0.2 41 | * Added `DefaultValue`. 42 | 43 | # 2.0.1 44 | * Added `Serializers.typescript`. 45 | 46 | # 2.0.0 47 | * Dart 2+ constraint 48 | -------------------------------------------------------------------------------- /.idea/serialize.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /angel_serialize/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 The Angel Framework 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /angel_serialize_generator/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 The Angel Framework 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /angel_serialize_generator/build.yaml: -------------------------------------------------------------------------------- 1 | builders: 2 | angel_serialize: 3 | import: "package:angel_serialize_generator/angel_serialize_generator.dart" 4 | builder_factories: 5 | - jsonModelBuilder 6 | - serializerBuilder 7 | auto_apply: root_package 8 | build_to: cache 9 | build_extensions: 10 | .dart: 11 | - ".angel_serialize.g.part" 12 | - ".angel_serialize_serializer.g.part" 13 | applies_builders: ["source_gen|combining_builder", "source_gen|part_cleanup"] 14 | runs_before: ["angel_orm_generator|angel_orm"] 15 | typescript: 16 | import: "package:angel_serialize_generator/angel_serialize_generator.dart" 17 | builder_factories: 18 | - typescriptDefinitionBuilder 19 | auto_apply: root_package 20 | build_to: source 21 | build_extensions: 22 | .dart: 23 | - ".d.ts" 24 | # targets: 25 | # _book: 26 | # sources: 27 | # - "test/models/book.dart" 28 | # - "test/models/has_map.dart" 29 | # - "test/models/goat.dart" 30 | # - "test/models/game_pad_button.dart" 31 | # - "test/models/with_enum.dart" 32 | # $default: 33 | # dependencies: 34 | # - "angel_serialize_generator:_book" 35 | # sources: 36 | # - "test/models/author.dart" 37 | # - "test/models/game_pad.dart" 38 | -------------------------------------------------------------------------------- /angel_serialize/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://www.dartlang.org/tools/private-files.html 2 | 3 | # Files and directories created by pub 4 | ../.packages 5 | .packages 6 | .pub/ 7 | build/ 8 | # If you're building an application, you may want to check-in your pubspec.lock 9 | pubspec.lock 10 | 11 | # Directory created by dartdoc 12 | # If you don't generate documentation locally you can remove this line. 13 | doc/api/ 14 | ### JetBrains template 15 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 16 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 17 | 18 | # User-specific stuff: 19 | .idea/**/workspace.xml 20 | .idea/**/tasks.xml 21 | .idea/dictionaries 22 | 23 | # Sensitive or high-churn files: 24 | .idea/**/dataSources/ 25 | .idea/**/dataSources.ids 26 | .idea/**/dataSources.xml 27 | .idea/**/dataSources.local.xml 28 | .idea/**/sqlDataSources.xml 29 | .idea/**/dynamic.xml 30 | .idea/**/uiDesigner.xml 31 | 32 | # Gradle: 33 | .idea/**/gradle.xml 34 | .idea/**/libraries 35 | 36 | # Mongo Explorer plugin: 37 | .idea/**/mongoSettings.xml 38 | 39 | ## File-based project format: 40 | *.iws 41 | 42 | ## Plugin-specific files: 43 | 44 | # IntelliJ 45 | /out/ 46 | 47 | # mpeltonen/sbt-idea plugin 48 | .idea_modules/ 49 | 50 | # JIRA plugin 51 | atlassian-ide-plugin.xml 52 | 53 | # Crashlytics plugin (for Android Studio and IntelliJ) 54 | com_crashlytics_export_strings.xml 55 | crashlytics.properties 56 | crashlytics-build.properties 57 | fabric.properties 58 | -------------------------------------------------------------------------------- /angel_serialize_generator/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://www.dartlang.org/tools/private-files.html 2 | 3 | # Files and directories created by pub 4 | .packages 5 | .pub/ 6 | build/ 7 | # If you're building an application, you may want to check-in your pubspec.lock 8 | pubspec.lock 9 | 10 | # Directory created by dartdoc 11 | # If you don't generate documentation locally you can remove this line. 12 | doc/api/ 13 | ### JetBrains template 14 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 15 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 16 | 17 | # User-specific stuff: 18 | .idea/**/workspace.xml 19 | .idea/**/tasks.xml 20 | .idea/dictionaries 21 | 22 | # Sensitive or high-churn files: 23 | .idea/**/dataSources/ 24 | .idea/**/dataSources.ids 25 | .idea/**/dataSources.xml 26 | .idea/**/dataSources.local.xml 27 | .idea/**/sqlDataSources.xml 28 | .idea/**/dynamic.xml 29 | .idea/**/uiDesigner.xml 30 | 31 | # Gradle: 32 | .idea/**/gradle.xml 33 | .idea/**/libraries 34 | 35 | # Mongo Explorer plugin: 36 | .idea/**/mongoSettings.xml 37 | 38 | ## File-based project format: 39 | *.iws 40 | 41 | ## Plugin-specific files: 42 | 43 | # IntelliJ 44 | /out/ 45 | 46 | # mpeltonen/sbt-idea plugin 47 | .idea_modules/ 48 | 49 | # JIRA plugin 50 | atlassian-ide-plugin.xml 51 | 52 | # Crashlytics plugin (for Android Studio and IntelliJ) 53 | com_crashlytics_export_strings.xml 54 | crashlytics.properties 55 | crashlytics-build.properties 56 | fabric.properties 57 | 58 | *.g.part -------------------------------------------------------------------------------- /angel_serialize_generator/test/models/book.dart: -------------------------------------------------------------------------------- 1 | library angel_serialize.test.models.book; 2 | 3 | import 'package:angel_model/angel_model.dart'; 4 | import 'package:angel_serialize/angel_serialize.dart'; 5 | import 'package:collection/collection.dart'; 6 | import 'package:meta/meta.dart'; 7 | part 'book.g.dart'; 8 | 9 | @Serializable( 10 | serializers: Serializers.all, 11 | includeAnnotations: [ 12 | pragma('hello'), 13 | SerializableField(alias: 'omg'), 14 | ], 15 | ) 16 | abstract class _Book extends Model { 17 | String author, title, description; 18 | 19 | /// The number of pages the book has. 20 | int pageCount; 21 | 22 | List notModels; 23 | 24 | @SerializableField(alias: 'camelCase', isNullable: true) 25 | String camelCaseString; 26 | } 27 | 28 | @Serializable(serializers: Serializers.all) 29 | abstract class _Author extends Model { 30 | @SerializableField(isNullable: false) 31 | String get name; 32 | 33 | String get customMethod => 'hey!'; 34 | 35 | @SerializableField( 36 | isNullable: false, errorMessage: 'Custom message for missing `age`') 37 | int get age; 38 | 39 | List<_Book> get books; 40 | 41 | /// The newest book. 42 | _Book get newestBook; 43 | 44 | @SerializableField(exclude: true, isNullable: true) 45 | String get secret; 46 | 47 | @SerializableField(exclude: true, canDeserialize: true, isNullable: true) 48 | String get obscured; 49 | } 50 | 51 | @Serializable(serializers: Serializers.all) 52 | abstract class _Library extends Model { 53 | Map get collection; 54 | } 55 | 56 | @Serializable(serializers: Serializers.all) 57 | abstract class _Bookmark extends Model { 58 | @SerializableField(exclude: true) 59 | final _Book book; 60 | 61 | List get history; 62 | 63 | @SerializableField(isNullable: false) 64 | int get page; 65 | 66 | String get comment; 67 | 68 | _Bookmark(this.book); 69 | } 70 | -------------------------------------------------------------------------------- /angel_serialize_generator/test/enum_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:test/test.dart'; 4 | import 'models/with_enum.dart'; 5 | 6 | const WithEnum aWithEnum = WithEnum(type: WithEnumType.a); 7 | const WithEnum aWithEnum2 = WithEnum(type: WithEnumType.a); 8 | 9 | void main() { 10 | test('enum serializes to int', () { 11 | var w = WithEnum(type: WithEnumType.b).toJson(); 12 | expect(w[WithEnumFields.type], WithEnumType.values.indexOf(WithEnumType.b)); 13 | }); 14 | 15 | test('enum serializes null if null', () { 16 | var w = WithEnum(type: null).toJson(); 17 | expect(w[WithEnumFields.type], null); 18 | }); 19 | 20 | test('enum deserializes to default value from null', () { 21 | var map = {WithEnumFields.type: null}; 22 | var w = WithEnumSerializer.fromMap(map); 23 | expect(w.type, WithEnumType.b); 24 | }); 25 | 26 | test('enum deserializes from int', () { 27 | var map = { 28 | WithEnumFields.type: WithEnumType.values.indexOf(WithEnumType.b) 29 | }; 30 | var w = WithEnumSerializer.fromMap(map); 31 | expect(w.type, WithEnumType.b); 32 | }); 33 | 34 | test('enum deserializes from value', () { 35 | var map = {WithEnumFields.type: WithEnumType.c}; 36 | var w = WithEnumSerializer.fromMap(map); 37 | expect(w.type, WithEnumType.c); 38 | }); 39 | 40 | test('equality', () { 41 | expect(WithEnum(type: WithEnumType.a), WithEnum(type: WithEnumType.a)); 42 | expect( 43 | WithEnum(type: WithEnumType.a), isNot(WithEnum(type: WithEnumType.b))); 44 | }); 45 | 46 | test('const', () { 47 | expect(identical(aWithEnum, aWithEnum2), true); 48 | }); 49 | 50 | test('uint8list', () { 51 | var ee = WithEnum( 52 | imageBytes: Uint8List.fromList(List.generate(1000, (i) => i))); 53 | var eeMap = ee.toJson(); 54 | print(ee); 55 | var ef = WithEnumSerializer.fromMap(eeMap); 56 | expect(ee.copyWith(), ee); 57 | expect(ef, ee); 58 | }); 59 | } 60 | -------------------------------------------------------------------------------- /angel_serialize_generator/example/main.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'main.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonModelGenerator 7 | // ************************************************************************** 8 | 9 | @generatedSerializable 10 | class Todo extends _Todo { 11 | Todo({this.text, this.completed}); 12 | 13 | @override 14 | String text; 15 | 16 | @override 17 | bool completed; 18 | 19 | Todo copyWith({String text, bool completed}) { 20 | return Todo( 21 | text: text ?? this.text, completed: completed ?? this.completed); 22 | } 23 | 24 | bool operator ==(other) { 25 | return other is _Todo && other.text == text && other.completed == completed; 26 | } 27 | 28 | @override 29 | int get hashCode { 30 | return hashObjects([text, completed]); 31 | } 32 | 33 | @override 34 | String toString() { 35 | return "Todo(text=$text, completed=$completed)"; 36 | } 37 | 38 | Map toJson() { 39 | return TodoSerializer.toMap(this); 40 | } 41 | } 42 | 43 | // ************************************************************************** 44 | // SerializerGenerator 45 | // ************************************************************************** 46 | 47 | const TodoSerializer todoSerializer = TodoSerializer(); 48 | 49 | class TodoEncoder extends Converter { 50 | const TodoEncoder(); 51 | 52 | @override 53 | Map convert(Todo model) => TodoSerializer.toMap(model); 54 | } 55 | 56 | class TodoDecoder extends Converter { 57 | const TodoDecoder(); 58 | 59 | @override 60 | Todo convert(Map map) => TodoSerializer.fromMap(map); 61 | } 62 | 63 | class TodoSerializer extends Codec { 64 | const TodoSerializer(); 65 | 66 | @override 67 | get encoder => const TodoEncoder(); 68 | @override 69 | get decoder => const TodoDecoder(); 70 | static Todo fromMap(Map map) { 71 | return Todo( 72 | text: map['text'] as String, completed: map['completed'] as bool); 73 | } 74 | 75 | static Map toMap(_Todo model) { 76 | if (model == null) { 77 | return null; 78 | } 79 | return {'text': model.text, 'completed': model.completed}; 80 | } 81 | } 82 | 83 | abstract class TodoFields { 84 | static const List allFields = [text, completed]; 85 | 86 | static const String text = 'text'; 87 | 88 | static const String completed = 'completed'; 89 | } 90 | -------------------------------------------------------------------------------- /angel_serialize_generator/test/models/goat.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'goat.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonModelGenerator 7 | // ************************************************************************** 8 | 9 | @generatedSerializable 10 | class Goat implements _Goat { 11 | const Goat({this.integer = 34, this.list = const [34, 35]}); 12 | 13 | @override 14 | final int integer; 15 | 16 | @override 17 | final List list; 18 | 19 | Goat copyWith({int integer, List list}) { 20 | return Goat(integer: integer ?? this.integer, list: list ?? this.list); 21 | } 22 | 23 | bool operator ==(other) { 24 | return other is _Goat && 25 | other.integer == integer && 26 | ListEquality(DefaultEquality()).equals(other.list, list); 27 | } 28 | 29 | @override 30 | int get hashCode { 31 | return hashObjects([integer, list]); 32 | } 33 | 34 | @override 35 | String toString() { 36 | return "Goat(integer=$integer, list=$list)"; 37 | } 38 | 39 | Map toJson() { 40 | return GoatSerializer.toMap(this); 41 | } 42 | } 43 | 44 | // ************************************************************************** 45 | // SerializerGenerator 46 | // ************************************************************************** 47 | 48 | const GoatSerializer goatSerializer = GoatSerializer(); 49 | 50 | class GoatEncoder extends Converter { 51 | const GoatEncoder(); 52 | 53 | @override 54 | Map convert(Goat model) => GoatSerializer.toMap(model); 55 | } 56 | 57 | class GoatDecoder extends Converter { 58 | const GoatDecoder(); 59 | 60 | @override 61 | Goat convert(Map map) => GoatSerializer.fromMap(map); 62 | } 63 | 64 | class GoatSerializer extends Codec { 65 | const GoatSerializer(); 66 | 67 | @override 68 | get encoder => const GoatEncoder(); 69 | @override 70 | get decoder => const GoatDecoder(); 71 | static Goat fromMap(Map map) { 72 | return Goat( 73 | integer: map['integer'] as int ?? 34, 74 | list: map['list'] is Iterable 75 | ? (map['list'] as Iterable).cast().toList() 76 | : const [34, 35]); 77 | } 78 | 79 | static Map toMap(_Goat model) { 80 | if (model == null) { 81 | return null; 82 | } 83 | return {'integer': model.integer, 'list': model.list}; 84 | } 85 | } 86 | 87 | abstract class GoatFields { 88 | static const List allFields = [integer, list]; 89 | 90 | static const String integer = 'integer'; 91 | 92 | static const String list = 'list'; 93 | } 94 | -------------------------------------------------------------------------------- /angel_serialize_generator/test/models/has_map.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'has_map.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonModelGenerator 7 | // ************************************************************************** 8 | 9 | @generatedSerializable 10 | class HasMap implements _HasMap { 11 | const HasMap({@required this.value}); 12 | 13 | @override 14 | final Map value; 15 | 16 | HasMap copyWith({Map value}) { 17 | return HasMap(value: value ?? this.value); 18 | } 19 | 20 | bool operator ==(other) { 21 | return other is _HasMap && 22 | MapEquality( 23 | keys: DefaultEquality(), values: DefaultEquality()) 24 | .equals(other.value, value); 25 | } 26 | 27 | @override 28 | int get hashCode { 29 | return hashObjects([value]); 30 | } 31 | 32 | @override 33 | String toString() { 34 | return "HasMap(value=$value)"; 35 | } 36 | 37 | Map toJson() { 38 | return HasMapSerializer.toMap(this); 39 | } 40 | } 41 | 42 | // ************************************************************************** 43 | // SerializerGenerator 44 | // ************************************************************************** 45 | 46 | const HasMapSerializer hasMapSerializer = HasMapSerializer(); 47 | 48 | class HasMapEncoder extends Converter { 49 | const HasMapEncoder(); 50 | 51 | @override 52 | Map convert(HasMap model) => HasMapSerializer.toMap(model); 53 | } 54 | 55 | class HasMapDecoder extends Converter { 56 | const HasMapDecoder(); 57 | 58 | @override 59 | HasMap convert(Map map) => HasMapSerializer.fromMap(map); 60 | } 61 | 62 | class HasMapSerializer extends Codec { 63 | const HasMapSerializer(); 64 | 65 | @override 66 | get encoder => const HasMapEncoder(); 67 | @override 68 | get decoder => const HasMapDecoder(); 69 | static HasMap fromMap(Map map) { 70 | if (map['value'] == null) { 71 | throw FormatException("Missing required field 'value' on HasMap."); 72 | } 73 | 74 | return HasMap(value: _fromString(map['value'])); 75 | } 76 | 77 | static Map toMap(_HasMap model) { 78 | if (model == null) { 79 | return null; 80 | } 81 | if (model.value == null) { 82 | throw FormatException("Missing required field 'value' on HasMap."); 83 | } 84 | 85 | return {'value': _toString(model.value)}; 86 | } 87 | } 88 | 89 | abstract class HasMapFields { 90 | static const List allFields = [value]; 91 | 92 | static const String value = 'value'; 93 | } 94 | -------------------------------------------------------------------------------- /angel_serialize_generator/lib/context.dart: -------------------------------------------------------------------------------- 1 | import 'package:analyzer/dart/constant/value.dart'; 2 | import 'package:analyzer/dart/element/element.dart'; 3 | import 'package:analyzer/dart/element/type.dart'; 4 | import 'package:angel_serialize/angel_serialize.dart'; 5 | import 'package:code_builder/code_builder.dart'; 6 | import 'package:recase/recase.dart'; 7 | import 'package:source_gen/source_gen.dart'; 8 | 9 | /// A base context for building serializable classes. 10 | class BuildContext { 11 | ReCase _modelClassNameRecase; 12 | TypeReference _modelClassType; 13 | 14 | /// A map of fields that are absolutely required, and error messages for when they are absent. 15 | final Map requiredFields = {}; 16 | 17 | /// A map of field names to resolved names from `@Alias()` declarations. 18 | final Map aliases = {}; 19 | 20 | /// A map of field names to their default values. 21 | final Map defaults = {}; 22 | 23 | /// A map of fields to their related information. 24 | final Map fieldInfo = {}; 25 | 26 | /// A map of fields that have been marked as to be excluded from serialization. 27 | // ignore: deprecated_member_use 28 | final Map excluded = {}; 29 | 30 | /// A map of "synthetic" fields, i.e. `id` and `created_at` injected automatically. 31 | final Map shimmed = {}; 32 | 33 | final bool autoIdAndDateFields, autoSnakeCaseNames; 34 | 35 | final String originalClassName, sourceFilename; 36 | 37 | /// The fields declared on the original class. 38 | final List fields = []; 39 | 40 | final List constructorParameters = []; 41 | 42 | final ConstantReader annotation; 43 | 44 | final ClassElement clazz; 45 | 46 | /// Any annotations to include in the generated class. 47 | final List includeAnnotations; 48 | 49 | /// The name of the field that identifies data of this model type. 50 | String primaryKeyName = 'id'; 51 | 52 | BuildContext(this.annotation, this.clazz, 53 | {this.originalClassName, 54 | this.sourceFilename, 55 | this.autoSnakeCaseNames, 56 | this.autoIdAndDateFields, 57 | this.includeAnnotations = const []}); 58 | 59 | /// The name of the generated class. 60 | String get modelClassName => originalClassName.startsWith('_') 61 | ? originalClassName.substring(1) 62 | : originalClassName; 63 | 64 | /// A [ReCase] instance reflecting on the [modelClassName]. 65 | ReCase get modelClassNameRecase => 66 | _modelClassNameRecase ??= ReCase(modelClassName); 67 | 68 | TypeReference get modelClassType => 69 | _modelClassType ??= TypeReference((b) => b.symbol = modelClassName); 70 | 71 | /// The [FieldElement] pointing to the primary key. 72 | FieldElement get primaryKeyField => 73 | fields.firstWhere((f) => f.name == primaryKeyName); 74 | 75 | bool get importsPackageMeta { 76 | return clazz.library.imports.any((i) => i.uri == 'package:meta/meta.dart'); 77 | } 78 | 79 | /// Get the aliased name (if one is defined) for a field. 80 | String resolveFieldName(String name) => 81 | aliases.containsKey(name) ? aliases[name] : name; 82 | 83 | /// Finds the type that the field [name] should serialize to. 84 | DartType resolveSerializedFieldType(String name) { 85 | return fieldInfo[name]?.serializesTo ?? 86 | fields.firstWhere((f) => f.name == name).type; 87 | } 88 | } 89 | 90 | class SerializableFieldMirror { 91 | final String alias; 92 | final DartObject defaultValue; 93 | final Symbol serializer, deserializer; 94 | final String errorMessage; 95 | final bool isNullable, canDeserialize, canSerialize, exclude; 96 | final DartType serializesTo; 97 | 98 | SerializableFieldMirror( 99 | {this.alias, 100 | this.defaultValue, 101 | this.serializer, 102 | this.deserializer, 103 | this.errorMessage, 104 | this.isNullable, 105 | this.canDeserialize, 106 | this.canSerialize, 107 | this.exclude, 108 | this.serializesTo}); 109 | } 110 | -------------------------------------------------------------------------------- /angel_serialize_generator/test/models/with_enum.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'with_enum.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonModelGenerator 7 | // ************************************************************************** 8 | 9 | @generatedSerializable 10 | class WithEnum implements _WithEnum { 11 | const WithEnum({this.type = WithEnumType.b, this.finalList, this.imageBytes}); 12 | 13 | @override 14 | final WithEnumType type; 15 | 16 | @override 17 | final List finalList; 18 | 19 | @override 20 | final Uint8List imageBytes; 21 | 22 | WithEnum copyWith( 23 | {WithEnumType type, List finalList, Uint8List imageBytes}) { 24 | return WithEnum( 25 | type: type ?? this.type, 26 | finalList: finalList ?? this.finalList, 27 | imageBytes: imageBytes ?? this.imageBytes); 28 | } 29 | 30 | bool operator ==(other) { 31 | return other is _WithEnum && 32 | other.type == type && 33 | ListEquality(DefaultEquality()) 34 | .equals(other.finalList, finalList) && 35 | ListEquality().equals(other.imageBytes, imageBytes); 36 | } 37 | 38 | @override 39 | int get hashCode { 40 | return hashObjects([type, finalList, imageBytes]); 41 | } 42 | 43 | @override 44 | String toString() { 45 | return "WithEnum(type=$type, finalList=$finalList, imageBytes=$imageBytes)"; 46 | } 47 | 48 | Map toJson() { 49 | return WithEnumSerializer.toMap(this); 50 | } 51 | } 52 | 53 | // ************************************************************************** 54 | // SerializerGenerator 55 | // ************************************************************************** 56 | 57 | const WithEnumSerializer withEnumSerializer = WithEnumSerializer(); 58 | 59 | class WithEnumEncoder extends Converter { 60 | const WithEnumEncoder(); 61 | 62 | @override 63 | Map convert(WithEnum model) => WithEnumSerializer.toMap(model); 64 | } 65 | 66 | class WithEnumDecoder extends Converter { 67 | const WithEnumDecoder(); 68 | 69 | @override 70 | WithEnum convert(Map map) => WithEnumSerializer.fromMap(map); 71 | } 72 | 73 | class WithEnumSerializer extends Codec { 74 | const WithEnumSerializer(); 75 | 76 | @override 77 | get encoder => const WithEnumEncoder(); 78 | @override 79 | get decoder => const WithEnumDecoder(); 80 | static WithEnum fromMap(Map map) { 81 | return WithEnum( 82 | type: map['type'] is WithEnumType 83 | ? (map['type'] as WithEnumType) 84 | : (map['type'] is int 85 | ? WithEnumType.values[map['type'] as int] 86 | : WithEnumType.b), 87 | finalList: map['final_list'] is Iterable 88 | ? (map['final_list'] as Iterable).cast().toList() 89 | : null, 90 | imageBytes: map['image_bytes'] is Uint8List 91 | ? (map['image_bytes'] as Uint8List) 92 | : (map['image_bytes'] is Iterable 93 | ? Uint8List.fromList( 94 | (map['image_bytes'] as Iterable).toList()) 95 | : (map['image_bytes'] is String 96 | ? Uint8List.fromList( 97 | base64.decode(map['image_bytes'] as String)) 98 | : null))); 99 | } 100 | 101 | static Map toMap(_WithEnum model) { 102 | if (model == null) { 103 | return null; 104 | } 105 | return { 106 | 'type': 107 | model.type == null ? null : WithEnumType.values.indexOf(model.type), 108 | 'final_list': model.finalList, 109 | 'image_bytes': 110 | model.imageBytes == null ? null : base64.encode(model.imageBytes) 111 | }; 112 | } 113 | } 114 | 115 | abstract class WithEnumFields { 116 | static const List allFields = [type, finalList, imageBytes]; 117 | 118 | static const String type = 'type'; 119 | 120 | static const String finalList = 'final_list'; 121 | 122 | static const String imageBytes = 'image_bytes'; 123 | } 124 | -------------------------------------------------------------------------------- /angel_serialize_generator/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 2.5.0 2 | * Support mutable models (again). 3 | * Use `whereType()` instead of chaining `where()` and `cast()`. 4 | * Support pulling fields from parent classes and interfaces. 5 | * Only generate `const` constructors if *all* 6 | fields lack a setter. 7 | * Don't type-annotate initializing formals. 8 | 9 | # 2.4.4 10 | * Remove unnecessary `new` and `const`. 11 | 12 | # 2.4.3 13 | * Generate `Codec` and `Converter` classes. 14 | * Generate `toString` methods. 15 | * Include original documentation comments from the model. 16 | 17 | # 2.4.2 18 | * Fix bug where enums didn't support default values. 19 | * Stop emitting `@required` on items with default values. 20 | * Create default `@SerializableField` for fields without them. 21 | 22 | # 2.4.1+1 23 | * Change `as Iterable` to `.cast`. 24 | 25 | # 2.4.1 26 | * Support `serializesTo`. 27 | * Don't emit `@required` if there is a default value. 28 | * Deprecate `autoIdAndDateFields`. 29 | 30 | # 2.4.0 31 | * Introduce `@SerializableField`, and say goodbye to annotation hell. 32 | * Support custom (de)serializers. 33 | * Allow passing of annotations to the generated class. 34 | * Fixted TypeScript `ref` generator. 35 | 36 | # 2.3.0 37 | * Add `@DefaultValue` support. 38 | 39 | # 2.2.2 40 | * Split out TS def builder, to emit to source. 41 | 42 | # 2.2.1 43 | * Explicit changes for assisting `angel_orm_generator`. 44 | 45 | # 2.2.0 46 | * Build to `cache`. 47 | * Only generate one `.g.dart` file. 48 | * Support for `Uint8List`. 49 | * Use `.cast()` for `List`s and `Map`s of *non-`Model`* types. 50 | 51 | # 2.1.2 52 | * Add `declare module` to generated TypeScript files. 53 | 54 | # 2.1.1 55 | * Generate `hashCode`. 56 | 57 | # 2.1.0 58 | * Removed dependency on `package:id`. 59 | * Update dependencies for Dart2Stable. 60 | * `jsonModelBuilder` now uses `SharedPartBuilder`, rather than 61 | `PartBuilder`. 62 | 63 | # 2.0.10 64 | * Generate `XFields.allFields` constant. 65 | * No longer breaks in cases where `dynamic` is present. 66 | * Call `toJson` in `toMap` on nested models. 67 | * Never generate named parameters from private fields. 68 | * Use the new `@generatedSerializable` to *always* find generated 69 | models. 70 | 71 | # 2.0.9+4 72 | * Remove `defaults` in `build.yaml`. 73 | 74 | # 2.0.9+3 75 | * Fix a cast error when self-referencing nested list expressions. 76 | 77 | # 2.0.9+2 78 | * Fix previously unseen cast errors with enums. 79 | 80 | # 2.0.9+1 81 | * Fix a cast error when deserializing nested model classes. 82 | 83 | # 2.0.9 84 | * Upgrade to `source_gen@^0.8.0`. 85 | 86 | # 2.0.8+3 87 | * Don't fail on `null` in `toMap`. 88 | * Support self-referencing via `toJson()`. 89 | 90 | # 2.0.8+2 91 | * Better discern when custom methods disqualify classes 92 | from `const` protection. 93 | 94 | # 2.0.8+1 95 | * Fix generation of `const` constructors with iterables. 96 | 97 | # 2.0.8 98 | * Now supports de/serialization of `enum` types. 99 | * Generate `const` constructors when possible. 100 | * Remove `whereType`, perform manual coercion. 101 | * Generate a `fromMap` with typecasting, for Dart 2's sake. 102 | 103 | # 2.0.7 104 | * Create unmodifiable Lists and Maps. 105 | * Support `@required` on fields. 106 | * Affix an `@immutable` annotation to classes, if 107 | `package:meta` is imported. 108 | * Add `/// ` to TypeScript models. 109 | 110 | # 2.0.6 111 | * Support for using `abstract` to create immutable model classes. 112 | * Add support for custom constructor parameters. 113 | * Closed [#21](https://github.com/angel-dart/serialize/issues/21) - better naming 114 | of `Map` types. 115 | * Added overridden `==` operators. 116 | 117 | # 2.0.5 118 | * Deserialization now supports un-serialized `DateTime`. 119 | * Better support for regular typed Lists and Maps in TypeScript. 120 | 121 | # 2.0.4 122 | * Fields in TypeScript definitions are now nullable by default. 123 | 124 | # 2.0.3 125 | * Added a `TypeScriptDefinitionBuilder`. 126 | 127 | # 2.0.2 128 | * Generates an `XFields` class with the serialized names of 129 | all fields in a model class `X`. 130 | * Removed unnecessary named parameters from `XSerializer.fromMap`. 131 | 132 | # 2.0.1 133 | * Ensured that `List` is only transformed if 134 | it generically references a `Model`. -------------------------------------------------------------------------------- /angel_serialize/lib/angel_serialize.dart: -------------------------------------------------------------------------------- 1 | export 'dart:convert' show json, Codec, Converter; 2 | export 'package:angel_model/angel_model.dart'; 3 | export 'package:collection/collection.dart'; 4 | export 'package:meta/meta.dart' show required, Required; 5 | export 'package:quiver_hashcode/hashcode.dart' show hashObjects; 6 | 7 | /// Excludes a field from being excluded. 8 | class Exclude extends SerializableField { 9 | const Exclude({bool canDeserialize = false, bool canSerialize = false}) 10 | : super( 11 | exclude: true, 12 | canDeserialize: canDeserialize, 13 | canSerialize: canSerialize); 14 | } 15 | 16 | /// No longer necessary, as this is the default. 17 | @deprecated 18 | const SerializableField nullable = SerializableField(isNullable: true); 19 | 20 | /// Marks a field as not accepting `null` values. 21 | const SerializableField notNull = SerializableField(isNullable: false); 22 | 23 | const Exclude exclude = Exclude(); 24 | 25 | /// Shorthand for [SerializableField]. 26 | class DefaultsTo extends SerializableField { 27 | const DefaultsTo(value) : super(defaultValue: value); 28 | } 29 | 30 | /// Shorthand for [SerializableField]. 31 | class HasAlias extends SerializableField { 32 | const HasAlias(String name) : super(alias: name); 33 | } 34 | 35 | /// Attaches options to a field. 36 | class SerializableField { 37 | /// An alternative name for this field. 38 | final String alias; 39 | 40 | /// A default for this field. 41 | final defaultValue; 42 | 43 | /// A custom serializer for this field. 44 | final Symbol serializer; 45 | 46 | /// A custom serializer for this field. 47 | final Symbol deserializer; 48 | 49 | /// An error message to be printed when the provided value is invalid. 50 | final String errorMessage; 51 | 52 | /// Whether this field can be set to `null`. 53 | final bool isNullable; 54 | 55 | /// Whether to exclude this field from serialization. Defaults to `false`. 56 | final bool exclude; 57 | 58 | /// Whether this field can be serialized, if [exclude] is `true`. Defaults to `false`. 59 | final bool canDeserialize; 60 | 61 | /// Whether this field can be serialized, if [exclude] is `true`. Defaults to `false`. 62 | final bool canSerialize; 63 | 64 | /// May be used with [serializer] and [deserializer]. 65 | /// 66 | /// Specifies the [Type] that this field serializes to. 67 | /// 68 | /// Ex. If you have a field that serializes to a JSON string, 69 | /// specify `serializesTo: String`. 70 | final Type serializesTo; 71 | 72 | const SerializableField( 73 | {this.alias, 74 | this.defaultValue, 75 | this.serializer, 76 | this.deserializer, 77 | this.errorMessage, 78 | this.isNullable = true, 79 | this.exclude = false, 80 | this.canDeserialize = false, 81 | this.canSerialize = false, 82 | this.serializesTo}); 83 | } 84 | 85 | /// Marks a class as eligible for serialization. 86 | class Serializable { 87 | const Serializable( 88 | {this.serializers = const [Serializers.map, Serializers.json], 89 | this.autoSnakeCaseNames = true, 90 | // ignore: deprecated_member_use_from_same_package 91 | @deprecated this.autoIdAndDateFields = true, 92 | this.includeAnnotations = const []}); 93 | 94 | /// A list of enabled serialization modes. 95 | /// 96 | /// See [Serializers]. 97 | final List serializers; 98 | 99 | /// Overrides the setting in `SerializerGenerator`. 100 | final bool autoSnakeCaseNames; 101 | 102 | /// Overrides the setting in `JsonModelGenerator`. 103 | @deprecated 104 | final bool autoIdAndDateFields; 105 | 106 | /// A list of constant members to affix to the generated class. 107 | final List includeAnnotations; 108 | } 109 | 110 | const Serializable serializable = Serializable(); 111 | 112 | /// Used by `package:angel_serialize_generator` to reliably identify generated models. 113 | class GeneratedSerializable { 114 | const GeneratedSerializable(); 115 | } 116 | 117 | const GeneratedSerializable generatedSerializable = GeneratedSerializable(); 118 | 119 | /// The supported serialization types. 120 | abstract class Serializers { 121 | /// All supported serialization types. 122 | static const List all = [map, json, typescript]; 123 | 124 | /// Enable `fromMap` and `toMap` methods on the model class. 125 | static const int map = 0; 126 | 127 | /// Enable a `toJson` method on the model class. 128 | static const int json = 1; 129 | 130 | /// Generate a TypeScript definition file (`.d.ts`) for use on the client-side. 131 | static const int typescript = 2; 132 | } 133 | 134 | @deprecated 135 | class DefaultValue { 136 | final value; 137 | 138 | const DefaultValue(this.value); 139 | } 140 | 141 | @deprecated 142 | 143 | /// Prefer [SerializableField] instead. 144 | class Alias { 145 | final String name; 146 | 147 | const Alias(this.name); 148 | } 149 | -------------------------------------------------------------------------------- /angel_serialize_generator/test/models/game_pad_button.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'game_pad_button.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonModelGenerator 7 | // ************************************************************************** 8 | 9 | @generatedSerializable 10 | class GamepadButton implements _GamepadButton { 11 | const GamepadButton({this.name, this.radius}); 12 | 13 | @override 14 | final String name; 15 | 16 | @override 17 | final int radius; 18 | 19 | GamepadButton copyWith({String name, int radius}) { 20 | return GamepadButton( 21 | name: name ?? this.name, radius: radius ?? this.radius); 22 | } 23 | 24 | bool operator ==(other) { 25 | return other is _GamepadButton && 26 | other.name == name && 27 | other.radius == radius; 28 | } 29 | 30 | @override 31 | int get hashCode { 32 | return hashObjects([name, radius]); 33 | } 34 | 35 | @override 36 | String toString() { 37 | return "GamepadButton(name=$name, radius=$radius)"; 38 | } 39 | 40 | Map toJson() { 41 | return GamepadButtonSerializer.toMap(this); 42 | } 43 | } 44 | 45 | @generatedSerializable 46 | class Gamepad extends _Gamepad { 47 | Gamepad({List<_GamepadButton> buttons, Map dynamicMap}) 48 | : this.buttons = List.unmodifiable(buttons ?? []), 49 | this.dynamicMap = Map.unmodifiable(dynamicMap ?? {}); 50 | 51 | @override 52 | List<_GamepadButton> buttons; 53 | 54 | @override 55 | Map dynamicMap; 56 | 57 | Gamepad copyWith( 58 | {List<_GamepadButton> buttons, Map dynamicMap}) { 59 | return Gamepad( 60 | buttons: buttons ?? this.buttons, 61 | dynamicMap: dynamicMap ?? this.dynamicMap); 62 | } 63 | 64 | bool operator ==(other) { 65 | return other is _Gamepad && 66 | ListEquality<_GamepadButton>(DefaultEquality<_GamepadButton>()) 67 | .equals(other.buttons, buttons) && 68 | MapEquality( 69 | keys: DefaultEquality(), values: DefaultEquality()) 70 | .equals(other.dynamicMap, dynamicMap); 71 | } 72 | 73 | @override 74 | int get hashCode { 75 | return hashObjects([buttons, dynamicMap]); 76 | } 77 | 78 | @override 79 | String toString() { 80 | return "Gamepad(buttons=$buttons, dynamicMap=$dynamicMap)"; 81 | } 82 | 83 | Map toJson() { 84 | return GamepadSerializer.toMap(this); 85 | } 86 | } 87 | 88 | // ************************************************************************** 89 | // SerializerGenerator 90 | // ************************************************************************** 91 | 92 | const GamepadButtonSerializer gamepadButtonSerializer = 93 | GamepadButtonSerializer(); 94 | 95 | class GamepadButtonEncoder extends Converter { 96 | const GamepadButtonEncoder(); 97 | 98 | @override 99 | Map convert(GamepadButton model) => GamepadButtonSerializer.toMap(model); 100 | } 101 | 102 | class GamepadButtonDecoder extends Converter { 103 | const GamepadButtonDecoder(); 104 | 105 | @override 106 | GamepadButton convert(Map map) => GamepadButtonSerializer.fromMap(map); 107 | } 108 | 109 | class GamepadButtonSerializer extends Codec { 110 | const GamepadButtonSerializer(); 111 | 112 | @override 113 | get encoder => const GamepadButtonEncoder(); 114 | @override 115 | get decoder => const GamepadButtonDecoder(); 116 | static GamepadButton fromMap(Map map) { 117 | return GamepadButton( 118 | name: map['name'] as String, radius: map['radius'] as int); 119 | } 120 | 121 | static Map toMap(_GamepadButton model) { 122 | if (model == null) { 123 | return null; 124 | } 125 | return {'name': model.name, 'radius': model.radius}; 126 | } 127 | } 128 | 129 | abstract class GamepadButtonFields { 130 | static const List allFields = [name, radius]; 131 | 132 | static const String name = 'name'; 133 | 134 | static const String radius = 'radius'; 135 | } 136 | 137 | const GamepadSerializer gamepadSerializer = GamepadSerializer(); 138 | 139 | class GamepadEncoder extends Converter { 140 | const GamepadEncoder(); 141 | 142 | @override 143 | Map convert(Gamepad model) => GamepadSerializer.toMap(model); 144 | } 145 | 146 | class GamepadDecoder extends Converter { 147 | const GamepadDecoder(); 148 | 149 | @override 150 | Gamepad convert(Map map) => GamepadSerializer.fromMap(map); 151 | } 152 | 153 | class GamepadSerializer extends Codec { 154 | const GamepadSerializer(); 155 | 156 | @override 157 | get encoder => const GamepadEncoder(); 158 | @override 159 | get decoder => const GamepadDecoder(); 160 | static Gamepad fromMap(Map map) { 161 | return Gamepad( 162 | buttons: map['buttons'] is Iterable 163 | ? List.unmodifiable(((map['buttons'] as Iterable).whereType()) 164 | .map(GamepadButtonSerializer.fromMap)) 165 | : null, 166 | dynamicMap: map['dynamic_map'] is Map 167 | ? (map['dynamic_map'] as Map).cast() 168 | : null); 169 | } 170 | 171 | static Map toMap(_Gamepad model) { 172 | if (model == null) { 173 | return null; 174 | } 175 | return { 176 | 'buttons': 177 | model.buttons?.map((m) => GamepadButtonSerializer.toMap(m))?.toList(), 178 | 'dynamic_map': model.dynamicMap 179 | }; 180 | } 181 | } 182 | 183 | abstract class GamepadFields { 184 | static const List allFields = [buttons, dynamicMap]; 185 | 186 | static const String buttons = 'buttons'; 187 | 188 | static const String dynamicMap = 'dynamic_map'; 189 | } 190 | -------------------------------------------------------------------------------- /angel_serialize_generator/test/models/subclass.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'subclass.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonModelGenerator 7 | // ************************************************************************** 8 | 9 | @generatedSerializable 10 | class Animal extends _Animal { 11 | Animal({@required this.genus, @required this.species}); 12 | 13 | @override 14 | String genus; 15 | 16 | @override 17 | String species; 18 | 19 | Animal copyWith({String genus, String species}) { 20 | return Animal(genus: genus ?? this.genus, species: species ?? this.species); 21 | } 22 | 23 | bool operator ==(other) { 24 | return other is _Animal && other.genus == genus && other.species == species; 25 | } 26 | 27 | @override 28 | int get hashCode { 29 | return hashObjects([genus, species]); 30 | } 31 | 32 | @override 33 | String toString() { 34 | return "Animal(genus=$genus, species=$species)"; 35 | } 36 | 37 | Map toJson() { 38 | return AnimalSerializer.toMap(this); 39 | } 40 | } 41 | 42 | @generatedSerializable 43 | class Bird extends _Bird { 44 | Bird({@required this.genus, @required this.species, this.isSparrow = false}); 45 | 46 | @override 47 | String genus; 48 | 49 | @override 50 | String species; 51 | 52 | @override 53 | bool isSparrow; 54 | 55 | Bird copyWith({String genus, String species, bool isSparrow}) { 56 | return Bird( 57 | genus: genus ?? this.genus, 58 | species: species ?? this.species, 59 | isSparrow: isSparrow ?? this.isSparrow); 60 | } 61 | 62 | bool operator ==(other) { 63 | return other is _Bird && 64 | other.genus == genus && 65 | other.species == species && 66 | other.isSparrow == isSparrow; 67 | } 68 | 69 | @override 70 | int get hashCode { 71 | return hashObjects([genus, species, isSparrow]); 72 | } 73 | 74 | @override 75 | String toString() { 76 | return "Bird(genus=$genus, species=$species, isSparrow=$isSparrow)"; 77 | } 78 | 79 | Map toJson() { 80 | return BirdSerializer.toMap(this); 81 | } 82 | } 83 | 84 | // ************************************************************************** 85 | // SerializerGenerator 86 | // ************************************************************************** 87 | 88 | const AnimalSerializer animalSerializer = AnimalSerializer(); 89 | 90 | class AnimalEncoder extends Converter { 91 | const AnimalEncoder(); 92 | 93 | @override 94 | Map convert(Animal model) => AnimalSerializer.toMap(model); 95 | } 96 | 97 | class AnimalDecoder extends Converter { 98 | const AnimalDecoder(); 99 | 100 | @override 101 | Animal convert(Map map) => AnimalSerializer.fromMap(map); 102 | } 103 | 104 | class AnimalSerializer extends Codec { 105 | const AnimalSerializer(); 106 | 107 | @override 108 | get encoder => const AnimalEncoder(); 109 | @override 110 | get decoder => const AnimalDecoder(); 111 | static Animal fromMap(Map map) { 112 | if (map['genus'] == null) { 113 | throw FormatException("Missing required field 'genus' on Animal."); 114 | } 115 | 116 | if (map['species'] == null) { 117 | throw FormatException("Missing required field 'species' on Animal."); 118 | } 119 | 120 | return Animal( 121 | genus: map['genus'] as String, species: map['species'] as String); 122 | } 123 | 124 | static Map toMap(_Animal model) { 125 | if (model == null) { 126 | return null; 127 | } 128 | if (model.genus == null) { 129 | throw FormatException("Missing required field 'genus' on Animal."); 130 | } 131 | 132 | if (model.species == null) { 133 | throw FormatException("Missing required field 'species' on Animal."); 134 | } 135 | 136 | return {'genus': model.genus, 'species': model.species}; 137 | } 138 | } 139 | 140 | abstract class AnimalFields { 141 | static const List allFields = [genus, species]; 142 | 143 | static const String genus = 'genus'; 144 | 145 | static const String species = 'species'; 146 | } 147 | 148 | const BirdSerializer birdSerializer = BirdSerializer(); 149 | 150 | class BirdEncoder extends Converter { 151 | const BirdEncoder(); 152 | 153 | @override 154 | Map convert(Bird model) => BirdSerializer.toMap(model); 155 | } 156 | 157 | class BirdDecoder extends Converter { 158 | const BirdDecoder(); 159 | 160 | @override 161 | Bird convert(Map map) => BirdSerializer.fromMap(map); 162 | } 163 | 164 | class BirdSerializer extends Codec { 165 | const BirdSerializer(); 166 | 167 | @override 168 | get encoder => const BirdEncoder(); 169 | @override 170 | get decoder => const BirdDecoder(); 171 | static Bird fromMap(Map map) { 172 | if (map['genus'] == null) { 173 | throw FormatException("Missing required field 'genus' on Bird."); 174 | } 175 | 176 | if (map['species'] == null) { 177 | throw FormatException("Missing required field 'species' on Bird."); 178 | } 179 | 180 | return Bird( 181 | genus: map['genus'] as String, 182 | species: map['species'] as String, 183 | isSparrow: map['is_sparrow'] as bool ?? false); 184 | } 185 | 186 | static Map toMap(_Bird model) { 187 | if (model == null) { 188 | return null; 189 | } 190 | if (model.genus == null) { 191 | throw FormatException("Missing required field 'genus' on Bird."); 192 | } 193 | 194 | if (model.species == null) { 195 | throw FormatException("Missing required field 'species' on Bird."); 196 | } 197 | 198 | return { 199 | 'genus': model.genus, 200 | 'species': model.species, 201 | 'is_sparrow': model.isSparrow 202 | }; 203 | } 204 | } 205 | 206 | abstract class BirdFields { 207 | static const List allFields = [genus, species, isSparrow]; 208 | 209 | static const String genus = 'genus'; 210 | 211 | static const String species = 'species'; 212 | 213 | static const String isSparrow = 'is_sparrow'; 214 | } 215 | -------------------------------------------------------------------------------- /angel_serialize_generator/test/book_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'models/book.dart'; 3 | 4 | const String deathlyHallowsIsbn = '0-545-01022-5'; 5 | 6 | main() { 7 | var deathlyHallows = Book( 8 | id: '0', 9 | author: 'J.K. Rowling', 10 | title: 'Harry Potter and the Deathly Hallows', 11 | description: 'The 7th book.', 12 | pageCount: 759, 13 | notModels: [1.0, 3.0], 14 | updatedAt: DateTime.now()); 15 | var serializedDeathlyHallows = deathlyHallows.toJson(); 16 | print('Deathly Hallows: $deathlyHallows'); 17 | 18 | var jkRowling = Author( 19 | id: '1', 20 | name: 'J.K. Rowling', 21 | age: 51, 22 | books: [deathlyHallows], 23 | newestBook: deathlyHallows); 24 | var serializedJkRowling = authorSerializer.encode(jkRowling); 25 | var deathlyHallowsMap = bookSerializer.encode(deathlyHallows); 26 | print('J.K. Rowling: $jkRowling'); 27 | 28 | var library = Library(collection: {deathlyHallowsIsbn: deathlyHallows}); 29 | var serializedLibrary = LibrarySerializer.toMap(library); 30 | print('Library: $library'); 31 | 32 | group('serialization', () { 33 | test('serialization sets proper fields', () { 34 | expect(serializedDeathlyHallows['id'], deathlyHallows.id); 35 | expect(serializedDeathlyHallows['author'], deathlyHallows.author); 36 | expect( 37 | serializedDeathlyHallows['description'], deathlyHallows.description); 38 | expect(serializedDeathlyHallows['page_count'], deathlyHallows.pageCount); 39 | expect(serializedDeathlyHallows['created_at'], isNull); 40 | expect(serializedDeathlyHallows['updated_at'], 41 | deathlyHallows.updatedAt.toIso8601String()); 42 | }); 43 | 44 | test('can be mutated', () { 45 | var b = deathlyHallows.copyWith(); 46 | b.author = 'Hey'; 47 | expect(b.author, 'Hey'); 48 | expect(b.toJson()[BookFields.author], 'Hey'); 49 | }); 50 | 51 | test('heeds @Alias', () { 52 | expect(serializedDeathlyHallows['page_count'], deathlyHallows.pageCount); 53 | expect(serializedDeathlyHallows.keys, isNot(contains('pageCount'))); 54 | }); 55 | 56 | test('standard list', () { 57 | expect(serializedDeathlyHallows['not_models'], deathlyHallows.notModels); 58 | }); 59 | 60 | test('heeds @exclude', () { 61 | expect(serializedJkRowling.keys, isNot(contains('secret'))); 62 | }); 63 | 64 | test('heeds canDeserialize', () { 65 | var map = Map.from(serializedJkRowling)..['obscured'] = 'foo'; 66 | var author = authorSerializer.decode(map); 67 | expect(author.obscured, 'foo'); 68 | }); 69 | 70 | test('heeds canSerialize', () { 71 | expect(serializedJkRowling.keys, isNot(contains('obscured'))); 72 | }); 73 | 74 | test('nested @serializable class is serialized', () { 75 | expect(serializedJkRowling['newest_book'], deathlyHallowsMap); 76 | }); 77 | 78 | test('list of nested @serializable class is serialized', () { 79 | expect(serializedJkRowling['books'], [deathlyHallowsMap]); 80 | }); 81 | 82 | test('map with @serializable class as second key is serialized', () { 83 | expect(serializedLibrary['collection'], 84 | {deathlyHallowsIsbn: deathlyHallowsMap}); 85 | }); 86 | }); 87 | 88 | test('fields', () { 89 | expect(BookFields.author, 'author'); 90 | expect(BookFields.notModels, 'not_models'); 91 | expect(BookFields.camelCaseString, 'camelCase'); 92 | }); 93 | 94 | test('equals', () { 95 | expect(jkRowling.copyWith(), jkRowling); 96 | expect(deathlyHallows.copyWith(), deathlyHallows); 97 | expect(library.copyWith(), library); 98 | }); 99 | 100 | test('custom method', () { 101 | expect(jkRowling.customMethod, 'hey!'); 102 | }); 103 | 104 | test('required fields fromMap', () { 105 | expect(() => AuthorSerializer.fromMap({}), throwsFormatException); 106 | }); 107 | 108 | test('required fields toMap', () { 109 | var author = Author(name: null, age: 24); 110 | expect(() => author.toJson(), throwsFormatException); 111 | }); 112 | 113 | group('deserialization', () { 114 | test('deserialization sets proper fields', () { 115 | var book = BookSerializer.fromMap(deathlyHallowsMap); 116 | expect(book.id, deathlyHallows.id); 117 | expect(book.author, deathlyHallows.author); 118 | expect(book.description, deathlyHallows.description); 119 | expect(book.pageCount, deathlyHallows.pageCount); 120 | expect(book.notModels, deathlyHallows.notModels); 121 | expect(book.createdAt, isNull); 122 | expect(book.updatedAt, deathlyHallows.updatedAt); 123 | }); 124 | 125 | group('nested @serializable', () { 126 | var author = AuthorSerializer.fromMap(serializedJkRowling); 127 | 128 | test('nested @serializable class is deserialized', () { 129 | var newestBook = author.newestBook; 130 | expect(newestBook, isNotNull); 131 | expect(newestBook.id, deathlyHallows.id); 132 | expect(newestBook.pageCount, deathlyHallows.pageCount); 133 | expect(newestBook.updatedAt, deathlyHallows.updatedAt); 134 | }); 135 | 136 | test('list of nested @serializable class is deserialized', () { 137 | expect(author.books, allOf(isList, isNotEmpty, hasLength(1))); 138 | var book = author.books.first; 139 | expect(book.id, deathlyHallows.id); 140 | expect(book.author, deathlyHallows.author); 141 | expect(book.description, deathlyHallows.description); 142 | expect(book.pageCount, deathlyHallows.pageCount); 143 | expect(book.createdAt, isNull); 144 | expect(book.updatedAt, deathlyHallows.updatedAt); 145 | }); 146 | 147 | test('map with @serializable class as second key is deserialized', () { 148 | var lib = LibrarySerializer.fromMap(serializedLibrary); 149 | expect(lib.collection, allOf(isNotEmpty, hasLength(1))); 150 | expect(lib.collection.keys.first, deathlyHallowsIsbn); 151 | var book = lib.collection[deathlyHallowsIsbn]; 152 | expect(book.id, deathlyHallows.id); 153 | expect(book.author, deathlyHallows.author); 154 | expect(book.description, deathlyHallows.description); 155 | expect(book.pageCount, deathlyHallows.pageCount); 156 | expect(book.createdAt, isNull); 157 | expect(book.updatedAt, deathlyHallows.updatedAt); 158 | }); 159 | }); 160 | }); 161 | } 162 | -------------------------------------------------------------------------------- /angel_serialize_generator/lib/angel_serialize_generator.dart: -------------------------------------------------------------------------------- 1 | library angel_serialize_generator; 2 | 3 | import 'dart:async'; 4 | import 'dart:mirrors'; 5 | import 'dart:typed_data'; 6 | import 'package:analyzer/dart/constant/value.dart'; 7 | import 'package:analyzer/dart/element/element.dart'; 8 | import 'package:analyzer/dart/element/type.dart'; 9 | import 'package:angel_model/angel_model.dart'; 10 | import 'package:angel_serialize/angel_serialize.dart'; 11 | import 'package:build/build.dart'; 12 | import 'package:code_buffer/code_buffer.dart'; 13 | import 'package:code_builder/code_builder.dart'; 14 | import 'package:path/path.dart' as p; 15 | import 'package:recase/recase.dart'; 16 | import 'package:source_gen/source_gen.dart' hide LibraryBuilder; 17 | 18 | import 'build_context.dart'; 19 | import 'context.dart'; 20 | 21 | part 'model.dart'; 22 | 23 | part 'serialize.dart'; 24 | 25 | part 'typescript.dart'; 26 | 27 | Builder jsonModelBuilder(_) { 28 | return SharedPartBuilder(const [JsonModelGenerator()], 'angel_serialize'); 29 | } 30 | 31 | Builder serializerBuilder(_) { 32 | return SharedPartBuilder( 33 | const [SerializerGenerator()], 'angel_serialize_serializer'); 34 | } 35 | 36 | Builder typescriptDefinitionBuilder(_) { 37 | return TypeScriptDefinitionBuilder(); 38 | } 39 | 40 | /// Converts a [DartType] to a [TypeReference]. 41 | TypeReference convertTypeReference(DartType t) { 42 | return TypeReference((b) { 43 | b..symbol = t.name; 44 | 45 | if (t is InterfaceType) { 46 | b.types.addAll(t.typeArguments.map(convertTypeReference)); 47 | } 48 | }); 49 | } 50 | 51 | Expression convertObject(DartObject o) { 52 | if (o.isNull) return literalNull; 53 | if (o.toBoolValue() != null) return literalBool(o.toBoolValue()); 54 | if (o.toIntValue() != null) return literalNum(o.toIntValue()); 55 | if (o.toDoubleValue() != null) return literalNum(o.toDoubleValue()); 56 | if (o.toSymbolValue() != null) { 57 | return CodeExpression(Code('#' + o.toSymbolValue())); 58 | } 59 | if (o.toStringValue() != null) return literalString(o.toStringValue()); 60 | if (o.toTypeValue() != null) return convertTypeReference(o.toTypeValue()); 61 | if (o.toListValue() != null) { 62 | return literalList(o.toListValue().map(convertObject)); 63 | } 64 | if (o.toMapValue() != null) { 65 | return literalMap(o 66 | .toMapValue() 67 | .map((k, v) => MapEntry(convertObject(k), convertObject(v)))); 68 | } 69 | 70 | var rev = ConstantReader(o).revive(); 71 | Expression target = convertTypeReference(o.type); 72 | target = rev.accessor.isEmpty ? target : target.property(rev.accessor); 73 | return target.call(rev.positionalArguments.map(convertObject), 74 | rev.namedArguments.map((k, v) => MapEntry(k, convertObject(v)))); 75 | } 76 | 77 | String dartObjectToString(DartObject v) { 78 | var type = v.type; 79 | if (v.isNull) return 'null'; 80 | if (v.toBoolValue() != null) return v.toBoolValue().toString(); 81 | if (v.toIntValue() != null) return v.toIntValue().toString(); 82 | if (v.toDoubleValue() != null) return v.toDoubleValue().toString(); 83 | if (v.toSymbolValue() != null) return '#' + v.toSymbolValue(); 84 | if (v.toTypeValue() != null) return v.toTypeValue().name; 85 | if (v.toListValue() != null) { 86 | return 'const [' + v.toListValue().map(dartObjectToString).join(', ') + ']'; 87 | } 88 | if (v.toMapValue() != null) { 89 | return 'const {' + 90 | v.toMapValue().entries.map((entry) { 91 | var k = dartObjectToString(entry.key); 92 | var v = dartObjectToString(entry.value); 93 | return '$k: $v'; 94 | }).join(', ') + 95 | '}'; 96 | } 97 | if (v.toStringValue() != null) { 98 | return literalString(v.toStringValue()).accept(DartEmitter()).toString(); 99 | } 100 | if (type is InterfaceType && type.element.isEnum) { 101 | // Find the index of the enum, then find the member. 102 | for (var field in type.element.fields) { 103 | if (field.isEnumConstant && field.isStatic) { 104 | var value = type.element.getField(field.name).constantValue; 105 | if (value == v) { 106 | return '${type.name}.${field.name}'; 107 | } 108 | } 109 | } 110 | } 111 | 112 | throw ArgumentError(v.toString()); 113 | } 114 | 115 | /// Determines if a type supports `package:angel_serialize`. 116 | bool isModelClass(DartType t) { 117 | if (t == null) return false; 118 | 119 | if (serializableTypeChecker.hasAnnotationOf(t.element)) { 120 | return true; 121 | } 122 | 123 | if (generatedSerializableTypeChecker.hasAnnotationOf(t.element)) { 124 | return true; 125 | } 126 | 127 | if (const TypeChecker.fromRuntime(Model).isAssignableFromType(t)) { 128 | return true; 129 | } 130 | 131 | if (t is InterfaceType) { 132 | return isModelClass(t.superclass); 133 | } else { 134 | return false; 135 | } 136 | } 137 | 138 | bool isListOrMapType(DartType t) { 139 | return (const TypeChecker.fromRuntime(List).isAssignableFromType(t) || 140 | const TypeChecker.fromRuntime(Map).isAssignableFromType(t)) && 141 | !const TypeChecker.fromRuntime(Uint8List).isAssignableFromType(t); 142 | } 143 | 144 | bool isEnumType(DartType t) { 145 | if (t is InterfaceType) { 146 | return t.element.isEnum; 147 | } 148 | 149 | return false; 150 | } 151 | 152 | /// Determines if a [DartType] is a `List` with the first type argument being a `Model`. 153 | bool isListOfModelType(InterfaceType t) { 154 | return const TypeChecker.fromRuntime(List).isAssignableFromType(t) && 155 | t.typeArguments.length == 1 && 156 | isModelClass(t.typeArguments[0]); 157 | } 158 | 159 | /// Determines if a [DartType] is a `Map` with the second type argument being a `Model`. 160 | bool isMapToModelType(InterfaceType t) { 161 | return const TypeChecker.fromRuntime(Map).isAssignableFromType(t) && 162 | t.typeArguments.length == 2 && 163 | isModelClass(t.typeArguments[1]); 164 | } 165 | 166 | bool isAssignableToModel(DartType type) => 167 | const TypeChecker.fromRuntime(Model).isAssignableFromType(type); 168 | 169 | /// Compute a [String] representation of a [type]. 170 | String typeToString(DartType type) { 171 | if (type is InterfaceType) { 172 | if (type.typeArguments.isEmpty) return type.name; 173 | return type.name + 174 | '<' + 175 | type.typeArguments.map(typeToString).join(', ') + 176 | '>'; 177 | } else { 178 | return type.name; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /angel_serialize_generator/lib/typescript.dart: -------------------------------------------------------------------------------- 1 | part of angel_serialize_generator; 2 | 3 | class TypeScriptDefinitionBuilder implements Builder { 4 | final bool autoSnakeCaseNames; 5 | 6 | const TypeScriptDefinitionBuilder({this.autoSnakeCaseNames = true}); 7 | 8 | @override 9 | Map> get buildExtensions { 10 | return { 11 | '.dart': ['.d.ts'] 12 | }; 13 | } 14 | 15 | Future compileToTypeScriptType( 16 | BuildContext ctx, 17 | String fieldName, 18 | DartType type, 19 | List refs, 20 | List ext, 21 | BuildStep buildStep) async { 22 | String typeScriptType = 'any'; 23 | 24 | var types = const { 25 | num: 'number', 26 | bool: 'boolean', 27 | String: 'string', 28 | Symbol: 'Symbol', 29 | }; 30 | 31 | types.forEach((t, tsType) { 32 | if (TypeChecker.fromRuntime(t).isAssignableFromType(type)) { 33 | typeScriptType = tsType; 34 | } 35 | }); 36 | 37 | if (type is InterfaceType) { 38 | if (isListOfModelType(type)) { 39 | var arg = await compileToTypeScriptType( 40 | ctx, fieldName, type.typeArguments[0], refs, ext, buildStep); 41 | typeScriptType = '$arg[]'; 42 | } else if (const TypeChecker.fromRuntime(Map) 43 | .isAssignableFromType(type) && 44 | type.typeArguments.length == 2) { 45 | var key = await compileToTypeScriptType( 46 | ctx, fieldName, type.typeArguments[0], refs, ext, buildStep); 47 | var value = await compileToTypeScriptType( 48 | ctx, fieldName, type.typeArguments[1], refs, ext, buildStep); 49 | //var modelType = type.typeArguments[1]; 50 | /*var innerCtx = await buildContext( 51 | modelType.element, 52 | ConstantReader( 53 | serializableTypeChecker.firstAnnotationOf(modelType.element)), 54 | buildStep, 55 | buildStep.resolver, 56 | autoSnakeCaseNames, 57 | true, 58 | );*/ 59 | 60 | typeScriptType = 61 | ctx.modelClassNameRecase.pascalCase + ReCase(fieldName).pascalCase; 62 | 63 | ext.add(CodeBuffer() 64 | ..writeln('interface $typeScriptType {') 65 | ..indent() 66 | ..writeln('[key: $key]: $value;') 67 | ..outdent() 68 | ..writeln('}')); 69 | } else if (const TypeChecker.fromRuntime(List) 70 | .isAssignableFromType(type)) { 71 | if (type.typeArguments.isEmpty) { 72 | typeScriptType = 'any[]'; 73 | } else { 74 | var arg = await compileToTypeScriptType( 75 | ctx, fieldName, type.typeArguments[0], refs, ext, buildStep); 76 | typeScriptType = '$arg[]'; 77 | } 78 | } else if (isModelClass(type)) { 79 | var sourcePath = buildStep.inputId.uri.toString(); 80 | var targetPath = type.element.source.uri.toString(); 81 | 82 | if (!p.equals(sourcePath, targetPath)) { 83 | var relative = p.relative(targetPath, from: sourcePath); 84 | String ref; 85 | 86 | if (type.element.source.uri.scheme == 'asset') { 87 | var id = AssetId.resolve(type.element.source.uri.toString()); 88 | if (id.package != buildStep.inputId.package) { 89 | ref = '/// '; 90 | } 91 | } 92 | 93 | if (ref == null) { 94 | // var relative = (p.dirname(targetPath) == p.dirname(sourcePath)) 95 | // ? p.basename(targetPath) 96 | // : p.relative(targetPath, from: sourcePath); 97 | var parent = p.dirname(relative); 98 | var filename = 99 | p.setExtension(p.basenameWithoutExtension(relative), '.d.ts'); 100 | relative = p.joinAll(p.split(parent).toList()..add(filename)); 101 | ref = '/// '; 102 | } 103 | if (!refs.contains(ref)) refs.add(ref); 104 | } 105 | 106 | var ctx = await buildContext( 107 | type.element, 108 | ConstantReader( 109 | serializableTypeChecker.firstAnnotationOf(type.element)), 110 | buildStep, 111 | buildStep.resolver, 112 | autoSnakeCaseNames, 113 | ); 114 | typeScriptType = ctx.modelClassNameRecase.pascalCase; 115 | } 116 | } 117 | 118 | return typeScriptType; 119 | } 120 | 121 | @override 122 | Future build(BuildStep buildStep) async { 123 | var contexts = []; 124 | LibraryReader lib; 125 | 126 | try { 127 | lib = LibraryReader(await buildStep.inputLibrary); 128 | } catch (_) { 129 | return; 130 | } 131 | 132 | var elements = []; 133 | 134 | try { 135 | elements = lib 136 | .annotatedWith(const TypeChecker.fromRuntime(Serializable)) 137 | .toList(); 138 | } catch (_) { 139 | // Ignore error in source_gen/build_runner that has no explanation 140 | } 141 | 142 | for (var element in elements) { 143 | if (element.element.kind != ElementKind.CLASS) { 144 | throw 'Only classes can be annotated with a @Serializable() annotation.'; 145 | } 146 | 147 | var annotation = element.annotation; 148 | 149 | var serializers = annotation.peek('serializers')?.listValue ?? []; 150 | 151 | if (serializers.isEmpty) continue; 152 | 153 | // Check if TypeScript serializer is supported 154 | if (!serializers.any((s) => s.toIntValue() == Serializers.typescript)) { 155 | continue; 156 | } 157 | 158 | contexts.add(await buildContext( 159 | element.element as ClassElement, 160 | element.annotation, 161 | buildStep, 162 | await buildStep.resolver, 163 | autoSnakeCaseNames != false)); 164 | } 165 | 166 | if (contexts.isEmpty) return; 167 | 168 | var refs = []; 169 | var buf = CodeBuffer( 170 | trailingNewline: true, 171 | sourceUrl: buildStep.inputId.uri, 172 | ); 173 | 174 | buf.writeln('// GENERATED CODE - DO NOT MODIFY BY HAND'); 175 | 176 | // declare module `foo` { 177 | buf 178 | ..writeln("declare module '${buildStep.inputId.package}' {") 179 | ..indent(); 180 | 181 | for (var ctx in contexts) { 182 | // interface Bar { ... } 183 | buf 184 | ..writeln('interface ${ctx.modelClassNameRecase.pascalCase} {') 185 | ..indent(); 186 | 187 | var ext = []; 188 | 189 | for (var field in ctx.fields) { 190 | // Skip excluded fields 191 | if (ctx.excluded[field.name]?.canSerialize == false) continue; 192 | 193 | var alias = ctx.resolveFieldName(field.name); 194 | var typeScriptType = await compileToTypeScriptType(ctx, field.name, 195 | ctx.resolveSerializedFieldType(field.name), refs, ext, buildStep); 196 | 197 | // foo: string; 198 | if (!ctx.requiredFields.containsKey(field.name)) alias += '?'; 199 | buf.writeln('$alias: $typeScriptType;'); 200 | } 201 | 202 | buf 203 | ..outdent() 204 | ..writeln('}'); 205 | 206 | for (var b in ext) { 207 | b.copyInto(buf); 208 | } 209 | } 210 | 211 | buf 212 | ..outdent() 213 | ..writeln('}'); 214 | var finalBuf = CodeBuffer(); 215 | refs.forEach(finalBuf.writeln); 216 | buf.copyInto(finalBuf); 217 | 218 | await buildStep.writeAsString( 219 | buildStep.inputId.changeExtension('.d.ts'), 220 | finalBuf.toString(), 221 | ); 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /angel_serialize_generator/lib/build_context.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:analyzer/dart/constant/value.dart'; 3 | import 'package:analyzer/dart/element/element.dart'; 4 | import 'package:analyzer/dart/element/type.dart'; 5 | import 'package:analyzer/src/dart/element/element.dart'; 6 | import 'package:angel_serialize/angel_serialize.dart'; 7 | import 'package:build/build.dart'; 8 | import 'package:meta/meta.dart'; 9 | import 'package:path/path.dart' as p; 10 | import 'package:recase/recase.dart'; 11 | import 'package:source_gen/source_gen.dart'; 12 | import 'context.dart'; 13 | 14 | // ignore: deprecated_member_use 15 | const TypeChecker aliasTypeChecker = TypeChecker.fromRuntime(Alias); 16 | 17 | const TypeChecker dateTimeTypeChecker = TypeChecker.fromRuntime(DateTime); 18 | 19 | // ignore: deprecated_member_use 20 | const TypeChecker excludeTypeChecker = TypeChecker.fromRuntime(Exclude); 21 | 22 | const TypeChecker serializableFieldTypeChecker = 23 | TypeChecker.fromRuntime(SerializableField); 24 | 25 | const TypeChecker serializableTypeChecker = 26 | TypeChecker.fromRuntime(Serializable); 27 | 28 | const TypeChecker generatedSerializableTypeChecker = 29 | TypeChecker.fromRuntime(GeneratedSerializable); 30 | 31 | final Map _cache = {}; 32 | 33 | /// Create a [BuildContext]. 34 | Future buildContext(ClassElement clazz, ConstantReader annotation, 35 | BuildStep buildStep, Resolver resolver, bool autoSnakeCaseNames, 36 | {bool heedExclude = true}) async { 37 | var id = clazz.location.components.join('-'); 38 | if (_cache.containsKey(id)) { 39 | return _cache[id]; 40 | } 41 | 42 | // Check for autoIdAndDateFields, autoSnakeCaseNames 43 | autoSnakeCaseNames = 44 | annotation.peek('autoSnakeCaseNames')?.boolValue ?? autoSnakeCaseNames; 45 | 46 | var ctx = BuildContext( 47 | annotation, 48 | clazz, 49 | originalClassName: clazz.name, 50 | sourceFilename: p.basename(buildStep.inputId.path), 51 | autoSnakeCaseNames: autoSnakeCaseNames, 52 | includeAnnotations: 53 | annotation.peek('includeAnnotations')?.listValue ?? [], 54 | ); 55 | // var lib = await resolver.libraryFor(buildStep.inputId); 56 | List fieldNames = []; 57 | var fields = []; 58 | 59 | // Crawl for classes from parent classes. 60 | void crawlClass(InterfaceType t) { 61 | while (t != null) { 62 | fields.insertAll(0, t.element.fields); 63 | t.interfaces.forEach(crawlClass); 64 | t = t.superclass; 65 | } 66 | } 67 | 68 | crawlClass(clazz.type); 69 | 70 | for (var field in fields) { 71 | // Skip private fields 72 | if (field.name.startsWith('_')) { 73 | continue; 74 | } 75 | 76 | if (field.getter != null && 77 | (field.setter != null || field.getter.isAbstract)) { 78 | var el = field.setter == null ? field.getter : field; 79 | fieldNames.add(field.name); 80 | 81 | // Check for @SerializableField 82 | var fieldAnn = serializableFieldTypeChecker.firstAnnotationOf(el); 83 | 84 | void handleSerializableField(SerializableFieldMirror sField) { 85 | ctx.fieldInfo[field.name] = sField; 86 | 87 | if (sField.defaultValue != null) { 88 | ctx.defaults[field.name] = sField.defaultValue; 89 | } 90 | 91 | if (sField.alias != null) { 92 | ctx.aliases[field.name] = sField.alias; 93 | } else if (autoSnakeCaseNames != false) { 94 | ctx.aliases[field.name] = ReCase(field.name).snakeCase; 95 | } 96 | 97 | if (sField.isNullable == false) { 98 | var reason = sField.errorMessage ?? 99 | "Missing required field '${ctx.resolveFieldName(field.name)}' on ${ctx.modelClassName}."; 100 | ctx.requiredFields[field.name] = reason; 101 | } 102 | 103 | if (sField.exclude) { 104 | // ignore: deprecated_member_use 105 | ctx.excluded[field.name] = Exclude( 106 | canSerialize: sField.canSerialize, 107 | canDeserialize: sField.canDeserialize, 108 | ); 109 | } 110 | } 111 | 112 | if (fieldAnn != null) { 113 | var cr = ConstantReader(fieldAnn); 114 | var excluded = cr.peek('exclude')?.boolValue ?? false; 115 | var sField = SerializableFieldMirror( 116 | alias: cr.peek('alias')?.stringValue, 117 | defaultValue: cr.peek('defaultValue')?.objectValue, 118 | serializer: cr.peek('serializer')?.symbolValue, 119 | deserializer: cr.peek('deserializer')?.symbolValue, 120 | errorMessage: cr.peek('errorMessage')?.stringValue, 121 | isNullable: cr.peek('isNullable')?.boolValue ?? !excluded, 122 | canDeserialize: cr.peek('canDeserialize')?.boolValue ?? false, 123 | canSerialize: cr.peek('canSerialize')?.boolValue ?? false, 124 | exclude: excluded, 125 | serializesTo: cr.peek('serializesTo')?.typeValue, 126 | ); 127 | 128 | handleSerializableField(sField); 129 | 130 | // Apply 131 | } else { 132 | var foundNone = true; 133 | // Skip if annotated with @exclude 134 | var excludeAnnotation = excludeTypeChecker.firstAnnotationOf(el); 135 | 136 | if (excludeAnnotation != null) { 137 | var cr = ConstantReader(excludeAnnotation); 138 | foundNone = false; 139 | 140 | // ignore: deprecated_member_use 141 | ctx.excluded[field.name] = Exclude( 142 | canSerialize: cr.read('canSerialize').boolValue, 143 | canDeserialize: cr.read('canDeserialize').boolValue, 144 | ); 145 | } 146 | 147 | // Check for @DefaultValue() 148 | var defAnn = 149 | // ignore: deprecated_member_use 150 | const TypeChecker.fromRuntime(DefaultValue).firstAnnotationOf(el); 151 | if (defAnn != null) { 152 | var rev = ConstantReader(defAnn).revive().positionalArguments[0]; 153 | ctx.defaults[field.name] = rev; 154 | foundNone = false; 155 | } 156 | 157 | // Check for alias 158 | // ignore: deprecated_member_use 159 | Alias alias; 160 | var aliasAnn = aliasTypeChecker.firstAnnotationOf(el); 161 | 162 | if (aliasAnn != null) { 163 | // ignore: deprecated_member_use 164 | alias = Alias(aliasAnn.getField('name').toStringValue()); 165 | foundNone = false; 166 | } 167 | 168 | if (alias?.name?.isNotEmpty == true) { 169 | ctx.aliases[field.name] = alias.name; 170 | } else if (autoSnakeCaseNames != false) { 171 | ctx.aliases[field.name] = ReCase(field.name).snakeCase; 172 | } 173 | 174 | // Check for @required 175 | var required = 176 | const TypeChecker.fromRuntime(Required).firstAnnotationOf(el); 177 | 178 | if (required != null) { 179 | log.warning( 180 | 'Using @required on fields (like ${clazz.name}.${field.name}) is now deprecated; use @SerializableField(isNullable: false) instead.'); 181 | var cr = ConstantReader(required); 182 | var reason = cr.peek('reason')?.stringValue ?? 183 | "Missing required field '${ctx.resolveFieldName(field.name)}' on ${ctx.modelClassName}."; 184 | ctx.requiredFields[field.name] = reason; 185 | foundNone = false; 186 | } 187 | 188 | if (foundNone) { 189 | var f = SerializableField(); 190 | var sField = SerializableFieldMirror( 191 | alias: f.alias, 192 | defaultValue: null, 193 | serializer: f.serializer, 194 | deserializer: f.deserializer, 195 | errorMessage: f.errorMessage, 196 | isNullable: f.isNullable, 197 | canDeserialize: f.canDeserialize, 198 | canSerialize: f.canSerialize, 199 | exclude: f.exclude, 200 | serializesTo: null, 201 | ); 202 | handleSerializableField(sField); 203 | } 204 | } 205 | 206 | ctx.fields.add(field); 207 | } 208 | } 209 | 210 | // ShimFields are no longer used. 211 | // if (const TypeChecker.fromRuntime(Model).isAssignableFromType(clazz.type)) { 212 | // if (!fieldNames.contains('id')) { 213 | // var idField = ShimFieldImpl('id', lib.context.typeProvider.stringType); 214 | // ctx.fields.insert(0, idField); 215 | // ctx.shimmed['id'] = true; 216 | // } 217 | 218 | // DartType dateTime; 219 | // for (var key in ['createdAt', 'updatedAt']) { 220 | // if (!fieldNames.contains(key)) { 221 | // if (dateTime == null) { 222 | // var coreLib = 223 | // await resolver.libraries.singleWhere((lib) => lib.isDartCore); 224 | // var dt = coreLib.getType('DateTime'); 225 | // dateTime = dt.type; 226 | // } 227 | 228 | // var field = ShimFieldImpl(key, dateTime); 229 | // ctx.aliases[key] = ReCase(key).snakeCase; 230 | // ctx.fields.add(field); 231 | // ctx.shimmed[key] = true; 232 | // } 233 | // } 234 | // } 235 | 236 | // Get constructor params, if any 237 | ctx.constructorParameters.addAll(clazz.unnamedConstructor.parameters); 238 | 239 | return ctx; 240 | } 241 | 242 | /// A manually-instantiated [FieldElement]. 243 | class ShimFieldImpl extends FieldElementImpl { 244 | @override 245 | final DartType type; 246 | 247 | ShimFieldImpl(String name, this.type) : super(name, -1); 248 | } 249 | -------------------------------------------------------------------------------- /angel_serialize_generator/lib/model.dart: -------------------------------------------------------------------------------- 1 | part of angel_serialize_generator; 2 | 3 | class JsonModelGenerator extends GeneratorForAnnotation { 4 | const JsonModelGenerator(); 5 | 6 | @override 7 | Future generateForAnnotatedElement( 8 | Element element, ConstantReader annotation, BuildStep buildStep) async { 9 | if (element.kind != ElementKind.CLASS) { 10 | throw 'Only classes can be annotated with a @Serializable() annotation.'; 11 | } 12 | 13 | var ctx = await buildContext(element as ClassElement, annotation, buildStep, 14 | await buildStep.resolver, true); 15 | 16 | var lib = Library((b) { 17 | generateClass(ctx, b, annotation); 18 | }); 19 | 20 | var buf = lib.accept(DartEmitter()); 21 | return buf.toString(); 22 | } 23 | 24 | /// Generate an extended model class. 25 | void generateClass( 26 | BuildContext ctx, LibraryBuilder file, ConstantReader annotation) { 27 | file.body.add(Class((clazz) { 28 | clazz 29 | ..name = ctx.modelClassNameRecase.pascalCase 30 | ..annotations.add(refer('generatedSerializable')); 31 | 32 | for (var ann in ctx.includeAnnotations) { 33 | clazz.annotations.add(convertObject(ann)); 34 | } 35 | 36 | if (shouldBeConstant(ctx)) { 37 | clazz.implements.add(Reference(ctx.originalClassName)); 38 | } else { 39 | clazz.extend = Reference(ctx.originalClassName); 40 | } 41 | 42 | //if (ctx.importsPackageMeta) 43 | // clazz.annotations.add(CodeExpression(Code('immutable'))); 44 | 45 | for (var field in ctx.fields) { 46 | clazz.fields.add(Field((b) { 47 | b 48 | ..name = field.name 49 | // ..modifier = FieldModifier.final$ 50 | ..annotations.add(CodeExpression(Code('override'))) 51 | ..type = convertTypeReference(field.type); 52 | 53 | // Fields should only be forced-final if the original field has no setter. 54 | if (field.setter == null && field is! ShimFieldImpl) { 55 | b.modifier = FieldModifier.final$; 56 | } 57 | 58 | for (var el in [field.getter, field]) { 59 | if (el?.documentationComment != null) { 60 | b.docs.addAll(el.documentationComment.split('\n')); 61 | } 62 | } 63 | })); 64 | } 65 | 66 | generateConstructor(ctx, clazz, file); 67 | generateCopyWithMethod(ctx, clazz, file); 68 | generateEqualsOperator(ctx, clazz, file); 69 | generateHashCode(ctx, clazz); 70 | generateToString(ctx, clazz); 71 | 72 | // Generate toJson() method if necessary 73 | var serializers = annotation.peek('serializers')?.listValue ?? []; 74 | 75 | if (serializers.any((o) => o.toIntValue() == Serializers.json)) { 76 | clazz.methods.add(Method((method) { 77 | method 78 | ..name = 'toJson' 79 | ..returns = Reference('Map') 80 | ..body = Code('return ${clazz.name}Serializer.toMap(this);'); 81 | })); 82 | } 83 | })); 84 | } 85 | 86 | bool shouldBeConstant(BuildContext ctx) { 87 | // Check if all fields are without a getter 88 | return !isAssignableToModel(ctx.clazz.type) && 89 | ctx.clazz.fields.every((f) => 90 | f.getter?.isAbstract != false && f.setter?.isAbstract != false); 91 | } 92 | 93 | /// Generate a constructor with named parameters. 94 | void generateConstructor( 95 | BuildContext ctx, ClassBuilder clazz, LibraryBuilder file) { 96 | clazz.constructors.add(Constructor((constructor) { 97 | // Add all `super` params 98 | constructor.constant = (ctx.clazz.unnamedConstructor?.isConst == true || 99 | shouldBeConstant(ctx)) && 100 | ctx.fields.every((f) { 101 | return f.setter == null && f is! ShimFieldImpl; 102 | }); 103 | 104 | for (var param in ctx.constructorParameters) { 105 | constructor.requiredParameters.add(Parameter((b) => b 106 | ..name = param.name 107 | ..type = convertTypeReference(param.type))); 108 | } 109 | 110 | for (var field in ctx.fields) { 111 | if (!shouldBeConstant(ctx) && isListOrMapType(field.type)) { 112 | String typeName = const TypeChecker.fromRuntime(List) 113 | .isAssignableFromType(field.type) 114 | ? 'List' 115 | : 'Map'; 116 | var defaultValue = typeName == 'List' ? '[]' : '{}'; 117 | var existingDefault = ctx.defaults[field.name]; 118 | 119 | if (existingDefault != null) { 120 | defaultValue = dartObjectToString(existingDefault); 121 | } 122 | 123 | constructor.initializers.add(Code(''' 124 | this.${field.name} = 125 | $typeName.unmodifiable(${field.name} ?? $defaultValue)''')); 126 | } 127 | } 128 | 129 | for (var field in ctx.fields) { 130 | constructor.optionalParameters.add(Parameter((b) { 131 | b 132 | ..toThis = shouldBeConstant(ctx) 133 | ..name = field.name 134 | ..named = true; 135 | 136 | var existingDefault = ctx.defaults[field.name]; 137 | 138 | if (existingDefault != null) { 139 | b.defaultTo = Code(dartObjectToString(existingDefault)); 140 | } 141 | 142 | if (!isListOrMapType(field.type)) { 143 | b.toThis = true; 144 | } else if (!b.toThis) { 145 | b.type = convertTypeReference(field.type); 146 | } 147 | 148 | if (ctx.requiredFields.containsKey(field.name) && 149 | b.defaultTo == null) { 150 | b.annotations.add(CodeExpression(Code('required'))); 151 | } 152 | })); 153 | } 154 | 155 | if (ctx.constructorParameters.isNotEmpty) { 156 | if (!shouldBeConstant(ctx) || 157 | ctx.clazz.unnamedConstructor?.isConst == true) { 158 | constructor.initializers.add(Code( 159 | 'super(${ctx.constructorParameters.map((p) => p.name).join(',')})')); 160 | } 161 | } 162 | })); 163 | } 164 | 165 | /// Generate a `copyWith` method. 166 | void generateCopyWithMethod( 167 | BuildContext ctx, ClassBuilder clazz, LibraryBuilder file) { 168 | clazz.methods.add(Method((method) { 169 | method 170 | ..name = 'copyWith' 171 | ..returns = ctx.modelClassType; 172 | 173 | // Add all `super` params 174 | if (ctx.constructorParameters.isNotEmpty) { 175 | for (var param in ctx.constructorParameters) { 176 | method.requiredParameters.add(Parameter((b) => b 177 | ..name = param.name 178 | ..type = convertTypeReference(param.type))); 179 | } 180 | } 181 | 182 | var buf = StringBuffer('return ${ctx.modelClassName}('); 183 | int i = 0; 184 | 185 | for (var param in ctx.constructorParameters) { 186 | if (i++ > 0) buf.write(', '); 187 | buf.write(param.name); 188 | } 189 | 190 | // Add named parameters 191 | for (var field in ctx.fields) { 192 | method.optionalParameters.add(Parameter((b) { 193 | b 194 | ..name = field.name 195 | ..named = true 196 | ..type = convertTypeReference(field.type); 197 | })); 198 | 199 | if (i++ > 0) buf.write(', '); 200 | buf.write('${field.name}: ${field.name} ?? this.${field.name}'); 201 | } 202 | 203 | buf.write(');'); 204 | method.body = Code(buf.toString()); 205 | })); 206 | } 207 | 208 | static String generateEquality(DartType type, [bool nullable = false]) { 209 | if (type is InterfaceType) { 210 | if (const TypeChecker.fromRuntime(List).isAssignableFromType(type)) { 211 | if (type.typeParameters.length == 1) { 212 | var eq = generateEquality(type.typeArguments[0]); 213 | return 'ListEquality<${type.typeArguments[0].name}>($eq)'; 214 | } else { 215 | return 'ListEquality()'; 216 | } 217 | } else if (const TypeChecker.fromRuntime(Map) 218 | .isAssignableFromType(type)) { 219 | if (type.typeParameters.length == 2) { 220 | var keq = generateEquality(type.typeArguments[0]), 221 | veq = generateEquality(type.typeArguments[1]); 222 | return 'MapEquality<${type.typeArguments[0].name}, ${type.typeArguments[1].name}>(keys: $keq, values: $veq)'; 223 | } else { 224 | return 'MapEquality()'; 225 | } 226 | } 227 | 228 | return nullable ? null : 'DefaultEquality<${type.name}>()'; 229 | } else { 230 | return 'DefaultEquality()'; 231 | } 232 | } 233 | 234 | static String Function(String, String) generateComparator(DartType type) { 235 | if (type is! InterfaceType || type.name == 'dynamic') { 236 | return (a, b) => '$a == $b'; 237 | } 238 | var eq = generateEquality(type, true); 239 | if (eq == null) return (a, b) => '$a == $b'; 240 | return (a, b) => '$eq.equals($a, $b)'; 241 | } 242 | 243 | void generateHashCode(BuildContext ctx, ClassBuilder clazz) { 244 | clazz 245 | ..methods.add(Method((method) { 246 | method 247 | ..name = 'hashCode' 248 | ..type = MethodType.getter 249 | ..returns = refer('int') 250 | ..annotations.add(refer('override')) 251 | ..body = refer('hashObjects') 252 | .call([literalList(ctx.fields.map((f) => refer(f.name)))]) 253 | .returned 254 | .statement; 255 | })); 256 | } 257 | 258 | void generateToString(BuildContext ctx, ClassBuilder clazz) { 259 | clazz.methods.add(Method((b) { 260 | b 261 | ..name = 'toString' 262 | ..returns = refer('String') 263 | ..annotations.add(refer('override')) 264 | ..body = Block((b) { 265 | var buf = StringBuffer('\"${ctx.modelClassName}('); 266 | var i = 0; 267 | for (var field in ctx.fields) { 268 | if (i++ > 0) buf.write(', '); 269 | buf.write('${field.name}=\$${field.name}'); 270 | } 271 | buf.write(')\"'); 272 | b.addExpression(CodeExpression(Code(buf.toString())).returned); 273 | }); 274 | })); 275 | } 276 | 277 | void generateEqualsOperator( 278 | BuildContext ctx, ClassBuilder clazz, LibraryBuilder file) { 279 | clazz.methods.add(Method((method) { 280 | method 281 | ..name = 'operator ==' 282 | ..returns = Reference('bool') 283 | ..requiredParameters.add(Parameter((b) => b.name = 'other')); 284 | 285 | var buf = ['other is ${ctx.originalClassName}']; 286 | 287 | buf.addAll(ctx.fields.map((f) { 288 | return generateComparator(f.type)('other.${f.name}', f.name); 289 | })); 290 | 291 | method.body = Code('return ${buf.join('&&')};'); 292 | })); 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # serialize 2 | 3 | [![Pub](https://img.shields.io/pub/v/angel_serialize.svg)](https://pub.dartlang.org/packages/angel_serialize) 4 | [![build status](https://travis-ci.org/angel-dart/serialize.svg)](https://travis-ci.org/angel-dart/serialize) 5 | 6 | Source-generated serialization for Dart objects. This package uses `package:source_gen` to eliminate 7 | the time you spend writing boilerplate serialization code for your models. 8 | `package:angel_serialize` also powers `package:angel_orm`. 9 | 10 | - [Usage](#usage) 11 | - [Models](#models) 12 | - [Subclasses](#subclasses) 13 | - [Field Aliases](#aliases) 14 | - [Excluding Keys](#excluding-keys) 15 | - [Required Fields](#required-fields) 16 | - [Adding Annotations to Generated Classes](#adding-annotations-to-generated-classes) 17 | - [Custom Serializers](#custom-serializers) 18 | - [Serialization](#serializaition) 19 | - [Nesting](#nesting) 20 | - [ID and Date Fields](#id-and-dates) 21 | - [Binary Data](#binary-data) 22 | - [TypeScript Definition Generator](#typescript-definitions) 23 | - [Constructor Parameters](#constructor-parameters) 24 | 25 | # Usage 26 | 27 | In your `pubspec.yaml`, you need to install the following dependencies: 28 | 29 | ```yaml 30 | dependencies: 31 | angel_model: ^1.0.0 32 | angel_serialize: ^2.0.0 33 | dev_dependencies: 34 | angel_serialize_generator: ^2.0.0 35 | build_runner: ^1.0.0 36 | ``` 37 | 38 | With the recent updates to `package:build_runner`, you can build models automatically, 39 | anywhere in your project structure, 40 | by running `pub run build_runner build`. 41 | 42 | To tweak this: 43 | https://pub.dartlang.org/packages/build_config 44 | 45 | If you want to watch for file changes and re-build when necessary, replace the `build` call 46 | with a call to `watch`. They take the same parameters. 47 | 48 | # Models 49 | 50 | There are a few changes opposed to normal Model classes. You need to add a `@serializable` annotation to your model 51 | class to have it serialized, and a serializable model class's name should also start 52 | with a leading underscore. 53 | 54 | In addition, you may consider using an `abstract` class to ensure immutability 55 | of models. 56 | 57 | Rather you writing the public class, `angel_serialize` does it for you. This means that the main class can have 58 | its constructors automatically generated, in addition into serialization functions. 59 | 60 | For example, say we have a `Book` model. Create a class named `_Book`: 61 | 62 | ```dart 63 | library angel_serialize.test.models.book; 64 | 65 | import 'package:angel_model/angel_model.dart'; 66 | import 'package:angel_serialize/angel_serialize.dart'; 67 | import 'package:collection/collection.dart'; 68 | part 'book.g.dart'; 69 | 70 | @serializable 71 | abstract class _Book extends Model { 72 | String get author; 73 | 74 | @SerializableField(defaultValue: '[Untitled]') 75 | String get title; 76 | 77 | String get description; 78 | 79 | int get pageCount; 80 | 81 | BookType get type; 82 | } 83 | 84 | /// It even supports enums! 85 | enum BookType { 86 | fiction, 87 | nonFiction 88 | } 89 | ``` 90 | 91 | The following file will be generated: 92 | 93 | - `book.g.dart` 94 | 95 | Producing these classes: 96 | 97 | - `Book`: Extends or implements `_Book`; may be `const`-enabled. 98 | - `BookSerializer`: static functionality for serializing `Book` models. 99 | - `BookFields`: The names of all fields from the `Book` model, statically-available. 100 | - `BookEncoder`: Allows `BookSerializer` to extend `Codec`. 101 | - `BookDecoder`: Also allows `BookSerializer` to extend `Codec`. 102 | 103 | And the following other features: 104 | - `bookSerializer`: A top-level, `const` instance of `BookSerializer`. 105 | - `Book.toString`: Prints out all of a `Book` instance's fields. 106 | 107 | # Serialization 108 | 109 | You can use the generated files as follows: 110 | 111 | ```dart 112 | myFunction() { 113 | var warAndPeace = new Book( 114 | author: 'Leo Tolstoy', 115 | title: 'War and Peace', 116 | description: 'You will cry after reading this.', 117 | pageCount: 1225 118 | ); 119 | 120 | // Easily serialize models into Maps 121 | var map = BookSerializer.toMap(warAndPeace); 122 | 123 | // Also deserialize from Maps 124 | var book = BookSerializer.fromMap(map); 125 | print(book.title); // 'War and Peace' 126 | 127 | // For compatibility with `JSON.encode`, a `toJson` method 128 | // is included that forwards to `BookSerializer.toMap`: 129 | expect(book.toJson(), map); 130 | 131 | // Generated classes act as value types, and thus can be compared. 132 | expect(BookSerializer.fromMap(map), equals(warAndPeace)); 133 | } 134 | ``` 135 | 136 | As of `2.0.2`, the generated output also includes information 137 | about the serialized names of keys on your model class. 138 | 139 | ```dart 140 | myOtherFunction() { 141 | // Relying on the serialized key of a field? No worries. 142 | map[BookFields.author] = 'Zora Neale Hurston'; 143 | } 144 | } 145 | ``` 146 | 147 | ## Customizing Serialization 148 | 149 | Currently, these serialization methods are supported: 150 | 151 | - to `Map` 152 | - to JSON 153 | - to TypeScript definitions 154 | 155 | You can customize these by means of `serializers`: 156 | 157 | ```dart 158 | @Serializable(serializers: const [Serializers.map, Serializers.json]) 159 | class _MyClass extends Model {} 160 | ``` 161 | 162 | ## Subclasses 163 | `angel_serialize` pulls in fields from parent classes, as well as 164 | implemented interfaces, so it is extremely easy to share attributes among 165 | model classes: 166 | 167 | ```dart 168 | import 'package:angel_serialize/angel_serialize.dart'; 169 | part 'subclass.g.dart'; 170 | 171 | @serializable 172 | class _Animal { 173 | @notNull 174 | String genus; 175 | @notNull 176 | String species; 177 | } 178 | 179 | @serializable 180 | class _Bird extends _Animal { 181 | @DefaultsTo(false) 182 | bool isSparrow; 183 | } 184 | 185 | var saxaulSparrow = Bird( 186 | genus: 'Passer', 187 | species: 'ammodendri', 188 | isSparrow: true, 189 | ); 190 | ``` 191 | 192 | ## Aliases 193 | 194 | Whereas Dart fields conventionally are camelCased, most database columns 195 | tend to be snake_cased. This is not a problem, because we can define an alias 196 | for a field. 197 | 198 | By default `angel_serialize` will transform keys into snake case. Use `alias` to 199 | provide a custom name, or pass `autoSnakeCaseNames`: `false` to the builder; 200 | 201 | ```dart 202 | @serializable 203 | abstract class _Spy extends Model { 204 | /// Will show up as 'agency_id' in serialized JSON. 205 | /// 206 | /// When deserializing JSON, instead of searching for an 'agencyId' key, 207 | /// it will use 'agency_id'. 208 | /// 209 | /// Hooray! 210 | String agencyId; 211 | 212 | @SerializableField(alias: 'foo') 213 | String someOtherField; 214 | } 215 | ``` 216 | 217 | You can also override `autoSnakeCaseNames` per model: 218 | 219 | ```dart 220 | @Serializable(autoSnakeCaseNames: false) 221 | abstract class _OtherCasing extends Model { 222 | String camelCasedField; 223 | } 224 | ``` 225 | 226 | ## Excluding Keys 227 | 228 | In pratice, there may keys that you want to exclude from JSON. 229 | To accomplish this, simply annotate them with `@exclude`: 230 | 231 | ```dart 232 | @serializable 233 | abstract class _Whisper extends Model { 234 | /// Will never be serialized to JSON 235 | @SerializableField(exclude: true) 236 | String secret; 237 | } 238 | ``` 239 | 240 | There are times, however, when you want to only exclude either serialization 241 | or deserialization, but not both. For example, you might want to deserialize 242 | passwords from a database without sending them to users as JSON. 243 | 244 | In this case, use `canSerialize` or `canDeserialize`: 245 | 246 | ```dart 247 | @serializable 248 | abstract class _Whisper extends Model { 249 | /// Will never be serialized to JSON 250 | /// 251 | /// ... But it can be deserialized 252 | @SerializableField(exclude: true, canDeserialize: true) 253 | String secret; 254 | } 255 | ``` 256 | 257 | ## Required Fields 258 | 259 | It is easy to mark a field as required: 260 | 261 | ```dart 262 | @serializable 263 | abstract class _Foo extends Model { 264 | @SerializableField(isNullable: false) 265 | int myRequiredInt; 266 | 267 | @SerializableField(isNullable: false, errorMessage: 'Custom message') 268 | int myOtherRequiredInt; 269 | } 270 | ``` 271 | 272 | The given field will be marked as `@required` in the 273 | generated constructor, and serializers will check for its 274 | presence, throwing a `FormatException` if it is missing. 275 | 276 | ## Adding Annotations to Generated Classes 277 | There are times when you need the generated class to have annotations affixed to it: 278 | 279 | ```dart 280 | @Serializable( 281 | includeAnnotations: [ 282 | Deprecated('blah blah blah'), 283 | pragma('something...'), 284 | ] 285 | ) 286 | abstract class _Foo extends Model {} 287 | ``` 288 | 289 | ## Custom Serializers 290 | `package:angel_serialize` does not cover every known Dart data type; you can add support for your own. 291 | Provide `serializer` and `deserializer` arguments to `@SerializableField()` as you see fit. 292 | 293 | They are typically used together. Note that the argument to `deserializer` will always be 294 | `dynamic`, while `serializer` can receive the data type in question. 295 | 296 | In such a case, you might want to also provide a `serializesTo` argument. 297 | This lets the generator, as well as the ORM, apply the correct (de)serialization rules 298 | and validations. 299 | 300 | ```dart 301 | DateTime _dateFromString(s) => s is String ? HttpDate.parse(s) : null; 302 | String _dateToString(DateTime v) => v == null ? null : HttpDate.format(v); 303 | 304 | @serializable 305 | abstract class _HttpRequest { 306 | @SerializableField( 307 | serializer: #_dateToString, 308 | deserializer: #_dateFromString, 309 | serializesTo: String) 310 | DateTime date; 311 | } 312 | ``` 313 | 314 | # Nesting 315 | 316 | `angel_serialize` also supports a few types of nesting of `@serializable` classes: 317 | 318 | - As a class member, ex. `Book myField` 319 | - As the type argument to a `List`, ex. `List` 320 | - As the second type argument to a `Map`, ex. `Map` 321 | 322 | In other words, the following are all legal, and will be serialized/deserialized. 323 | You can use either the underscored name of a child class (ex. `_Book`), or the 324 | generated class name (ex `Book`): 325 | 326 | ```dart 327 | @serializable 328 | abstract class _Author extends Model { 329 | List books; 330 | Book newestBook; 331 | Map booksByIsbn; 332 | } 333 | ``` 334 | 335 | If your model (`Author`) depends on a model defined in another file (`Book`), 336 | then you will need to generate `book.g.dart` before, `author.g.dart`, 337 | **in a separate build action**. This way, the analyzer can resolve the `Book` type. 338 | 339 | # ID and Dates 340 | 341 | This package will automatically generate `id`, `createdAt`, and `updatedAt` fields for you, 342 | in the style of an Angel `Model`. This will automatically be generated, **only** for classes 343 | extending `Model`. 344 | 345 | # Binary Data 346 | 347 | `package:angel_serialize` also handles `Uint8List` fields, by means of serialization to 348 | and from `base64` encoding. 349 | 350 | # TypeScript Definitions 351 | 352 | It is quite common to build frontends with JavaScript and/or TypeScript, 353 | so why not generate typings as well? 354 | 355 | To accomplish this, add `Serializers.typescript` to your `@Serializable()` declaration: 356 | 357 | ```dart 358 | @Serializable(serializers: const [Serializers.map, Serializers.json, Serializers.typescript]) 359 | class _Foo extends Model {} 360 | ``` 361 | 362 | The aforementioned `_Author` class will generate the following in `author.d.ts`: 363 | 364 | ```typescript 365 | interface Author { 366 | id: string; 367 | name: string; 368 | age: number; 369 | books: Book[]; 370 | newest_book: Book; 371 | created_at: any; 372 | updated_at: any; 373 | } 374 | interface Library { 375 | id: string; 376 | collection: BookCollection; 377 | created_at: any; 378 | updated_at: any; 379 | } 380 | interface BookCollection { 381 | [key: string]: Book; 382 | } 383 | ``` 384 | 385 | Fields with an `@Exclude()` that specifies `canSerialize: false` will not be present in the 386 | TypeScript definition. The rationale for this is that if a field (i.e. `password`) will 387 | never be sent to the client, the client shouldn't even know the field exists. 388 | 389 | # Constructor Parameters 390 | 391 | Sometimes, you may need to have custom constructor parameters, for example, when 392 | using depedency injection frameworks. For these cases, `angel_serialize` can forward 393 | custom constructor parameters. 394 | 395 | The following: 396 | 397 | ```dart 398 | @serializable 399 | abstract class _Bookmark extends _BookmarkBase { 400 | @SerializableField(exclude: true) 401 | final Book book; 402 | 403 | int get page; 404 | String get comment; 405 | 406 | _Bookmark(this.book); 407 | } 408 | ``` 409 | 410 | Generates: 411 | 412 | ```dart 413 | class Bookmark extends _Bookmark { 414 | Bookmark(Book book, 415 | {this.id, 416 | this.page, 417 | this.comment, 418 | this.createdAt, 419 | this.updatedAt}) 420 | : super(book); 421 | 422 | @override 423 | final String id; 424 | 425 | // ... 426 | } 427 | ``` 428 | -------------------------------------------------------------------------------- /angel_serialize_generator/lib/serialize.dart: -------------------------------------------------------------------------------- 1 | part of angel_serialize_generator; 2 | 3 | class SerializerGenerator extends GeneratorForAnnotation { 4 | final bool autoSnakeCaseNames; 5 | 6 | const SerializerGenerator({this.autoSnakeCaseNames = true}); 7 | 8 | @override 9 | Future generateForAnnotatedElement( 10 | Element element, ConstantReader annotation, BuildStep buildStep) async { 11 | if (element.kind != ElementKind.CLASS) { 12 | throw 'Only classes can be annotated with a @Serializable() annotation.'; 13 | } 14 | 15 | var ctx = await buildContext(element as ClassElement, annotation, buildStep, 16 | await buildStep.resolver, autoSnakeCaseNames != false); 17 | 18 | var serializers = annotation.peek('serializers')?.listValue ?? []; 19 | 20 | if (serializers.isEmpty) return null; 21 | 22 | // Check if any serializer is recognized 23 | if (!serializers.any((s) => Serializers.all.contains(s.toIntValue()))) { 24 | return null; 25 | } 26 | 27 | var lib = Library((b) { 28 | generateClass(serializers.map((s) => s.toIntValue()).toList(), ctx, b); 29 | generateFieldsClass(ctx, b); 30 | }); 31 | 32 | var buf = lib.accept(DartEmitter()); 33 | return buf.toString(); 34 | } 35 | 36 | /// Generate a serializer class. 37 | void generateClass( 38 | List serializers, BuildContext ctx, LibraryBuilder file) { 39 | // Generate canonical codecs, etc. 40 | var pascal = ctx.modelClassNameRecase.pascalCase, 41 | camel = ctx.modelClassNameRecase.camelCase; 42 | 43 | if (ctx.constructorParameters.isEmpty) { 44 | file.body.add(Code(''' 45 | const ${pascal}Serializer ${camel}Serializer = ${pascal}Serializer(); 46 | 47 | class ${pascal}Encoder extends Converter<${pascal}, Map> { 48 | const ${pascal}Encoder(); 49 | 50 | @override 51 | Map convert(${pascal} model) => ${pascal}Serializer.toMap(model); 52 | } 53 | 54 | class ${pascal}Decoder extends Converter { 55 | const ${pascal}Decoder(); 56 | 57 | @override 58 | ${pascal} convert(Map map) => ${pascal}Serializer.fromMap(map); 59 | } 60 | ''')); 61 | } 62 | 63 | file.body.add(Class((clazz) { 64 | clazz..name = '${pascal}Serializer'; 65 | if (ctx.constructorParameters.isEmpty) { 66 | clazz 67 | ..extend = TypeReference((b) => b 68 | ..symbol = 'Codec' 69 | ..types.addAll([ctx.modelClassType, refer('Map')])); 70 | 71 | // Add constructor, Codec impl, etc. 72 | clazz.constructors.add(Constructor((b) => b..constant = true)); 73 | clazz.methods.add(Method((b) => b 74 | ..name = 'encoder' 75 | ..type = MethodType.getter 76 | ..annotations.add(refer('override')) 77 | ..body = refer('${pascal}Encoder').constInstance([]).code)); 78 | clazz.methods.add(Method((b) => b 79 | ..name = 'decoder' 80 | ..type = MethodType.getter 81 | ..annotations.add(refer('override')) 82 | ..body = refer('${pascal}Decoder').constInstance([]).code)); 83 | } else { 84 | clazz.abstract = true; 85 | } 86 | 87 | if (serializers.contains(Serializers.map)) { 88 | generateFromMapMethod(clazz, ctx, file); 89 | } 90 | 91 | if (serializers.contains(Serializers.map) || 92 | serializers.contains(Serializers.json)) { 93 | generateToMapMethod(clazz, ctx, file); 94 | } 95 | })); 96 | } 97 | 98 | void generateToMapMethod( 99 | ClassBuilder clazz, BuildContext ctx, LibraryBuilder file) { 100 | clazz.methods.add(Method((method) { 101 | method 102 | ..static = true 103 | ..name = 'toMap' 104 | ..returns = Reference('Map') 105 | ..requiredParameters.add(Parameter((b) { 106 | b 107 | ..name = 'model' 108 | ..type = refer(ctx.originalClassName); 109 | })); 110 | 111 | var buf = StringBuffer(); 112 | 113 | ctx.requiredFields.forEach((key, msg) { 114 | if (ctx.excluded[key]?.canSerialize == false) return; 115 | buf.writeln(''' 116 | if (model.$key == null) { 117 | throw FormatException("$msg"); 118 | } 119 | '''); 120 | }); 121 | 122 | buf.writeln('return {'); 123 | int i = 0; 124 | 125 | // Add named parameters 126 | for (var field in ctx.fields) { 127 | var type = ctx.resolveSerializedFieldType(field.name); 128 | 129 | // Skip excluded fields 130 | if (ctx.excluded[field.name]?.canSerialize == false) continue; 131 | 132 | var alias = ctx.resolveFieldName(field.name); 133 | 134 | if (i++ > 0) buf.write(', '); 135 | 136 | String serializedRepresentation = 'model.${field.name}'; 137 | 138 | String serializerToMap(ReCase rc, String value) { 139 | // if (rc.pascalCase == ctx.modelClassName) { 140 | // return '($value)?.toJson()'; 141 | // } 142 | return '${rc.pascalCase}Serializer.toMap($value)'; 143 | } 144 | 145 | if (ctx.fieldInfo[field.name]?.serializer != null) { 146 | var name = MirrorSystem.getName(ctx.fieldInfo[field.name].serializer); 147 | serializedRepresentation = '$name(model.${field.name})'; 148 | } 149 | 150 | // Serialize dates 151 | else if (dateTimeTypeChecker.isAssignableFromType(type)) { 152 | serializedRepresentation = 'model.${field.name}?.toIso8601String()'; 153 | } 154 | 155 | // Serialize model classes via `XSerializer.toMap` 156 | else if (isModelClass(type)) { 157 | var rc = ReCase(type.name); 158 | serializedRepresentation = 159 | '${serializerToMap(rc, 'model.${field.name}')}'; 160 | } else if (type is InterfaceType) { 161 | if (isListOfModelType(type)) { 162 | var name = type.typeArguments[0].name; 163 | if (name.startsWith('_')) name = name.substring(1); 164 | var rc = ReCase(name); 165 | var m = serializerToMap(rc, 'm'); 166 | serializedRepresentation = ''' 167 | model.${field.name} 168 | ?.map((m) => $m) 169 | ?.toList()'''; 170 | } else if (isMapToModelType(type)) { 171 | var rc = ReCase(type.typeArguments[1].name); 172 | serializedRepresentation = 173 | '''model.${field.name}.keys?.fold({}, (map, key) { 174 | return map..[key] = 175 | ${serializerToMap(rc, 'model.${field.name}[key]')}; 176 | })'''; 177 | } else if (type.element.isEnum) { 178 | serializedRepresentation = ''' 179 | model.${field.name} == null ? 180 | null 181 | : ${type.name}.values.indexOf(model.${field.name}) 182 | '''; 183 | } else if (const TypeChecker.fromRuntime(Uint8List) 184 | .isAssignableFromType(type)) { 185 | serializedRepresentation = ''' 186 | model.${field.name} == null ? 187 | null 188 | : base64.encode(model.${field.name}) 189 | '''; 190 | } 191 | } 192 | 193 | buf.write("'$alias': $serializedRepresentation"); 194 | } 195 | 196 | buf.write('};'); 197 | method.body = Block.of([ 198 | Code('if (model == null) { return null; }'), 199 | Code(buf.toString()), 200 | ]); 201 | })); 202 | } 203 | 204 | void generateFromMapMethod( 205 | ClassBuilder clazz, BuildContext ctx, LibraryBuilder file) { 206 | clazz.methods.add(Method((method) { 207 | method 208 | ..static = true 209 | ..name = 'fromMap' 210 | ..returns = ctx.modelClassType 211 | ..requiredParameters.add( 212 | Parameter((b) => b 213 | ..name = 'map' 214 | ..type = Reference('Map')), 215 | ); 216 | 217 | // Add all `super` params 218 | if (ctx.constructorParameters.isNotEmpty) { 219 | for (var param in ctx.constructorParameters) { 220 | method.requiredParameters.add(Parameter((b) => b 221 | ..name = param.name 222 | ..type = convertTypeReference(param.type))); 223 | } 224 | } 225 | 226 | var buf = StringBuffer(); 227 | 228 | ctx.requiredFields.forEach((key, msg) { 229 | if (ctx.excluded[key]?.canDeserialize == false) return; 230 | var name = ctx.resolveFieldName(key); 231 | buf.writeln(''' 232 | if (map['$name'] == null) { 233 | throw FormatException("$msg"); 234 | } 235 | '''); 236 | }); 237 | 238 | buf.writeln('return ${ctx.modelClassName}('); 239 | int i = 0; 240 | 241 | for (var param in ctx.constructorParameters) { 242 | if (i++ > 0) buf.write(', '); 243 | buf.write(param.name); 244 | } 245 | 246 | for (var field in ctx.fields) { 247 | var type = ctx.resolveSerializedFieldType(field.name); 248 | 249 | if (ctx.excluded[field.name]?.canDeserialize == false) continue; 250 | 251 | var alias = ctx.resolveFieldName(field.name); 252 | 253 | if (i++ > 0) buf.write(', '); 254 | 255 | String deserializedRepresentation = 256 | "map['$alias'] as ${typeToString(type)}"; 257 | 258 | var defaultValue = 'null'; 259 | var existingDefault = ctx.defaults[field.name]; 260 | 261 | if (existingDefault != null) { 262 | defaultValue = dartObjectToString(existingDefault); 263 | deserializedRepresentation = 264 | '$deserializedRepresentation ?? $defaultValue'; 265 | } 266 | 267 | if (ctx.fieldInfo[field.name]?.deserializer != null) { 268 | var name = 269 | MirrorSystem.getName(ctx.fieldInfo[field.name].deserializer); 270 | deserializedRepresentation = "$name(map['$alias'])"; 271 | } else if (dateTimeTypeChecker.isAssignableFromType(type)) { 272 | deserializedRepresentation = "map['$alias'] != null ? " 273 | "(map['$alias'] is DateTime ? (map['$alias'] as DateTime) : DateTime.parse(map['$alias'].toString()))" 274 | " : $defaultValue"; 275 | } 276 | 277 | // Serialize model classes via `XSerializer.toMap` 278 | else if (isModelClass(type)) { 279 | var rc = ReCase(type.name); 280 | deserializedRepresentation = "map['$alias'] != null" 281 | " ? ${rc.pascalCase}Serializer.fromMap(map['$alias'] as Map)" 282 | " : $defaultValue"; 283 | } else if (type is InterfaceType) { 284 | if (isListOfModelType(type)) { 285 | var rc = ReCase(type.typeArguments[0].name); 286 | deserializedRepresentation = "map['$alias'] is Iterable" 287 | " ? List.unmodifiable(((map['$alias'] as Iterable)" 288 | ".whereType())" 289 | ".map(${rc.pascalCase}Serializer.fromMap))" 290 | " : $defaultValue"; 291 | } else if (isMapToModelType(type)) { 292 | var rc = ReCase(type.typeArguments[1].name); 293 | deserializedRepresentation = ''' 294 | map['$alias'] is Map 295 | ? Map.unmodifiable((map['$alias'] as Map).keys.fold({}, (out, key) { 296 | return out..[key] = ${rc.pascalCase}Serializer 297 | .fromMap(((map['$alias'] as Map)[key]) as Map); 298 | })) 299 | : $defaultValue 300 | '''; 301 | } else if (type.element.isEnum) { 302 | deserializedRepresentation = ''' 303 | map['$alias'] is ${type.name} 304 | ? (map['$alias'] as ${type.name}) 305 | : 306 | ( 307 | map['$alias'] is int 308 | ? ${type.name}.values[map['$alias'] as int] 309 | : $defaultValue 310 | ) 311 | '''; 312 | } else if (const TypeChecker.fromRuntime(List) 313 | .isAssignableFromType(type) && 314 | type.typeArguments.length == 1) { 315 | var arg = convertTypeReference(type.typeArguments[0]) 316 | .accept(DartEmitter()); 317 | deserializedRepresentation = ''' 318 | map['$alias'] is Iterable 319 | ? (map['$alias'] as Iterable).cast<$arg>().toList() 320 | : $defaultValue 321 | '''; 322 | } else if (const TypeChecker.fromRuntime(Map) 323 | .isAssignableFromType(type) && 324 | type.typeArguments.length == 2) { 325 | var key = convertTypeReference(type.typeArguments[0]) 326 | .accept(DartEmitter()); 327 | var value = convertTypeReference(type.typeArguments[1]) 328 | .accept(DartEmitter()); 329 | deserializedRepresentation = ''' 330 | map['$alias'] is Map 331 | ? (map['$alias'] as Map).cast<$key, $value>() 332 | : $defaultValue 333 | '''; 334 | } else if (const TypeChecker.fromRuntime(Uint8List) 335 | .isAssignableFromType(type)) { 336 | deserializedRepresentation = ''' 337 | map['$alias'] is Uint8List 338 | ? (map['$alias'] as Uint8List) 339 | : 340 | ( 341 | map['$alias'] is Iterable 342 | ? Uint8List.fromList((map['$alias'] as Iterable).toList()) 343 | : 344 | ( 345 | map['$alias'] is String 346 | ? Uint8List.fromList(base64.decode(map['$alias'] as String)) 347 | : $defaultValue 348 | ) 349 | ) 350 | '''; 351 | } 352 | } 353 | 354 | buf.write('${field.name}: $deserializedRepresentation'); 355 | } 356 | 357 | buf.write(');'); 358 | method.body = Code(buf.toString()); 359 | })); 360 | } 361 | 362 | void generateFieldsClass(BuildContext ctx, LibraryBuilder file) { 363 | file.body.add(Class((clazz) { 364 | clazz 365 | ..abstract = true 366 | ..name = '${ctx.modelClassNameRecase.pascalCase}Fields'; 367 | 368 | clazz.fields.add(Field((b) { 369 | b 370 | ..static = true 371 | ..modifier = FieldModifier.constant 372 | ..type = TypeReference((b) => b 373 | ..symbol = 'List' 374 | ..types.add(refer('String'))) 375 | ..name = 'allFields' 376 | ..assignment = literalConstList( 377 | ctx.fields.map((f) => refer(f.name)).toList(), 378 | refer('String')) 379 | .code; 380 | })); 381 | 382 | for (var field in ctx.fields) { 383 | clazz.fields.add(Field((b) { 384 | b 385 | ..static = true 386 | ..modifier = FieldModifier.constant 387 | ..type = Reference('String') 388 | ..name = field.name 389 | ..assignment = Code("'${ctx.resolveFieldName(field.name)}'"); 390 | })); 391 | } 392 | })); 393 | } 394 | } 395 | -------------------------------------------------------------------------------- /angel_serialize_generator/test/models/book.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of angel_serialize.test.models.book; 4 | 5 | // ************************************************************************** 6 | // JsonModelGenerator 7 | // ************************************************************************** 8 | 9 | @generatedSerializable 10 | @pragma('hello') 11 | @SerializableField(alias: 'omg') 12 | class Book extends _Book { 13 | Book( 14 | {this.id, 15 | this.createdAt, 16 | this.updatedAt, 17 | this.author, 18 | this.title, 19 | this.description, 20 | this.pageCount, 21 | List notModels, 22 | this.camelCaseString}) 23 | : this.notModels = List.unmodifiable(notModels ?? []); 24 | 25 | /// A unique identifier corresponding to this item. 26 | @override 27 | String id; 28 | 29 | /// The time at which this item was created. 30 | @override 31 | DateTime createdAt; 32 | 33 | /// The last time at which this item was updated. 34 | @override 35 | DateTime updatedAt; 36 | 37 | @override 38 | String author; 39 | 40 | @override 41 | String title; 42 | 43 | @override 44 | String description; 45 | 46 | /// The number of pages the book has. 47 | @override 48 | int pageCount; 49 | 50 | @override 51 | List notModels; 52 | 53 | @override 54 | String camelCaseString; 55 | 56 | Book copyWith( 57 | {String id, 58 | DateTime createdAt, 59 | DateTime updatedAt, 60 | String author, 61 | String title, 62 | String description, 63 | int pageCount, 64 | List notModels, 65 | String camelCaseString}) { 66 | return Book( 67 | id: id ?? this.id, 68 | createdAt: createdAt ?? this.createdAt, 69 | updatedAt: updatedAt ?? this.updatedAt, 70 | author: author ?? this.author, 71 | title: title ?? this.title, 72 | description: description ?? this.description, 73 | pageCount: pageCount ?? this.pageCount, 74 | notModels: notModels ?? this.notModels, 75 | camelCaseString: camelCaseString ?? this.camelCaseString); 76 | } 77 | 78 | bool operator ==(other) { 79 | return other is _Book && 80 | other.id == id && 81 | other.createdAt == createdAt && 82 | other.updatedAt == updatedAt && 83 | other.author == author && 84 | other.title == title && 85 | other.description == description && 86 | other.pageCount == pageCount && 87 | ListEquality(DefaultEquality()) 88 | .equals(other.notModels, notModels) && 89 | other.camelCaseString == camelCaseString; 90 | } 91 | 92 | @override 93 | int get hashCode { 94 | return hashObjects([ 95 | id, 96 | createdAt, 97 | updatedAt, 98 | author, 99 | title, 100 | description, 101 | pageCount, 102 | notModels, 103 | camelCaseString 104 | ]); 105 | } 106 | 107 | @override 108 | String toString() { 109 | return "Book(id=$id, createdAt=$createdAt, updatedAt=$updatedAt, author=$author, title=$title, description=$description, pageCount=$pageCount, notModels=$notModels, camelCaseString=$camelCaseString)"; 110 | } 111 | 112 | Map toJson() { 113 | return BookSerializer.toMap(this); 114 | } 115 | } 116 | 117 | @generatedSerializable 118 | class Author extends _Author { 119 | Author( 120 | {this.id, 121 | this.createdAt, 122 | this.updatedAt, 123 | @required this.name, 124 | @required this.age, 125 | List<_Book> books, 126 | this.newestBook, 127 | this.secret, 128 | this.obscured}) 129 | : this.books = List.unmodifiable(books ?? []); 130 | 131 | /// A unique identifier corresponding to this item. 132 | @override 133 | String id; 134 | 135 | /// The time at which this item was created. 136 | @override 137 | DateTime createdAt; 138 | 139 | /// The last time at which this item was updated. 140 | @override 141 | DateTime updatedAt; 142 | 143 | @override 144 | final String name; 145 | 146 | @override 147 | final int age; 148 | 149 | @override 150 | final List<_Book> books; 151 | 152 | /// The newest book. 153 | @override 154 | final _Book newestBook; 155 | 156 | @override 157 | final String secret; 158 | 159 | @override 160 | final String obscured; 161 | 162 | Author copyWith( 163 | {String id, 164 | DateTime createdAt, 165 | DateTime updatedAt, 166 | String name, 167 | int age, 168 | List<_Book> books, 169 | _Book newestBook, 170 | String secret, 171 | String obscured}) { 172 | return Author( 173 | id: id ?? this.id, 174 | createdAt: createdAt ?? this.createdAt, 175 | updatedAt: updatedAt ?? this.updatedAt, 176 | name: name ?? this.name, 177 | age: age ?? this.age, 178 | books: books ?? this.books, 179 | newestBook: newestBook ?? this.newestBook, 180 | secret: secret ?? this.secret, 181 | obscured: obscured ?? this.obscured); 182 | } 183 | 184 | bool operator ==(other) { 185 | return other is _Author && 186 | other.id == id && 187 | other.createdAt == createdAt && 188 | other.updatedAt == updatedAt && 189 | other.name == name && 190 | other.age == age && 191 | ListEquality<_Book>(DefaultEquality<_Book>()) 192 | .equals(other.books, books) && 193 | other.newestBook == newestBook && 194 | other.secret == secret && 195 | other.obscured == obscured; 196 | } 197 | 198 | @override 199 | int get hashCode { 200 | return hashObjects([ 201 | id, 202 | createdAt, 203 | updatedAt, 204 | name, 205 | age, 206 | books, 207 | newestBook, 208 | secret, 209 | obscured 210 | ]); 211 | } 212 | 213 | @override 214 | String toString() { 215 | return "Author(id=$id, createdAt=$createdAt, updatedAt=$updatedAt, name=$name, age=$age, books=$books, newestBook=$newestBook, secret=$secret, obscured=$obscured)"; 216 | } 217 | 218 | Map toJson() { 219 | return AuthorSerializer.toMap(this); 220 | } 221 | } 222 | 223 | @generatedSerializable 224 | class Library extends _Library { 225 | Library( 226 | {this.id, this.createdAt, this.updatedAt, Map collection}) 227 | : this.collection = Map.unmodifiable(collection ?? {}); 228 | 229 | /// A unique identifier corresponding to this item. 230 | @override 231 | String id; 232 | 233 | /// The time at which this item was created. 234 | @override 235 | DateTime createdAt; 236 | 237 | /// The last time at which this item was updated. 238 | @override 239 | DateTime updatedAt; 240 | 241 | @override 242 | final Map collection; 243 | 244 | Library copyWith( 245 | {String id, 246 | DateTime createdAt, 247 | DateTime updatedAt, 248 | Map collection}) { 249 | return Library( 250 | id: id ?? this.id, 251 | createdAt: createdAt ?? this.createdAt, 252 | updatedAt: updatedAt ?? this.updatedAt, 253 | collection: collection ?? this.collection); 254 | } 255 | 256 | bool operator ==(other) { 257 | return other is _Library && 258 | other.id == id && 259 | other.createdAt == createdAt && 260 | other.updatedAt == updatedAt && 261 | MapEquality( 262 | keys: DefaultEquality(), 263 | values: DefaultEquality<_Book>()) 264 | .equals(other.collection, collection); 265 | } 266 | 267 | @override 268 | int get hashCode { 269 | return hashObjects([id, createdAt, updatedAt, collection]); 270 | } 271 | 272 | @override 273 | String toString() { 274 | return "Library(id=$id, createdAt=$createdAt, updatedAt=$updatedAt, collection=$collection)"; 275 | } 276 | 277 | Map toJson() { 278 | return LibrarySerializer.toMap(this); 279 | } 280 | } 281 | 282 | @generatedSerializable 283 | class Bookmark extends _Bookmark { 284 | Bookmark(_Book book, 285 | {this.id, 286 | this.createdAt, 287 | this.updatedAt, 288 | List history, 289 | @required this.page, 290 | this.comment}) 291 | : this.history = List.unmodifiable(history ?? []), 292 | super(book); 293 | 294 | /// A unique identifier corresponding to this item. 295 | @override 296 | String id; 297 | 298 | /// The time at which this item was created. 299 | @override 300 | DateTime createdAt; 301 | 302 | /// The last time at which this item was updated. 303 | @override 304 | DateTime updatedAt; 305 | 306 | @override 307 | final List history; 308 | 309 | @override 310 | final int page; 311 | 312 | @override 313 | final String comment; 314 | 315 | Bookmark copyWith(_Book book, 316 | {String id, 317 | DateTime createdAt, 318 | DateTime updatedAt, 319 | List history, 320 | int page, 321 | String comment}) { 322 | return Bookmark(book, 323 | id: id ?? this.id, 324 | createdAt: createdAt ?? this.createdAt, 325 | updatedAt: updatedAt ?? this.updatedAt, 326 | history: history ?? this.history, 327 | page: page ?? this.page, 328 | comment: comment ?? this.comment); 329 | } 330 | 331 | bool operator ==(other) { 332 | return other is _Bookmark && 333 | other.id == id && 334 | other.createdAt == createdAt && 335 | other.updatedAt == updatedAt && 336 | ListEquality(DefaultEquality()) 337 | .equals(other.history, history) && 338 | other.page == page && 339 | other.comment == comment; 340 | } 341 | 342 | @override 343 | int get hashCode { 344 | return hashObjects([id, createdAt, updatedAt, history, page, comment]); 345 | } 346 | 347 | @override 348 | String toString() { 349 | return "Bookmark(id=$id, createdAt=$createdAt, updatedAt=$updatedAt, history=$history, page=$page, comment=$comment)"; 350 | } 351 | 352 | Map toJson() { 353 | return BookmarkSerializer.toMap(this); 354 | } 355 | } 356 | 357 | // ************************************************************************** 358 | // SerializerGenerator 359 | // ************************************************************************** 360 | 361 | const BookSerializer bookSerializer = BookSerializer(); 362 | 363 | class BookEncoder extends Converter { 364 | const BookEncoder(); 365 | 366 | @override 367 | Map convert(Book model) => BookSerializer.toMap(model); 368 | } 369 | 370 | class BookDecoder extends Converter { 371 | const BookDecoder(); 372 | 373 | @override 374 | Book convert(Map map) => BookSerializer.fromMap(map); 375 | } 376 | 377 | class BookSerializer extends Codec { 378 | const BookSerializer(); 379 | 380 | @override 381 | get encoder => const BookEncoder(); 382 | @override 383 | get decoder => const BookDecoder(); 384 | static Book fromMap(Map map) { 385 | return Book( 386 | id: map['id'] as String, 387 | createdAt: map['created_at'] != null 388 | ? (map['created_at'] is DateTime 389 | ? (map['created_at'] as DateTime) 390 | : DateTime.parse(map['created_at'].toString())) 391 | : null, 392 | updatedAt: map['updated_at'] != null 393 | ? (map['updated_at'] is DateTime 394 | ? (map['updated_at'] as DateTime) 395 | : DateTime.parse(map['updated_at'].toString())) 396 | : null, 397 | author: map['author'] as String, 398 | title: map['title'] as String, 399 | description: map['description'] as String, 400 | pageCount: map['page_count'] as int, 401 | notModels: map['not_models'] is Iterable 402 | ? (map['not_models'] as Iterable).cast().toList() 403 | : null, 404 | camelCaseString: map['camelCase'] as String); 405 | } 406 | 407 | static Map toMap(_Book model) { 408 | if (model == null) { 409 | return null; 410 | } 411 | return { 412 | 'id': model.id, 413 | 'created_at': model.createdAt?.toIso8601String(), 414 | 'updated_at': model.updatedAt?.toIso8601String(), 415 | 'author': model.author, 416 | 'title': model.title, 417 | 'description': model.description, 418 | 'page_count': model.pageCount, 419 | 'not_models': model.notModels, 420 | 'camelCase': model.camelCaseString 421 | }; 422 | } 423 | } 424 | 425 | abstract class BookFields { 426 | static const List allFields = [ 427 | id, 428 | createdAt, 429 | updatedAt, 430 | author, 431 | title, 432 | description, 433 | pageCount, 434 | notModels, 435 | camelCaseString 436 | ]; 437 | 438 | static const String id = 'id'; 439 | 440 | static const String createdAt = 'created_at'; 441 | 442 | static const String updatedAt = 'updated_at'; 443 | 444 | static const String author = 'author'; 445 | 446 | static const String title = 'title'; 447 | 448 | static const String description = 'description'; 449 | 450 | static const String pageCount = 'page_count'; 451 | 452 | static const String notModels = 'not_models'; 453 | 454 | static const String camelCaseString = 'camelCase'; 455 | } 456 | 457 | const AuthorSerializer authorSerializer = AuthorSerializer(); 458 | 459 | class AuthorEncoder extends Converter { 460 | const AuthorEncoder(); 461 | 462 | @override 463 | Map convert(Author model) => AuthorSerializer.toMap(model); 464 | } 465 | 466 | class AuthorDecoder extends Converter { 467 | const AuthorDecoder(); 468 | 469 | @override 470 | Author convert(Map map) => AuthorSerializer.fromMap(map); 471 | } 472 | 473 | class AuthorSerializer extends Codec { 474 | const AuthorSerializer(); 475 | 476 | @override 477 | get encoder => const AuthorEncoder(); 478 | @override 479 | get decoder => const AuthorDecoder(); 480 | static Author fromMap(Map map) { 481 | if (map['name'] == null) { 482 | throw FormatException("Missing required field 'name' on Author."); 483 | } 484 | 485 | if (map['age'] == null) { 486 | throw FormatException("Custom message for missing `age`"); 487 | } 488 | 489 | return Author( 490 | id: map['id'] as String, 491 | createdAt: map['created_at'] != null 492 | ? (map['created_at'] is DateTime 493 | ? (map['created_at'] as DateTime) 494 | : DateTime.parse(map['created_at'].toString())) 495 | : null, 496 | updatedAt: map['updated_at'] != null 497 | ? (map['updated_at'] is DateTime 498 | ? (map['updated_at'] as DateTime) 499 | : DateTime.parse(map['updated_at'].toString())) 500 | : null, 501 | name: map['name'] as String, 502 | age: map['age'] as int, 503 | books: map['books'] is Iterable 504 | ? List.unmodifiable(((map['books'] as Iterable).whereType()) 505 | .map(BookSerializer.fromMap)) 506 | : null, 507 | newestBook: map['newest_book'] != null 508 | ? BookSerializer.fromMap(map['newest_book'] as Map) 509 | : null, 510 | obscured: map['obscured'] as String); 511 | } 512 | 513 | static Map toMap(_Author model) { 514 | if (model == null) { 515 | return null; 516 | } 517 | if (model.name == null) { 518 | throw FormatException("Missing required field 'name' on Author."); 519 | } 520 | 521 | if (model.age == null) { 522 | throw FormatException("Custom message for missing `age`"); 523 | } 524 | 525 | return { 526 | 'id': model.id, 527 | 'created_at': model.createdAt?.toIso8601String(), 528 | 'updated_at': model.updatedAt?.toIso8601String(), 529 | 'name': model.name, 530 | 'age': model.age, 531 | 'books': model.books?.map((m) => BookSerializer.toMap(m))?.toList(), 532 | 'newest_book': BookSerializer.toMap(model.newestBook) 533 | }; 534 | } 535 | } 536 | 537 | abstract class AuthorFields { 538 | static const List allFields = [ 539 | id, 540 | createdAt, 541 | updatedAt, 542 | name, 543 | age, 544 | books, 545 | newestBook, 546 | secret, 547 | obscured 548 | ]; 549 | 550 | static const String id = 'id'; 551 | 552 | static const String createdAt = 'created_at'; 553 | 554 | static const String updatedAt = 'updated_at'; 555 | 556 | static const String name = 'name'; 557 | 558 | static const String age = 'age'; 559 | 560 | static const String books = 'books'; 561 | 562 | static const String newestBook = 'newest_book'; 563 | 564 | static const String secret = 'secret'; 565 | 566 | static const String obscured = 'obscured'; 567 | } 568 | 569 | const LibrarySerializer librarySerializer = LibrarySerializer(); 570 | 571 | class LibraryEncoder extends Converter { 572 | const LibraryEncoder(); 573 | 574 | @override 575 | Map convert(Library model) => LibrarySerializer.toMap(model); 576 | } 577 | 578 | class LibraryDecoder extends Converter { 579 | const LibraryDecoder(); 580 | 581 | @override 582 | Library convert(Map map) => LibrarySerializer.fromMap(map); 583 | } 584 | 585 | class LibrarySerializer extends Codec { 586 | const LibrarySerializer(); 587 | 588 | @override 589 | get encoder => const LibraryEncoder(); 590 | @override 591 | get decoder => const LibraryDecoder(); 592 | static Library fromMap(Map map) { 593 | return Library( 594 | id: map['id'] as String, 595 | createdAt: map['created_at'] != null 596 | ? (map['created_at'] is DateTime 597 | ? (map['created_at'] as DateTime) 598 | : DateTime.parse(map['created_at'].toString())) 599 | : null, 600 | updatedAt: map['updated_at'] != null 601 | ? (map['updated_at'] is DateTime 602 | ? (map['updated_at'] as DateTime) 603 | : DateTime.parse(map['updated_at'].toString())) 604 | : null, 605 | collection: map['collection'] is Map 606 | ? Map.unmodifiable( 607 | (map['collection'] as Map).keys.fold({}, (out, key) { 608 | return out 609 | ..[key] = BookSerializer.fromMap( 610 | ((map['collection'] as Map)[key]) as Map); 611 | })) 612 | : null); 613 | } 614 | 615 | static Map toMap(_Library model) { 616 | if (model == null) { 617 | return null; 618 | } 619 | return { 620 | 'id': model.id, 621 | 'created_at': model.createdAt?.toIso8601String(), 622 | 'updated_at': model.updatedAt?.toIso8601String(), 623 | 'collection': model.collection.keys?.fold({}, (map, key) { 624 | return map..[key] = BookSerializer.toMap(model.collection[key]); 625 | }) 626 | }; 627 | } 628 | } 629 | 630 | abstract class LibraryFields { 631 | static const List allFields = [ 632 | id, 633 | createdAt, 634 | updatedAt, 635 | collection 636 | ]; 637 | 638 | static const String id = 'id'; 639 | 640 | static const String createdAt = 'created_at'; 641 | 642 | static const String updatedAt = 'updated_at'; 643 | 644 | static const String collection = 'collection'; 645 | } 646 | 647 | abstract class BookmarkSerializer { 648 | static Bookmark fromMap(Map map, _Book book) { 649 | if (map['page'] == null) { 650 | throw FormatException("Missing required field 'page' on Bookmark."); 651 | } 652 | 653 | return Bookmark(book, 654 | id: map['id'] as String, 655 | createdAt: map['created_at'] != null 656 | ? (map['created_at'] is DateTime 657 | ? (map['created_at'] as DateTime) 658 | : DateTime.parse(map['created_at'].toString())) 659 | : null, 660 | updatedAt: map['updated_at'] != null 661 | ? (map['updated_at'] is DateTime 662 | ? (map['updated_at'] as DateTime) 663 | : DateTime.parse(map['updated_at'].toString())) 664 | : null, 665 | history: map['history'] is Iterable 666 | ? (map['history'] as Iterable).cast().toList() 667 | : null, 668 | page: map['page'] as int, 669 | comment: map['comment'] as String); 670 | } 671 | 672 | static Map toMap(_Bookmark model) { 673 | if (model == null) { 674 | return null; 675 | } 676 | if (model.page == null) { 677 | throw FormatException("Missing required field 'page' on Bookmark."); 678 | } 679 | 680 | return { 681 | 'id': model.id, 682 | 'created_at': model.createdAt?.toIso8601String(), 683 | 'updated_at': model.updatedAt?.toIso8601String(), 684 | 'history': model.history, 685 | 'page': model.page, 686 | 'comment': model.comment 687 | }; 688 | } 689 | } 690 | 691 | abstract class BookmarkFields { 692 | static const List allFields = [ 693 | id, 694 | createdAt, 695 | updatedAt, 696 | history, 697 | page, 698 | comment 699 | ]; 700 | 701 | static const String id = 'id'; 702 | 703 | static const String createdAt = 'created_at'; 704 | 705 | static const String updatedAt = 'updated_at'; 706 | 707 | static const String history = 'history'; 708 | 709 | static const String page = 'page'; 710 | 711 | static const String comment = 'comment'; 712 | } 713 | --------------------------------------------------------------------------------